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 +4 -4
- data/.travis.yml +3 -3
- data/CHANGELOG.md +7 -0
- data/README.md +48 -2
- data/lib/goldiloader/active_record_patches.rb +12 -4
- data/lib/goldiloader/association_info.rb +8 -0
- data/lib/goldiloader/auto_include_context.rb +20 -0
- data/lib/goldiloader/version.rb +1 -1
- data/spec/db/schema.rb +4 -0
- data/spec/goldiloader/auto_include_context_spec.rb +133 -0
- data/spec/goldiloader/goldiloader_spec.rb +68 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5e5d374d8cba8f3ed0e484f0030a896af17c6315
|
4
|
+
data.tar.gz: 0749a259d93c68ff35c45207edfc7d87648b70c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 436cb0208f7cba5db8779af7ef9ad3d539ed988a2e48db1d8cbd3735885330afccba1844f8bbe8f540d5c37fc652cfe59c4edae7dc0c68cbc7282e153a209880
|
7
|
+
data.tar.gz: 41c5e5916200de2d44aa8228d18aa2ddc01a2e38dd29b4526212c51a3a27dae028602866e815baeb889a6277c4d1a9ac000c4f96b876b51592152060b27ae85a
|
data/.travis.yml
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
language: ruby
|
2
2
|
env:
|
3
3
|
matrix:
|
4
|
-
- RAILS_VERSION="~> 3.2.
|
5
|
-
- RAILS_VERSION="~> 4.0.
|
6
|
-
- RAILS_VERSION="~> 4.1.
|
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
|
data/CHANGELOG.md
CHANGED
@@ -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.
|
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
|
-
|
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
|
data/lib/goldiloader/version.rb
CHANGED
data/spec/db/schema.rb
CHANGED
@@ -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.
|
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-
|
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.
|
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
|