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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1cd22c993cb7fd5659c5371a85deb6c89ada5deb
4
- data.tar.gz: 9a3ecaff4ef43be8ee6f376613e896cb9875be59
3
+ metadata.gz: 856cc3b8807810dbf9e6b357bbc460d4c0bec3ac
4
+ data.tar.gz: b8cd9a6f87ffa7a9427dd089ad7fcab7580361da
5
5
  SHA512:
6
- metadata.gz: 1b8c078b155765e688e60c6471df92bff1a915812f9dada3ed8def41b65fc6783528adc4cb5890ae41f661baa3aec63189f68212ed52c7d66eaea33de991a2d7
7
- data.tar.gz: dabb87bd0a802bca323364851e78e210a43e1e4c9139a4b6faf9908912a57c86f43c33622cc1ae694132dba1bed6b4f9e67214d23d65ce0a0fcc5d08ea266dea
6
+ metadata.gz: 020f3bb39360cf4253f738592bb06fc222d83c71d6a6b343ca7e4df36741bf937f6e5bd62d7faee8c83638a6b020e2f950176184b757ac0b11a0a88687057c17
7
+ data.tar.gz: 4d3f56fd3c361608314c2c057cf0cbb9620513db6ecbc0595ac9e8f95a72a115c04339f678877c83142ed278d52f03a503a66ff651958deb39bfe038d1fe44cf
@@ -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 Goliloader in this [blog post](http://www.salsify.com/blog/automatic-eager-loading-rails/1869).
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
@@ -3,6 +3,7 @@
3
3
  require 'active_support/all'
4
4
  require 'active_record'
5
5
  require 'goldiloader/auto_include_context'
6
+ require 'goldiloader/association_info'
6
7
  require 'goldiloader/association_options'
7
8
  require 'goldiloader/association_loader'
8
9
  require 'goldiloader/model_registry'
@@ -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
- @association.fully_load? ? size > 0 : super
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 |model|
10
- load?(model, association_name)
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
- associated_models = models.map { |model| model.send(association_name) }.flatten.compact.uniq
20
- auto_include_context = Goldiloader::AutoIncludeContext.new(model_registry, association_path)
21
- auto_include_context.register_models(associated_models)
32
+ def mark_read_only(models)
33
+ models.each(&:readonly!)
22
34
  end
23
35
 
24
- private
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
- !model.destroyed? &&
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
@@ -1,5 +1,5 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Goldiloader
4
- VERSION = '0.0.3'
4
+ VERSION = '0.0.4'
5
5
  end
@@ -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
@@ -49,3 +49,5 @@ RSpec.configure do |config|
49
49
  DatabaseCleaner.clean
50
50
  end
51
51
  end
52
+
53
+ puts "Testing with ActiveRecord #{ActiveRecord::VERSION::STRING}"
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.3
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-17 00:00:00.000000000 Z
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