edge_rider 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,10 +1,184 @@
1
1
  Edge Rider [![Build Status](https://secure.travis-ci.org/makandra/edge_rider.png?branch=master)](https://travis-ci.org/makandra/edge_rider)
2
2
  ====================================
3
3
 
4
- Power tools for Active Record relations (scopes)
4
+ Power tools for ActiveRecord relations (scopes)
5
5
  -------------------------------------------------
6
6
 
7
- TODO: Write README
7
+ In ActiveRecord, relations (or scopes) allow you to construct complex queries piece-by-piece
8
+ and then trigger a query or update at a precisely defined moment. If you write any kind
9
+ of scalable code with ActiveRecord, you are probably using relations (or scopes) to do it.
10
+
11
+ Edge Rider was created with two intents:
12
+
13
+ 1. Provides a number of utility methods to facilitate hardcore work with relations.
14
+ 2. Provide a stable API for working with relations across multiple versions of Rails (since
15
+ Rails has a tradition of breaking details of its relation API every other release).
16
+
17
+ It has been tested with Rails 2.3, 3.0 and 3.2.
18
+
19
+
20
+ Usage
21
+ -----
22
+
23
+ ### Traversing a relation along an association
24
+
25
+ Edge Rider gives your relations a method `#traverse_association` which
26
+ returns a new relation by "pivoting" around a named association.
27
+
28
+ Say we have a `Post` model and each `Post` belongs to an author:
29
+
30
+ class Post < ActiveRecord::Base
31
+ belongs_to :author
32
+ end
33
+
34
+ To turn a relation of posts into a relation of its authors:
35
+
36
+ posts = Post.where(:archived => false)
37
+ authors = posts.traverse_association(:author)
38
+
39
+ You can traverse multiple associations in a single call.
40
+ E.g. to turn a relation of posts into a relation of all posts of their authors:
41
+
42
+ posts = Post.where(:archived => false)
43
+ posts_by_same_authors = posts.traverse_association(:author, :posts)
44
+
45
+ *Implementation note:* The traversal is achieved internally by collecting all foreign keys in the current relation
46
+ and return a new relation with an `IN(...)` query (which is very efficient even for many thousand keys).
47
+ This means every association that you pivot around will trigger one SQL query.
48
+
49
+
50
+ ### Efficiently collect all record IDs in a relation
51
+
52
+ You often want to retrieve an array of all record IDs in a relation.
53
+
54
+ You should **not** use `relation.collect(&:id)` for this because a call like that
55
+ will instantiate a potentially large number of ActiveRecord objects only to collect
56
+ its ID.
57
+
58
+ Edge Rider has a better way. Your relations gain a method `#collect_ids` that will
59
+ fetch all IDs in a single query without instantiating a single ActiveRecord object:
60
+
61
+ posts = Post.where(:archived => false)
62
+ post_ids = posts.collect_ids
63
+
64
+ *Implemenation note:* In Rails 3.2+, `#collect_ids` delegates to [`#pluck`](http://apidock.com/rails/ActiveRecord/Calculations/pluck),
65
+ which can be used for the same purpose.
66
+
67
+
68
+ ### Collect record IDs from any kind of object
69
+
70
+ When writing a method that filters by record IDs, you can make it more useful by accepting
71
+ any kind of argument that can be turned into a list of IDs:
72
+
73
+ Post.by_author(1)
74
+ Post.by_author([1, 2, 3])
75
+ Post.by_author(Author.find(1))
76
+ Post.by_author([Author.find(1), Author.find(2)])
77
+ Post.by_author(Author.active)
78
+
79
+ For this use case Edge Rider defines `#collect_ids` on many different types:
80
+
81
+ Post.where(:id => [1, 2]).collect_ids # => [1, 2, 3]
82
+ [Post.find(1), Post.find(2)].collect_ids # => [1, 2]
83
+ Post.find(1).collect_ids # => [1]
84
+ [1, 2, 3].collect_ids # => [1, 2, 3]
85
+ 1.collect_ids # => [1]
86
+
87
+ You can now write `Post.by_author` from the example above without a single `if` or `case`:
88
+
89
+ class Post < ActiveRecord::Base
90
+
91
+ belongs_to :author
92
+
93
+ def self.for_author(author_or_authors)
94
+ where(:author_id => author_or_authors.collect_ids)
95
+ end
96
+
97
+ end
98
+
99
+
100
+ ### Efficiently collect all values in a relation's column
101
+
102
+ You often want to ask a relation for an array of all values ofin a given column.
103
+
104
+ You should **not** use `relation.collect(&:column)` for this because a call like that
105
+ will instantiate a potentially large number of ActiveRecord objects only to collect
106
+ its column value.
107
+
108
+ Edge Rider has a better way. Your relations gain a method `#collect_column` that will
109
+ fetch all column values in a single query without instantiating a single ActiveRecord object:
110
+
111
+ posts = Post.where(:archived => false)
112
+ subjects = posts.collect_column(:subject)
113
+
114
+ *Implementation note:* In Rails 3.2+, `#collect_column` delegates to [`#pluck`](http://apidock.com/rails/ActiveRecord/Calculations/pluck),
115
+ which can be used for the same effect.
116
+
117
+ #### Collect unique values in a relation's column
118
+
119
+ If you only care about *unique* values, use the `:distinct => true` option:
120
+
121
+ posts = Post.where(:archived => false)
122
+ distinct_subjects = posts.collect_column(:subject, :distinct => true)
123
+
124
+ With this options duplicates are discarded by the database before making their way into Ruby.
125
+
126
+ *Implementation note:* In Rails 3.2+, the `:distinct` option is implemented with [`#uniq`](http://apidock.com/rails/ActiveRecord/QueryMethods/uniq)
127
+ which can be used for the same effect.
128
+
129
+
130
+ ### Retrieve the SQL a relation would produce
131
+
132
+ Sometimes it is useful to ask a relation which SQL query it would trigger,
133
+ if it was evaluated right now. For this, Edge Rider gives your relations a method
134
+ `#to_sql`:
135
+
136
+ # Rails 2 scope
137
+ Post.scoped(:conditions => { :id => [1, 2] }).to_sql
138
+ # => SELECT `posts`.* FROM `posts` WHERE `posts.id` IN (1, 2)
139
+
140
+ # Rails 3 relation
141
+ Post.where(:id => [1, 2]).to_sql
142
+ # => SELECT `posts`.* FROM `posts` WHERE `posts.id` IN (1, 2)
143
+
144
+ *Implementation note*: Rails 3+ implements `#to_sql`. Edge Rider backports this method to Rails 2 so you can use it
145
+ regardless of your Rails version.
146
+
147
+
148
+ ### Simplify a complex relation for better chainability
149
+
150
+ In theory you can take any relation and extend it with additional joins or conditions.
151
+ We call this *chaining** relations.
152
+
153
+ In practice chaining becomes problematic as relation chains grow more complex.
154
+ In particular having JOINs in your relation will reduce the relations's ability to be chained with additional JOINs
155
+ without crashes or side effects. This is because ActiveRecord doesn't really "understand" your relation chain, it only
156
+ mashes together strings that mostly happen to look like a MySQL query in the end.
157
+
158
+ Edge Rider gives your relations a new method `#to_id_query`:
159
+
160
+ Site.joins(:user).where(:users => { :name => 'Bruce' }).to_id_query
161
+
162
+ `#to_id_query` will immediately run an SQL query where it collects all the IDs that match your relation:
163
+
164
+ SELECT sites.id FROM sites INNER JOIN users WHERE sites.user_id = sites.id AND users.name = 'Bruce'
165
+
166
+ It now uses these IDs to return a new relation that has **no joins** and a single condition on the `id` column:
167
+
168
+ SELECT * FROM sites WHERE sites.user_id IN (3, 17, 103)
169
+
170
+
171
+ ### Preload associations for loaded ActiveRecords
172
+
173
+ Sometimes you want to fetch associations for records that you already instantiated, e.g. when it has deeply nested associations.
174
+
175
+ Edge Rider gives your model classes a method `.preload_associations`. The method can be used to preload associations for loaded objects like this:
176
+
177
+ @user = User.find(params[:id])
178
+ User.preload_associations [@user], { :threads => { :posts => :author }, :messages => :sender }
179
+
180
+ *Implementation note*: Rails 2.3 and Rails 3.0 already has a method [`.preload_associations`](http://apidock.com/rails/ActiveRecord/AssociationPreload/ClassMethods/preload_associations)
181
+ which Edge Rider merely makes public. Edge Rider ports this method forward to Rails 3.1+.
8
182
 
9
183
 
10
184
  Installation
@@ -17,7 +191,6 @@ In your `Gemfile` say:
17
191
  Now run `bundle install` and restart your server.
18
192
 
19
193
 
20
-
21
194
  Development
22
195
  -----------
23
196
 
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ module EdgeRider
4
+ module PreloadAssociations
5
+
6
+ def preload_associations(*args)
7
+ ActiveRecord::Associations::Preloader.new(*args).run
8
+ end
9
+
10
+ if ActiveRecord::Base.respond_to?(:preload_associations, true) # Rails 2.3, Rails 3.0
11
+ ActiveRecord::Base.class_eval do
12
+ class << self
13
+ public :preload_associations
14
+ end
15
+ end
16
+ else # Rails 3.2+
17
+ ActiveRecord::Base.send(:extend, self)
18
+ end
19
+
20
+ end
21
+ end
@@ -15,13 +15,13 @@ module EdgeRider
15
15
 
16
16
  raise NotImplementedError if reflection.options[:conditions].present?
17
17
 
18
- if reflection.macro == :belongs_to
18
+ if reflection.macro == :belongs_to # belongs_to
19
19
  ids = scope.collect_column(foreign_key, :distinct => true)
20
20
  scope = EdgeRider::Util.exclusive_query(reflection.klass, :id => ids)
21
21
  elsif reflection.macro == :has_many || reflection.macro == :has_one
22
- if reflection.through_reflection
22
+ if reflection.through_reflection # has_many :through
23
23
  scope = scope.traverse_association(reflection.through_reflection.name, reflection.source_reflection.name)
24
- else
24
+ else # has_many or has_one
25
25
  ids = scope.collect_ids
26
26
  scope = EdgeRider::Util.exclusive_query(reflection.klass, foreign_key => ids)
27
27
  end
@@ -1,3 +1,3 @@
1
1
  module EdgeRider
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/edge_rider.rb CHANGED
@@ -5,6 +5,7 @@ require 'edge_rider/util'
5
5
 
6
6
  require 'edge_rider/collect_column'
7
7
  require 'edge_rider/collect_ids'
8
+ require 'edge_rider/preload_associations'
8
9
  require 'edge_rider/traverse_association'
9
10
  require 'edge_rider/to_id_query'
10
11
  require 'edge_rider/to_sql'
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe EdgeRider::PreloadAssociations do
4
+
5
+ it 'should preload the given named associations so they are no longer fetched lazily' do
6
+ forum = Forum.create!
7
+ topic = Topic.create!(:forum => forum)
8
+ post = Post.create!(:topic => topic)
9
+ Forum.preload_associations([forum], :topics => :posts)
10
+ Topic.should_not_receive(:new)
11
+ Post.should_not_receive(:new)
12
+ forum.topics.collect(&:posts)
13
+ end
14
+
15
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: edge_rider
3
3
  version: !ruby/object:Gem::Version
4
- hash: 25
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 1
10
- version: 0.1.1
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Henning Koch
@@ -15,7 +15,8 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2013-02-11 00:00:00 Z
18
+ date: 2013-02-14 00:00:00 +01:00
19
+ default_executable:
19
20
  dependencies:
20
21
  - !ruby/object:Gem::Dependency
21
22
  name: rails
@@ -50,6 +51,7 @@ files:
50
51
  - lib/edge_rider/collect_column.rb
51
52
  - lib/edge_rider/collect_ids.rb
52
53
  - lib/edge_rider/development.rb
54
+ - lib/edge_rider/preload_associations.rb
53
55
  - lib/edge_rider/to_id_query.rb
54
56
  - lib/edge_rider/to_sql.rb
55
57
  - lib/edge_rider/traverse_association.rb
@@ -118,10 +120,12 @@ files:
118
120
  - spec/shared/app_root/db/migrate/001_create_test_tables.rb
119
121
  - spec/shared/spec/edge_rider/collect_column_spec.rb
120
122
  - spec/shared/spec/edge_rider/collect_ids_spec.rb
123
+ - spec/shared/spec/edge_rider/preload_associations_spec.rb
121
124
  - spec/shared/spec/edge_rider/to_id_query_spec.rb
122
125
  - spec/shared/spec/edge_rider/to_sql_spec.rb
123
126
  - spec/shared/spec/edge_rider/traverse_association_spec.rb
124
127
  - spec/shared/spec/edge_rider/util_spec.rb
128
+ has_rdoc: true
125
129
  homepage: https://github.com/makandra/edge_rider
126
130
  licenses: []
127
131
 
@@ -151,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
151
155
  requirements: []
152
156
 
153
157
  rubyforge_project:
154
- rubygems_version: 1.8.24
158
+ rubygems_version: 1.3.9.5
155
159
  signing_key:
156
160
  specification_version: 3
157
161
  summary: Power tools for ActiveRecord relations (scopes)