edge_rider 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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)