goldiloader 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +5 -1
- data/lib/goldiloader.rb +1 -0
- data/lib/goldiloader/active_record_patches.rb +15 -4
- data/lib/goldiloader/association_info.rb +51 -0
- data/lib/goldiloader/association_loader.rb +40 -8
- data/lib/goldiloader/version.rb +1 -1
- data/spec/db/schema.rb +33 -0
- data/spec/goldiloader/goldiloader_spec.rb +145 -0
- data/spec/spec_helper.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 856cc3b8807810dbf9e6b357bbc460d4c0bec3ac
|
4
|
+
data.tar.gz: b8cd9a6f87ffa7a9427dd089ad7fcab7580361da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 020f3bb39360cf4253f738592bb06fc222d83c71d6a6b343ca7e4df36741bf937f6e5bd62d7faee8c83638a6b020e2f950176184b757ac0b11a0a88687057c17
|
7
|
+
data.tar.gz: 4d3f56fd3c361608314c2c057cf0cbb9620513db6ecbc0595ac9e8f95a72a115c04339f678877c83142ed278d52f03a503a66ff651958deb39bfe038d1fe44cf
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
### 0.0.4
|
4
|
+
|
5
|
+
* Fix [issue 3](https://github.com/salsify/goldiloader/issues/3) - `exists?` method should take an argument
|
6
|
+
— thanks for reporting [Bert Goethals](https://github.com/Bertg)
|
7
|
+
* Fix [issue 4](https://github.com/salsify/goldiloader/issues/4) - Associations couldn't be loaded in after
|
8
|
+
destroy callbacks — thanks for reporting [Bert Goethals](https://github.com/Bertg)
|
9
|
+
* Fix [issue 6](https://github.com/salsify/goldiloader/issues/6) - Models in read only associations weren't
|
10
|
+
being marked as read only
|
11
|
+
* Fix [issue 7](https://github.com/salsify/goldiloader/issues/7) - Don't attempt to eager load associations that
|
12
|
+
aren't eager loadable e.g. if they have a limit
|
13
|
+
* Fix [issue 8](https://github.com/salsify/goldiloader/issues/8) - Handle eager loading associations whose
|
14
|
+
accessor methods have been overridden.
|
15
|
+
|
data/README.md
CHANGED
@@ -50,7 +50,7 @@ Here are the same queries with the Goldiloader:
|
|
50
50
|
|
51
51
|
Whoa! It automatically loaded all of the posts for our five blogs in a single database query without specifying any eager loads! Goldiloader assumes that you'll access all models loaded from a query in a uniform way. The first time you traverse an association on any of the models it will eager load the association for all the models. It even works with arbitrary nesting of associations.
|
52
52
|
|
53
|
-
Read more about the motivation for the
|
53
|
+
Read more about the motivation for the Goldiloader in this [blog post](http://www.salsify.com/blog/automatic-eager-loading-rails/).
|
54
54
|
|
55
55
|
## Installation
|
56
56
|
|
@@ -128,6 +128,10 @@ end
|
|
128
128
|
|
129
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.
|
130
130
|
|
131
|
+
## Change log
|
132
|
+
|
133
|
+
See the [change log](https://github.com/salsify/goldiloader/blob/master/CHANGELOG.md).
|
134
|
+
|
131
135
|
## Contributing
|
132
136
|
|
133
137
|
1. Fork it
|
data/lib/goldiloader.rb
CHANGED
@@ -45,7 +45,7 @@ ActiveRecord::Associations::Association.class_eval do
|
|
45
45
|
def auto_include?
|
46
46
|
# We only auto include associations that don't have in-memory changes since the
|
47
47
|
# Rails association Preloader clobbers any in-memory changes
|
48
|
-
!loaded? && target.blank? && options.fetch(:auto_include) { self.class.default_auto_include }
|
48
|
+
!loaded? && target.blank? && options.fetch(:auto_include) { self.class.default_auto_include } && eager_loadable?
|
49
49
|
end
|
50
50
|
|
51
51
|
def fully_load?
|
@@ -57,10 +57,18 @@ ActiveRecord::Associations::Association.class_eval do
|
|
57
57
|
owner.auto_include_context.association_path + [reflection.name])
|
58
58
|
end
|
59
59
|
|
60
|
+
def eager_loadable?
|
61
|
+
association_info = Goldiloader::AssociationInfo.new(self)
|
62
|
+
!association_info.limit? &&
|
63
|
+
!association_info.offset? &&
|
64
|
+
!association_info.group? &&
|
65
|
+
!association_info.from?
|
66
|
+
end
|
67
|
+
|
60
68
|
private
|
61
69
|
|
62
70
|
def load_with_auto_include(load_method, *args)
|
63
|
-
if loaded?
|
71
|
+
if loaded? && !stale_target?
|
64
72
|
target
|
65
73
|
elsif auto_include?
|
66
74
|
Goldiloader::AssociationLoader.load(auto_include_context.model_registry, owner,
|
@@ -122,7 +130,10 @@ end
|
|
122
130
|
# The CollectionProxy just forwards exists? to the underlying scope so we need to intercept this and
|
123
131
|
# force it to use size which handles fully_load properly.
|
124
132
|
ActiveRecord::Associations::CollectionProxy.class_eval do
|
125
|
-
def exists?
|
126
|
-
|
133
|
+
def exists?(*args)
|
134
|
+
# We don't fully_load the association when arguments are passed to exists? since Rails always
|
135
|
+
# pushes this query into the database without any caching (and it likely not a common
|
136
|
+
# scenario worth optimizing).
|
137
|
+
args.empty? && @association.fully_load? ? size > 0 : super
|
127
138
|
end
|
128
139
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Goldiloader
|
2
|
+
class AssociationInfo
|
3
|
+
|
4
|
+
def initialize(association)
|
5
|
+
@association = association
|
6
|
+
end
|
7
|
+
|
8
|
+
if ActiveRecord::VERSION::MAJOR >= 4
|
9
|
+
def read_only?
|
10
|
+
@association.association_scope.readonly_value.present?
|
11
|
+
end
|
12
|
+
|
13
|
+
def offset?
|
14
|
+
@association.association_scope.offset_value.present?
|
15
|
+
end
|
16
|
+
|
17
|
+
def limit?
|
18
|
+
@association.association_scope.limit_value.present?
|
19
|
+
end
|
20
|
+
|
21
|
+
def from?
|
22
|
+
@association.association_scope.from_value.present?
|
23
|
+
end
|
24
|
+
|
25
|
+
def group?
|
26
|
+
@association.association_scope.group_values.present?
|
27
|
+
end
|
28
|
+
else
|
29
|
+
def read_only?
|
30
|
+
@association.options[:readonly].present?
|
31
|
+
end
|
32
|
+
|
33
|
+
def offset?
|
34
|
+
@association.options[:offset].present?
|
35
|
+
end
|
36
|
+
|
37
|
+
def limit?
|
38
|
+
@association.options[:limit].present?
|
39
|
+
end
|
40
|
+
|
41
|
+
def from?
|
42
|
+
@association.options[:finder_sql].present?
|
43
|
+
end
|
44
|
+
|
45
|
+
def group?
|
46
|
+
@association.options[:group].present?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -6,30 +6,62 @@ module Goldiloader
|
|
6
6
|
|
7
7
|
def load(model_registry, model, association_path)
|
8
8
|
*model_path, association_name = *association_path
|
9
|
-
models = model_registry.peers(model, model_path).select do |
|
10
|
-
load?(
|
9
|
+
models = model_registry.peers(model, model_path).select do |peer|
|
10
|
+
load?(peer, association_name)
|
11
11
|
end
|
12
12
|
|
13
|
+
eager_load(models, association_name)
|
14
|
+
|
15
|
+
associated_models = associated_models(models, association_name)
|
16
|
+
# Workaround Rails #15853 by setting models read only
|
17
|
+
mark_read_only(associated_models) if read_only?(models, association_name)
|
18
|
+
auto_include_context = Goldiloader::AutoIncludeContext.new(model_registry, association_path)
|
19
|
+
auto_include_context.register_models(associated_models)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def eager_load(models, association_name)
|
13
25
|
if Gem::Version.new(::ActiveRecord::VERSION::STRING) >= Gem::Version.new('4.1')
|
14
26
|
::ActiveRecord::Associations::Preloader.new.preload(models, [association_name])
|
15
27
|
else
|
16
28
|
::ActiveRecord::Associations::Preloader.new(models, [association_name]).run
|
17
29
|
end
|
30
|
+
end
|
18
31
|
|
19
|
-
|
20
|
-
|
21
|
-
auto_include_context.register_models(associated_models)
|
32
|
+
def mark_read_only(models)
|
33
|
+
models.each(&:readonly!)
|
22
34
|
end
|
23
35
|
|
24
|
-
|
36
|
+
def read_only?(models, association_name)
|
37
|
+
model = first_model_with_association(models, association_name)
|
38
|
+
if model.nil?
|
39
|
+
false
|
40
|
+
else
|
41
|
+
association_info = AssociationInfo.new(model.association(association_name))
|
42
|
+
association_info.read_only?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def first_model_with_association(models, association_name)
|
47
|
+
models.find { |model| has_association?(model, association_name) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def associated_models(models, association_name)
|
51
|
+
# We can't just do model.send(association_name) because the association method may have been
|
52
|
+
# overridden
|
53
|
+
models.map { |model| model.association(association_name).target }.flatten.compact.uniq
|
54
|
+
end
|
25
55
|
|
26
56
|
def load?(model, association_name)
|
27
57
|
# Need to make sure the model actually has the association which won't always
|
28
58
|
# be the case in STI hierarchies e.g. only a subclass might have the association
|
29
|
-
|
30
|
-
model.class.reflect_on_association(association_name).present? &&
|
59
|
+
has_association?(model, association_name) &&
|
31
60
|
model.association(association_name).auto_include?
|
32
61
|
end
|
33
62
|
|
63
|
+
def has_association?(model, association_name)
|
64
|
+
model.class.reflect_on_association(association_name).present?
|
65
|
+
end
|
34
66
|
end
|
35
67
|
end
|
data/lib/goldiloader/version.rb
CHANGED
data/spec/db/schema.rb
CHANGED
@@ -60,11 +60,32 @@ class Blog < ActiveRecord::Base
|
|
60
60
|
has_many :posts
|
61
61
|
has_many :posts_without_auto_include, auto_include: false, class_name: 'Post'
|
62
62
|
has_many :posts_fully_load, fully_load: true, class_name: 'Post'
|
63
|
+
|
64
|
+
if ActiveRecord::VERSION::MAJOR >= 4
|
65
|
+
has_many :read_only_posts, -> { readonly }, class_name: 'Post'
|
66
|
+
has_many :limited_posts, -> { limit(2) }, class_name: 'Post'
|
67
|
+
has_many :grouped_posts, -> { group(:blog_id) }, class_name: 'Post'
|
68
|
+
has_many :offset_posts, -> { offset(2) }, class_name: 'Post'
|
69
|
+
has_many :from_posts, -> { from('(select distinct blog_id from posts) as posts') }, class_name: 'Post'
|
70
|
+
else
|
71
|
+
has_many :read_only_posts, readonly: true, class_name: 'Post'
|
72
|
+
has_many :limited_posts, limit: 2, class_name: 'Post'
|
73
|
+
has_many :grouped_posts, group: :blog_id, class_name: 'Post'
|
74
|
+
has_many :offset_posts, offset: 2, class_name: 'Post'
|
75
|
+
has_many :from_posts, finder_sql: Proc.new { "select distinct blog_id from posts where blog_id = #{self.id}" },
|
76
|
+
class_name: 'Post'
|
77
|
+
end
|
78
|
+
|
79
|
+
has_many :posts_overridden, class_name: 'Post'
|
63
80
|
has_many :authors, through: :posts
|
64
81
|
|
65
82
|
if Goldiloader::Compatibility.mass_assignment_security_enabled?
|
66
83
|
attr_accessible :name
|
67
84
|
end
|
85
|
+
|
86
|
+
def posts_overridden
|
87
|
+
'boom'
|
88
|
+
end
|
68
89
|
end
|
69
90
|
|
70
91
|
class Post < ActiveRecord::Base
|
@@ -74,9 +95,21 @@ class Post < ActiveRecord::Base
|
|
74
95
|
has_many :post_tags
|
75
96
|
has_many :tags, through: :post_tags
|
76
97
|
|
98
|
+
if ActiveRecord::VERSION::MAJOR >= 4
|
99
|
+
has_many :unique_tags, -> { distinct }, through: :post_tags, source: :tag, class_name: 'Tag'
|
100
|
+
else
|
101
|
+
has_many :unique_tags, through: :post_tags, source: :tag, uniq: true, class_name: 'Tag'
|
102
|
+
end
|
103
|
+
|
104
|
+
after_destroy :after_post_destroy
|
105
|
+
|
77
106
|
if Goldiloader::Compatibility.mass_assignment_security_enabled?
|
78
107
|
attr_accessible :title
|
79
108
|
end
|
109
|
+
|
110
|
+
def after_post_destroy
|
111
|
+
# Hook for tests
|
112
|
+
end
|
80
113
|
end
|
81
114
|
|
82
115
|
class User < ActiveRecord::Base
|
@@ -209,6 +209,142 @@ describe Goldiloader do
|
|
209
209
|
end
|
210
210
|
end
|
211
211
|
|
212
|
+
it "auto eager loads associations that have been overridden" do
|
213
|
+
blogs = Blog.order(:name).to_a
|
214
|
+
|
215
|
+
blogs.first.association(:posts_overridden).load_target
|
216
|
+
|
217
|
+
blogs.each do |blog|
|
218
|
+
expect(blog.association(:posts_overridden)).to be_loaded
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
it "marks auto eager loaded models as read only when the association is read only" do
|
223
|
+
blog = Blog.first!
|
224
|
+
post = blog.read_only_posts.to_a.first
|
225
|
+
expect { post.save! }.to raise_error(ActiveRecord::ReadOnlyRecord)
|
226
|
+
end
|
227
|
+
|
228
|
+
it "doesn't mark auto eager loaded models as read only when the association is not read only" do
|
229
|
+
blog = Blog.first!
|
230
|
+
post = blog.posts.to_a.first
|
231
|
+
expect { post.save! }.to_not raise_error
|
232
|
+
end
|
233
|
+
|
234
|
+
context "with associations that can't be eager loaded" do
|
235
|
+
let(:blogs) { Blog.order(:name).to_a }
|
236
|
+
|
237
|
+
before do
|
238
|
+
blog1.posts.create!(title: 'blog1-post3')
|
239
|
+
blog2.posts.create!(title: 'blog2-post3')
|
240
|
+
end
|
241
|
+
|
242
|
+
shared_examples "it doesn't auto eager the association" do |association_name|
|
243
|
+
specify do
|
244
|
+
blogs.drop(1).each do |blog|
|
245
|
+
expect(blog.association(association_name)).to_not be_loaded
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
context "associations with a limit" do
|
251
|
+
before do
|
252
|
+
blogs.first.limited_posts.to_a
|
253
|
+
end
|
254
|
+
|
255
|
+
it "applies the limit correctly" do
|
256
|
+
expect(blogs.first.limited_posts.to_a.size).to eq 2
|
257
|
+
end
|
258
|
+
|
259
|
+
it_behaves_like "it doesn't auto eager the association", :limited_posts
|
260
|
+
end
|
261
|
+
|
262
|
+
context "associations with a group" do
|
263
|
+
before do
|
264
|
+
blogs.first.grouped_posts.to_a
|
265
|
+
end
|
266
|
+
|
267
|
+
it "applies the group correctly" do
|
268
|
+
expect(blogs.first.grouped_posts.to_a.size).to eq 1
|
269
|
+
end
|
270
|
+
|
271
|
+
it_behaves_like "it doesn't auto eager the association", :grouped_posts
|
272
|
+
end
|
273
|
+
|
274
|
+
context "associations with an offset" do
|
275
|
+
before do
|
276
|
+
blogs.first.offset_posts.to_a
|
277
|
+
end
|
278
|
+
|
279
|
+
it "applies the offset correctly" do
|
280
|
+
expect(blogs.first.offset_posts.to_a.size).to eq 1
|
281
|
+
end
|
282
|
+
|
283
|
+
it_behaves_like "it doesn't auto eager the association", :offset_posts
|
284
|
+
end
|
285
|
+
|
286
|
+
context "associations with an overridden from" do
|
287
|
+
before do
|
288
|
+
blogs.first.from_posts.to_a
|
289
|
+
end
|
290
|
+
|
291
|
+
it "applies the from correctly" do
|
292
|
+
expect(blogs.first.from_posts.to_a.size).to eq 1
|
293
|
+
end
|
294
|
+
|
295
|
+
it_behaves_like "it doesn't auto eager the association", :from_posts
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
context "associations with a uniq" do
|
300
|
+
let!(:post1) do
|
301
|
+
Post.create! { |post| post.tags << child_tag1 << child_tag1 << child_tag3 }
|
302
|
+
end
|
303
|
+
|
304
|
+
let!(:post2) do
|
305
|
+
Post.create! { |post| post.tags << child_tag1 << child_tag1 << child_tag2 }
|
306
|
+
end
|
307
|
+
|
308
|
+
let(:posts) { Post.where(id: [post1.id, post2.id]).order(:id).to_a }
|
309
|
+
|
310
|
+
before do
|
311
|
+
posts.first.unique_tags.to_a
|
312
|
+
end
|
313
|
+
|
314
|
+
it "applies the uniq correctly" do
|
315
|
+
expect(posts.first.unique_tags.to_a).to match_array([child_tag1, child_tag3])
|
316
|
+
end
|
317
|
+
|
318
|
+
it "auto eager the association" do
|
319
|
+
posts.each do |blog|
|
320
|
+
expect(blog.association(:unique_tags)).to be_loaded
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
context "when a model is destroyed" do
|
326
|
+
let!(:posts) { Post.where(blog_id: blog1.id).to_a }
|
327
|
+
let(:destroyed_post) { posts.first }
|
328
|
+
let(:other_post) { posts.last }
|
329
|
+
|
330
|
+
before do
|
331
|
+
blog_after_destroy = nil
|
332
|
+
destroyed_post.define_singleton_method(:after_post_destroy) do
|
333
|
+
blog_after_destroy = self.blog
|
334
|
+
end
|
335
|
+
destroyed_post.destroy
|
336
|
+
@blog_after_destroy = blog_after_destroy
|
337
|
+
end
|
338
|
+
|
339
|
+
it "can load associations in after_destroy callbacks" do
|
340
|
+
expect(@blog_after_destroy).to eq blog1
|
341
|
+
end
|
342
|
+
|
343
|
+
it "auto eager loads the associaton on other models" do
|
344
|
+
expect(other_post.association(:blog)).to be_loaded
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
212
348
|
context "when a has_many association has in-memory changes" do
|
213
349
|
let!(:blogs) { Blog.order(:name).to_a }
|
214
350
|
let(:blog) { blogs.first }
|
@@ -317,6 +453,15 @@ describe Goldiloader do
|
|
317
453
|
end
|
318
454
|
end
|
319
455
|
|
456
|
+
it "doesn't auto eager load a has_many association when exists? is called with arguments" do
|
457
|
+
blogs = Blog.order(:name).to_a
|
458
|
+
blogs.first.posts_fully_load.exists?(false)
|
459
|
+
|
460
|
+
blogs.each do |blog|
|
461
|
+
expect(blog.association(:posts_fully_load)).to_not be_loaded
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
320
465
|
it "auto eager loads a has_many association when last is called" do
|
321
466
|
blogs = Blog.order(:name).to_a
|
322
467
|
blogs.first.posts_fully_load.last
|
data/spec/spec_helper.rb
CHANGED
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.4
|
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-
|
11
|
+
date: 2014-06-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -144,6 +144,7 @@ files:
|
|
144
144
|
- .gitignore
|
145
145
|
- .rspec
|
146
146
|
- .travis.yml
|
147
|
+
- CHANGELOG.md
|
147
148
|
- Gemfile
|
148
149
|
- LICENSE.txt
|
149
150
|
- README.md
|
@@ -151,6 +152,7 @@ files:
|
|
151
152
|
- goldiloader.gemspec
|
152
153
|
- lib/goldiloader.rb
|
153
154
|
- lib/goldiloader/active_record_patches.rb
|
155
|
+
- lib/goldiloader/association_info.rb
|
154
156
|
- lib/goldiloader/association_loader.rb
|
155
157
|
- lib/goldiloader/association_options.rb
|
156
158
|
- lib/goldiloader/auto_include_context.rb
|