goldiloader 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3912274cc2f34e5dafb2a3b1d8ee8e3f6521885e
4
- data.tar.gz: 72e93497e463d924c5c06c49df9d10cb7427170a
3
+ metadata.gz: 5e5d374d8cba8f3ed0e484f0030a896af17c6315
4
+ data.tar.gz: 0749a259d93c68ff35c45207edfc7d87648b70c5
5
5
  SHA512:
6
- metadata.gz: 80396bbcaddfe170d4fd0db9a199839ef07c65d4fbfe8947de40dd31ab0500c86db372c11b105f3efaa980506402582af656da202088d7cd559c1f2d0dc10521
7
- data.tar.gz: 060d399492cbbcd68cdb053fbc08c91781826404678e694287768d770c42736b39e5ad8e06cbad4600c0b2262fee4af4df469232fd973b6ccadb562c969d34ec
6
+ metadata.gz: 436cb0208f7cba5db8779af7ef9ad3d539ed988a2e48db1d8cbd3735885330afccba1844f8bbe8f540d5c37fc652cfe59c4edae7dc0c68cbc7282e153a209880
7
+ data.tar.gz: 41c5e5916200de2d44aa8228d18aa2ddc01a2e38dd29b4526212c51a3a27dae028602866e815baeb889a6277c4d1a9ac000c4f96b876b51592152060b27ae85a
@@ -1,9 +1,9 @@
1
1
  language: ruby
2
2
  env:
3
3
  matrix:
4
- - RAILS_VERSION="~> 3.2.18" JRUBY_OPTS="$JRUBY_OPTS --debug"
5
- - RAILS_VERSION="~> 4.0.5" JRUBY_OPTS="$JRUBY_OPTS --debug"
6
- - RAILS_VERSION="~> 4.1.1" JRUBY_OPTS="$JRUBY_OPTS --debug"
4
+ - RAILS_VERSION="~> 3.2.19" JRUBY_OPTS="$JRUBY_OPTS --debug"
5
+ - RAILS_VERSION="~> 4.0.10" JRUBY_OPTS="$JRUBY_OPTS --debug"
6
+ - RAILS_VERSION="~> 4.1.6" JRUBY_OPTS="$JRUBY_OPTS --debug"
7
7
  rvm:
8
8
  - 1.9.3
9
9
  - 2.0.0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ### 0.0.6 (unreleased)
4
+ * Workaround [issue 16](https://github.com/salsify/goldiloader/issues/16) by not auto-eager loading
5
+ has_and_belongs_to_many associations with a uniq in Rails 3.2 since Rails doesn't eager load them
6
+ properly.
7
+ * Fix [issue 17](https://github.com/salsify/goldiloader/issues/17) - models eager loaded via an explicit
8
+ call to eager_load now auto eager load nested models.
9
+
3
10
  ### 0.0.5
4
11
 
5
12
  * Fix ActiveRecord and ActiveSupport dependencies to work with versions greater than 4.1.0. Thanks for the pull
data/README.md CHANGED
@@ -68,7 +68,7 @@ Or install it yourself as:
68
68
 
69
69
  ## Usage
70
70
 
71
- By default all associations will be automatically eager loaded when they are first accessed so hopefully most use cases should require no additional configuration.
71
+ By default all associations will be automatically eager loaded when they are first accessed so hopefully most use cases should require no additional configuration. Note you're still free to explicitly eager load associations via `eager_load`, `includes`, or `preload`.
72
72
 
73
73
  ### Association Options
74
74
 
@@ -124,9 +124,55 @@ class Blog < ActiveRecord::Base
124
124
  end
125
125
  ```
126
126
 
127
+ ## Limitations
128
+
129
+ Goldiloader leverages the ActiveRecord eager loader so it shares some of the same limitations.
130
+
131
+ ### has_one associations that rely on a SQL limit
132
+
133
+ You should not try to auto eager load (or regular eager load) `has_one` associations that actually correspond to multiple records and rely on a SQL limit to only return one record. Consider the following example:
134
+
135
+ ```
136
+ class Blog < ActiveRecord::Base
137
+ has_many :posts
138
+ has_one :most_recent_post, -> { order(published_at: desc) }, class_name: 'Post'
139
+ end
140
+ ```
141
+
142
+ With standard Rails lazy loading the `most_recent_post` association is loaded with a query like this:
143
+
144
+ ```
145
+ SELECT * FROM posts WHERE blog_id = 1 ORDER BY published_at DESC LIMIT 1
146
+ ```
147
+
148
+ With auto eager loading (or regular eager loading) the `most_recent_post` association is loaded with a query like this:
149
+
150
+ ```
151
+ SELECT * FROM posts WHERE blog_id IN (1,2,3,4,5) ORDER BY published_at DESC
152
+ ```
153
+
154
+ Notice the SQL limit can no longer be used which results in fetching all posts for each blog. This can cause severe performance problems if there are a large number of posts.
155
+
156
+ ### Other Limitations
157
+
158
+ Associations with any of the following options cannot be eager loaded:
159
+
160
+ * `limit`
161
+ * `offset`
162
+ * `finder_sql`
163
+ * `group` (due to a Rails bug)
164
+ * `from` (due to a Rails bug)
165
+ * `unscope` (due to a Rails bug)
166
+ * `joins` (due to a Rails bug)
167
+ * `uniq` (only Rails 3.2 - due to a Rails bug)
168
+
169
+ Goldiloader detects associations with any of these options and disables automatic eager loading on them.
170
+
127
171
  ## Status
128
172
 
129
- This gem is tested with Rails 3.2, 4.0, and 4.1 using MRI 1.9.3, 2.0.0, 2.1.0 and JRuby in 1.9 mode. [Salsify](http://salsify.com) is not yet using this gem in production so proceed with caution. Let us know if you find any issues or have any other feedback.
173
+ This gem is tested with Rails 3.2, 4.0, and 4.1 using MRI 1.9.3, 2.0.0, 2.1.0 and JRuby in 1.9 mode.
174
+
175
+ Let us know if you find any issues or have any other feedback.
130
176
 
131
177
  ## Change log
132
178
 
@@ -32,9 +32,7 @@ ActiveRecord::Relation.class_eval do
32
32
  return exec_queries_without_auto_include if loaded?
33
33
 
34
34
  models = exec_queries_without_auto_include
35
- # Add all loaded models to the same AutoIncludeContext
36
- auto_include_context = Goldiloader::AutoIncludeContext.new
37
- auto_include_context.register_models(models)
35
+ Goldiloader::AutoIncludeContext.register_models(models, eager_load_values)
38
36
  models
39
37
  end
40
38
 
@@ -68,7 +66,7 @@ ActiveRecord::Associations::Association.class_eval do
68
66
  !association_info.finder_sql? &&
69
67
  # Joins not properly eager loaded - See https://github.com/salsify/goldiloader/issues/11
70
68
  !association_info.joins? &&
71
- # Unscope not properly eager loaded - https://github.com/salsify/goldiloader/issues/13
69
+ # Unscope not properly eager loaded - See https://github.com/salsify/goldiloader/issues/13
72
70
  !association_info.unscope?
73
71
  end
74
72
 
@@ -131,6 +129,16 @@ end
131
129
  end
132
130
  end
133
131
 
132
+ # uniq in Rails 3 not properly eager loaded - See https://github.com/salsify/goldiloader/issues/16
133
+ if ActiveRecord::VERSION::MAJOR < 4
134
+ ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do
135
+ def eager_loadable?
136
+ association_info = Goldiloader::AssociationInfo.new(self)
137
+ super && !association_info.uniq?
138
+ end
139
+ end
140
+ end
141
+
134
142
  # The CollectionProxy just forwards exists? to the underlying scope so we need to intercept this and
135
143
  # force it to use size which handles fully_load properly.
136
144
  ActiveRecord::Associations::CollectionProxy.class_eval do
@@ -43,6 +43,10 @@ module Goldiloader
43
43
  (@association.association_scope.joins_values.size - num_through_joins) > 0
44
44
  end
45
45
 
46
+ def uniq?
47
+ @association.association_scope.uniq_value
48
+ end
49
+
46
50
  private
47
51
 
48
52
  def num_through_joins
@@ -79,6 +83,10 @@ module Goldiloader
79
83
  # Rails 3 didn't support joins for associations
80
84
  false
81
85
  end
86
+
87
+ def uniq?
88
+ @association.options[:uniq]
89
+ end
82
90
  end
83
91
 
84
92
  end
@@ -8,6 +8,26 @@ module Goldiloader
8
8
  @models = []
9
9
  end
10
10
 
11
+ def self.register_models(models, included_associations = nil)
12
+ auto_include_context = Goldiloader::AutoIncludeContext.new
13
+ auto_include_context.register_models(models)
14
+
15
+ Array.wrap(included_associations).each do |included_association|
16
+ associations = included_association.is_a?(Hash) ?
17
+ included_association.keys : Array.wrap(included_association)
18
+ nested_associations = included_association.is_a?(Hash) ?
19
+ included_association : Hash.new([])
20
+
21
+ associations.each do |association|
22
+ nested_models = models.flat_map do |model|
23
+ model.association(association).target
24
+ end
25
+
26
+ register_models(nested_models, nested_associations[association])
27
+ end
28
+ end
29
+ end
30
+
11
31
  def register_models(models)
12
32
  Array.wrap(models).each do |model|
13
33
  model.auto_include_context = self
@@ -1,5 +1,5 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Goldiloader
4
- VERSION = '0.0.5'
4
+ VERSION = '0.0.6'
5
5
  end
@@ -111,6 +111,10 @@ class Post < ActiveRecord::Base
111
111
  has_many :unique_tags, through: :post_tags, source: :tag, uniq: true, class_name: 'Tag'
112
112
  end
113
113
 
114
+ if ActiveRecord::VERSION::MAJOR < 4
115
+ has_and_belongs_to_many :unique_tags_has_and_belongs, join_table: :post_tags, class_name: 'Tag', uniq: true
116
+ end
117
+
114
118
  after_destroy :after_post_destroy
115
119
 
116
120
  if Goldiloader::Compatibility.mass_assignment_security_enabled?
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ describe Goldiloader::AutoIncludeContext do
5
+ describe ".register_models" do
6
+ context "when included_associations is an array of symbols" do
7
+ let!(:roots) do
8
+ [
9
+ create_mock_model(cars: cars.take(2), fruit: fruits.first),
10
+ create_mock_model(cars: cars.drop(2).take(2), fruit: fruits.last)
11
+ ]
12
+ end
13
+ let!(:cars) { create_mock_models(4) }
14
+ let!(:fruits) { create_mock_models(2) }
15
+
16
+ before do
17
+ Goldiloader::AutoIncludeContext.register_models(roots, [:cars, :fruit])
18
+ end
19
+
20
+ it "sets the AutoIncludeContext for roots" do
21
+ expect(roots.map(&:auto_include_context).uniq.size).to eq 1
22
+ expect(roots.first.auto_include_context.models).to match_array(roots)
23
+ end
24
+
25
+ it "sets the AutoIncludeContext for singluar nested associations" do
26
+ expect(fruits.map(&:auto_include_context).uniq.size).to eq 1
27
+ expect(fruits.first.auto_include_context.models).to match_array(fruits)
28
+ end
29
+
30
+ it "sets the AutoIncludeContext for collection nested associations" do
31
+ expect(cars.map(&:auto_include_context).uniq.size).to eq 1
32
+ expect(cars.first.auto_include_context.models).to match_array(cars)
33
+ end
34
+ end
35
+
36
+ context "when included_associations is a hash" do
37
+ let!(:roots) do
38
+ [
39
+ create_mock_model(car: cars.first),
40
+ create_mock_model(car: cars.last)
41
+ ]
42
+ end
43
+
44
+ let!(:cars) do
45
+ [
46
+ create_mock_model(wheels: wheels.take(4)),
47
+ create_mock_model(wheels: wheels.drop(4).take(4))
48
+ ]
49
+ end
50
+
51
+ let!(:wheels) { create_mock_models(8) }
52
+
53
+ before do
54
+ Goldiloader::AutoIncludeContext.register_models(roots, car: :wheels)
55
+ end
56
+
57
+ it "sets the AutoIncludeContext for roots" do
58
+ expect(roots.map(&:auto_include_context).uniq.size).to eq 1
59
+ expect(roots.first.auto_include_context.models).to match_array(roots)
60
+ end
61
+
62
+ it "sets the AutoIncludeContext for child nested associations" do
63
+ expect(cars.map(&:auto_include_context).uniq.size).to eq 1
64
+ expect(cars.first.auto_include_context.models).to match_array(cars)
65
+ end
66
+
67
+ it "sets the AutoIncludeContext for grandchild nested associations" do
68
+ expect(wheels.map(&:auto_include_context).uniq.size).to eq 1
69
+ expect(wheels.first.auto_include_context.models).to match_array(wheels)
70
+ end
71
+ end
72
+
73
+ context "when included_associations is an array that mixes hashes and symbols" do
74
+ let!(:roots) do
75
+ [
76
+ create_mock_model(car: cars.first, person: people.first),
77
+ create_mock_model(car: cars.last, person: people.last)
78
+ ]
79
+ end
80
+
81
+ let!(:people) { create_mock_models(2) }
82
+
83
+ let!(:cars) do
84
+ [
85
+ create_mock_model(wheels: wheels.take(4)),
86
+ create_mock_model(wheels: wheels.drop(4).take(4))
87
+ ]
88
+ end
89
+
90
+ let!(:wheels) { create_mock_models(8) }
91
+
92
+ before do
93
+ Goldiloader::AutoIncludeContext.register_models(roots, [:person, car: :wheels])
94
+ end
95
+
96
+ it "sets the AutoIncludeContext for roots" do
97
+ expect(roots.map(&:auto_include_context).uniq.size).to eq 1
98
+ expect(roots.first.auto_include_context.models).to match_array(roots)
99
+ end
100
+
101
+ it "sets the AutoIncludeContext for child nested associations specified with a symbol" do
102
+ expect(people.map(&:auto_include_context).uniq.size).to eq 1
103
+ expect(people.first.auto_include_context.models).to match_array(people)
104
+ end
105
+
106
+ it "sets the AutoIncludeContext for child nested associations specified with a hash" do
107
+ expect(cars.map(&:auto_include_context).uniq.size).to eq 1
108
+ expect(cars.first.auto_include_context.models).to match_array(cars)
109
+ end
110
+
111
+ it "sets the AutoIncludeContext for grandchild nested associations" do
112
+ expect(wheels.map(&:auto_include_context).uniq.size).to eq 1
113
+ expect(wheels.first.auto_include_context.models).to match_array(wheels)
114
+ end
115
+ end
116
+
117
+ def create_mock_models(num)
118
+ num.times.map { create_mock_model }
119
+ end
120
+
121
+ def create_mock_model(associations = {})
122
+ model = AutoIncludeContextMockModel.new
123
+ associations.each do |association, models|
124
+ allow(model).to receive(:association).with(association) do
125
+ OpenStruct.new(target: models)
126
+ end
127
+ end
128
+ model
129
+ end
130
+
131
+ AutoIncludeContextMockModel = Struct.new(:auto_include_context)
132
+ end
133
+ end
@@ -244,6 +244,46 @@ describe Goldiloader do
244
244
  expect { post.save! }.to_not raise_error
245
245
  end
246
246
 
247
+ context "with manual eager loading" do
248
+ let(:blogs) { Blog.order(:name).send(load_method, :posts).to_a }
249
+
250
+ before do
251
+ blogs.first.posts.to_a.first.author
252
+ end
253
+
254
+ shared_examples "it auto-eager loads associations of manually eager loaded associations" do
255
+ specify do
256
+ blogs.flat_map(&:posts).drop(1).each do |blog|
257
+ expect(blog.association(:author)).to be_loaded
258
+ end
259
+
260
+ expect(blogs.first.posts.first.author).to eq author1
261
+ expect(blogs.first.posts.second.author).to eq author2
262
+ expect(blogs.second.posts.first.author).to eq author3
263
+ expect(blogs.second.posts.second.author).to eq author1
264
+ expect(Post).to have_received(:find_by_sql).at_most(:once)
265
+ end
266
+ end
267
+
268
+ context "via includes" do
269
+ let(:load_method) { :includes }
270
+
271
+ it_behaves_like "it auto-eager loads associations of manually eager loaded associations"
272
+ end
273
+
274
+ context "via eager_load" do
275
+ let(:load_method) { :eager_load }
276
+
277
+ it_behaves_like "it auto-eager loads associations of manually eager loaded associations"
278
+ end
279
+
280
+ context "via preload" do
281
+ let(:load_method) { :preload }
282
+
283
+ it_behaves_like "it auto-eager loads associations of manually eager loaded associations"
284
+ end
285
+ end
286
+
247
287
  context "with associations that can't be eager loaded" do
248
288
  let(:blogs) { Blog.order(:name).to_a }
249
289
 
@@ -372,6 +412,34 @@ describe Goldiloader do
372
412
  end
373
413
  end
374
414
  end
415
+
416
+ if ActiveRecord::VERSION::MAJOR < 4
417
+ context "unique_tags_has_and_belongs associations with a uniq" do
418
+ let!(:post1) do
419
+ Post.create! { |post| post.tags << child_tag1 << child_tag1 << child_tag3 }
420
+ end
421
+
422
+ let!(:post2) do
423
+ Post.create! { |post| post.tags << child_tag1 << child_tag1 << child_tag2 }
424
+ end
425
+
426
+ let(:posts) { Post.where(id: [post1.id, post2.id]).order(:id).to_a }
427
+
428
+ before do
429
+ posts.first.unique_tags_has_and_belongs.to_a
430
+ end
431
+
432
+ it "applies the uniq correctly" do
433
+ expect(posts.first.unique_tags_has_and_belongs.to_a.size).to eq 2
434
+ end
435
+
436
+ it "doesn't auto eager the association" do
437
+ posts.drop(1).each do |author|
438
+ expect(author.association(:unique_tags_has_and_belongs)).to_not be_loaded
439
+ end
440
+ end
441
+ end
442
+ end
375
443
  end
376
444
 
377
445
  context "associations with a uniq" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: goldiloader
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Turkel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-29 00:00:00.000000000 Z
11
+ date: 2014-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -160,6 +160,7 @@ files:
160
160
  - lib/goldiloader/version.rb
161
161
  - spec/db/database.yml
162
162
  - spec/db/schema.rb
163
+ - spec/goldiloader/auto_include_context_spec.rb
163
164
  - spec/goldiloader/goldiloader_spec.rb
164
165
  - spec/spec_helper.rb
165
166
  homepage: https://github.com/salsify/goldiloader
@@ -182,12 +183,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
182
183
  version: '0'
183
184
  requirements: []
184
185
  rubyforge_project:
185
- rubygems_version: 2.0.14
186
+ rubygems_version: 2.2.2
186
187
  signing_key:
187
188
  specification_version: 4
188
189
  summary: Automatic Rails association eager loading
189
190
  test_files:
190
191
  - spec/db/database.yml
191
192
  - spec/db/schema.rb
193
+ - spec/goldiloader/auto_include_context_spec.rb
192
194
  - spec/goldiloader/goldiloader_spec.rb
193
195
  - spec/spec_helper.rb