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 [](https://travis-ci.org/makandra/edge_rider)
|
2
2
|
====================================
|
3
3
|
|
4
|
-
Power tools for
|
4
|
+
Power tools for ActiveRecord relations (scopes)
|
5
5
|
-------------------------------------------------
|
6
6
|
|
7
|
-
|
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
|
data/lib/edge_rider/version.rb
CHANGED
data/lib/edge_rider.rb
CHANGED
@@ -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:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
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-
|
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.
|
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)
|