goldiloader 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1cd22c993cb7fd5659c5371a85deb6c89ada5deb
4
+ data.tar.gz: 9a3ecaff4ef43be8ee6f376613e896cb9875be59
5
+ SHA512:
6
+ metadata.gz: 1b8c078b155765e688e60c6471df92bff1a915812f9dada3ed8def41b65fc6783528adc4cb5890ae41f661baa3aec63189f68212ed52c7d66eaea33de991a2d7
7
+ data.tar.gz: dabb87bd0a802bca323364851e78e210a43e1e4c9139a4b6faf9908912a57c86f43c33622cc1ae694132dba1bed6b4f9e67214d23d65ce0a0fcc5d08ea266dea
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.iml
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .idea/
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ log/
15
+ pkg
16
+ rdoc
17
+ spec/reports
18
+ test/tmp
19
+ test/version_tmp
20
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ env:
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"
7
+ rvm:
8
+ - 1.9.3
9
+ - 2.0.0
10
+ - 2.1.0
11
+ - jruby-19mode
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Joel Turkel
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # Goldiloader
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/goldiloader.png)][gem]
4
+ [![Build Status](https://secure.travis-ci.org/salsify/goldiloader.png?branch=master)][travis]
5
+ [![Code Climate](https://codeclimate.com/github/salsify/goldiloader.png)][codeclimate]
6
+ [![Coverage Status](https://coveralls.io/repos/salsify/goldiloader/badge.png)][coveralls]
7
+
8
+ [gem]: https://rubygems.org/gems/goldiloader
9
+ [travis]: http://travis-ci.org/salsify/goldiloader
10
+ [codeclimate]: https://codeclimate.com/github/salsify/goldiloader
11
+ [coveralls]: https://coveralls.io/r/salsify/goldiloader
12
+
13
+ Wouldn't it be awesome if ActiveRecord didn't make you think about eager loading and it just did the "right" thing by default? With Goldiloader it can!
14
+
15
+ Consider the following models:
16
+
17
+ ```
18
+ class Blog < ActiveRecord::Base
19
+ has_many :posts
20
+ end
21
+
22
+ class Post < ActiveRecord::Base
23
+ belongs_to :blog
24
+ end
25
+ ```
26
+
27
+ Here are some sample queries without the Goldiloader:
28
+
29
+ ```
30
+ > blogs = Blogs.limit(5).to_a
31
+ # SELECT * FROM blogs LIMIT 5
32
+
33
+ > blogs.each { |blog| blog.posts.to_a }
34
+ # SELECT * FROM posts WHERE blog_id = 1
35
+ # SELECT * FROM posts WHERE blog_id = 2
36
+ # SELECT * FROM posts WHERE blog_id = 3
37
+ # SELECT * FROM posts WHERE blog_id = 4
38
+ # SELECT * FROM posts WHERE blog_id = 5
39
+ ```
40
+
41
+ Here are the same queries with the Goldiloader:
42
+
43
+ ```
44
+ > blogs = Blogs.limit(5).to_a
45
+ # SELECT * FROM blogs LIMIT 5
46
+
47
+ > blogs.each { |blog| blog.posts.to_a }
48
+ # SELECT * FROM posts WHERE blog_id IN (1,2,3,4,5)
49
+ ```
50
+
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
+
53
+ Read more about the motivation for the Goliloader in this [blog post](http://www.salsify.com/blog/automatic-eager-loading-rails/1869).
54
+
55
+ ## Installation
56
+
57
+ Add this line to your application's Gemfile:
58
+
59
+ gem 'goldiloader'
60
+
61
+ And then execute:
62
+
63
+ $ bundle
64
+
65
+ Or install it yourself as:
66
+
67
+ $ gem install goldiloader
68
+
69
+ ## Usage
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.
72
+
73
+ ### Association Options
74
+
75
+ Goldiloader supports a few options on ActiveRecord associations to customize its behavior.
76
+
77
+ #### auto_include
78
+
79
+ You can disable automatic eager loading on specific associations with the `auto_include` option:
80
+
81
+ ```
82
+ class Blog < ActiveRecord::Base
83
+ has_many :posts, auto_include: false
84
+ end
85
+ ```
86
+
87
+ #### fully_load
88
+
89
+ There are several association methods that ActiveRecord can either execute on in memory models or push down into SQL depending on whether or not the association is loaded. This includes the following methods:
90
+
91
+ * `first`
92
+ * `second`
93
+ * `third`
94
+ * `fourth`
95
+ * `fifth`
96
+ * `forty_two` (one of the hidden gems in Rails 4.1)
97
+ * `last`
98
+ * `size`
99
+ * `ids_reader`
100
+ * `empty?`
101
+ * `exists?`
102
+
103
+ This can cause problems for certain usage patterns if we're no longer specifying eager loads:
104
+
105
+ ```
106
+ > blogs = Blogs.limit(5).to_a
107
+ # SELECT * FROM blogs LIMIT 5
108
+
109
+ > blogs.each do |blog|
110
+ if blog.posts.exists?
111
+ puts blog.posts
112
+ else
113
+ puts 'No posts'
114
+ end
115
+ # SELECT 1 AS one FROM posts WHERE blog_id = 1 LIMIT 1
116
+ # SELECT * FROM posts WHERE blog_id IN (1,2,3,4,5)
117
+ ```
118
+
119
+ Notice the first call to `blog.posts.exists?` was executed via SQL because the `posts` association wasn't yet loaded. The `fully_load` option can be used to force ActiveRecord to fully load the association (and do any necessary automatic eager loading) when evaluating methods like `exists?`:
120
+
121
+ ```
122
+ class Blog < ActiveRecord::Base
123
+ has_many :posts, fully_load: true
124
+ end
125
+ ```
126
+
127
+ ## Status
128
+
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
+
131
+ ## Contributing
132
+
133
+ 1. Fork it
134
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
135
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
136
+ 4. Push to the branch (`git push origin my-new-feature`)
137
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/setup'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec) do |task|
6
+ task.verbose = false
7
+ end
8
+
9
+ task default: :spec
@@ -0,0 +1,37 @@
1
+ # encoding: UTF-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'goldiloader/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'goldiloader'
8
+ spec.version = Goldiloader::VERSION
9
+ spec.authors = ['Joel Turkel']
10
+ spec.email = ['jturkel@salsify.com']
11
+ spec.description = %q{Automatically eager loads Rails associations as associations are traversed}
12
+ spec.summary = %q{Automatic Rails association eager loading}
13
+ spec.homepage = 'https://github.com/salsify/goldiloader'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.test_files = Dir.glob('spec/**/*')
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_dependency 'activerecord', ENV.fetch('RAILS_VERSION', ['>= 3.2', '<= 4.1'])
21
+ spec.add_dependency 'activesupport', ENV.fetch('RAILS_VERSION', ['>= 3.2', '<= 4.1'])
22
+
23
+ spec.add_development_dependency 'coveralls'
24
+ spec.add_development_dependency 'database_cleaner', '>= 1.2'
25
+ spec.add_development_dependency 'rake'
26
+ spec.add_development_dependency 'rspec', '~> 2'
27
+ spec.add_development_dependency 'simplecov', '~> 0.7.1'
28
+
29
+ if RUBY_PLATFORM == 'java'
30
+ spec.add_development_dependency 'jdbc-sqlite3'
31
+ spec.add_development_dependency 'activerecord-jdbcsqlite3-adapter'
32
+ else
33
+ spec.add_development_dependency 'sqlite3'
34
+ end
35
+
36
+ end
37
+
@@ -0,0 +1,9 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'active_support/all'
4
+ require 'active_record'
5
+ require 'goldiloader/auto_include_context'
6
+ require 'goldiloader/association_options'
7
+ require 'goldiloader/association_loader'
8
+ require 'goldiloader/model_registry'
9
+ require 'goldiloader/active_record_patches'
@@ -0,0 +1,128 @@
1
+ # encoding: UTF-8
2
+
3
+ module Goldiloader
4
+ module AutoIncludableModel
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ attr_writer :auto_include_context
9
+ end
10
+
11
+ def initialize_copy(other)
12
+ super
13
+ @auto_include_context = nil
14
+ end
15
+
16
+ def auto_include_context
17
+ @auto_include_context ||= Goldiloader::AutoIncludeContext.create_empty.register_model(self)
18
+ end
19
+ end
20
+ end
21
+
22
+ ActiveRecord::Base.send(:include, Goldiloader::AutoIncludableModel)
23
+
24
+ ActiveRecord::Relation.class_eval do
25
+
26
+ def exec_queries_with_auto_include
27
+ return exec_queries_without_auto_include if loaded?
28
+
29
+ models = exec_queries_without_auto_include
30
+ # Add all loaded models to the same AutoIncludeContext
31
+ auto_include_context = Goldiloader::AutoIncludeContext.create_empty
32
+ auto_include_context.register_models(models)
33
+ models
34
+ end
35
+
36
+ alias_method_chain :exec_queries, :auto_include
37
+ end
38
+
39
+ ActiveRecord::Associations::Association.class_eval do
40
+
41
+ class_attribute :default_auto_include, :default_fully_load
42
+ self.default_auto_include = true
43
+ self.default_fully_load = false
44
+
45
+ def auto_include?
46
+ # We only auto include associations that don't have in-memory changes since the
47
+ # Rails association Preloader clobbers any in-memory changes
48
+ !loaded? && target.blank? && options.fetch(:auto_include) { self.class.default_auto_include }
49
+ end
50
+
51
+ def fully_load?
52
+ !loaded? && options.fetch(:fully_load) { self.class.default_fully_load }
53
+ end
54
+
55
+ def auto_include_context
56
+ @auto_include_context ||= Goldiloader::AutoIncludeContext.new(owner.auto_include_context.model_registry,
57
+ owner.auto_include_context.association_path + [reflection.name])
58
+ end
59
+
60
+ private
61
+
62
+ def load_with_auto_include(load_method, *args)
63
+ if loaded?
64
+ target
65
+ elsif auto_include?
66
+ Goldiloader::AssociationLoader.load(auto_include_context.model_registry, owner,
67
+ auto_include_context.association_path)
68
+ target
69
+ else
70
+ send("#{load_method}_without_auto_include", *args)
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ ActiveRecord::Associations::SingularAssociation.class_eval do
77
+
78
+ private
79
+
80
+ def find_target_with_auto_include(*args)
81
+ load_with_auto_include(:find_target, *args)
82
+ end
83
+
84
+ alias_method_chain :find_target, :auto_include
85
+ end
86
+
87
+ ActiveRecord::Associations::CollectionAssociation.class_eval do
88
+ # Force these methods to load the entire association for fully_load associations
89
+ [:first, :second, :third, :fourth, :fifth, :last, :size, :ids_reader, :empty?].each do |method|
90
+ # Some of these methods were added in Rails 4
91
+ next unless method_defined?(method)
92
+
93
+ aliased_target, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
94
+ define_method("#{aliased_target}_with_fully_load#{punctuation}") do |*args, &block|
95
+ load_target if fully_load?
96
+ send("#{aliased_target}_without_fully_load#{punctuation}", *args, &block)
97
+ end
98
+
99
+ alias_method_chain method, :fully_load
100
+ end
101
+
102
+ private
103
+
104
+ def load_target_with_auto_include(*args)
105
+ load_with_auto_include(:load_target, *args)
106
+ end
107
+
108
+ alias_method_chain :load_target, :auto_include
109
+
110
+ end
111
+
112
+ [ActiveRecord::Associations::HasManyThroughAssociation, ActiveRecord::Associations::HasOneThroughAssociation].each do |klass|
113
+ klass.class_eval do
114
+ def auto_include?
115
+ # Only auto include through associations if the target association is auto-loadable
116
+ through_association = owner.association(through_reflection.name)
117
+ through_association.auto_include? && super
118
+ end
119
+ end
120
+ end
121
+
122
+ # The CollectionProxy just forwards exists? to the underlying scope so we need to intercept this and
123
+ # force it to use size which handles fully_load properly.
124
+ ActiveRecord::Associations::CollectionProxy.class_eval do
125
+ def exists?
126
+ @association.fully_load? ? size > 0 : super
127
+ end
128
+ end
@@ -0,0 +1,35 @@
1
+ # encoding: UTF-8
2
+
3
+ module Goldiloader
4
+ module AssociationLoader
5
+ extend self
6
+
7
+ def load(model_registry, model, association_path)
8
+ *model_path, association_name = *association_path
9
+ models = model_registry.peers(model, model_path).select do |model|
10
+ load?(model, association_name)
11
+ end
12
+
13
+ if Gem::Version.new(::ActiveRecord::VERSION::STRING) >= Gem::Version.new('4.1')
14
+ ::ActiveRecord::Associations::Preloader.new.preload(models, [association_name])
15
+ else
16
+ ::ActiveRecord::Associations::Preloader.new(models, [association_name]).run
17
+ end
18
+
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)
22
+ end
23
+
24
+ private
25
+
26
+ def load?(model, association_name)
27
+ # Need to make sure the model actually has the association which won't always
28
+ # 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? &&
31
+ model.association(association_name).auto_include?
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: UTF-8
2
+
3
+ module Goldiloader
4
+ module AssociationOptions
5
+ extend self
6
+
7
+ OPTIONS = [:auto_include, :fully_load]
8
+
9
+ def register
10
+ if ::ActiveRecord::VERSION::MAJOR >= 4
11
+ ActiveRecord::Associations::Builder::Association.valid_options.concat(OPTIONS)
12
+ else
13
+ # Each subclass of CollectionAssociation will have its own copy of valid_options so we need
14
+ # to register the valid option for each one.
15
+ collection_association_classes.each do |assoc_class|
16
+ assoc_class.valid_options.concat(OPTIONS)
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def collection_association_classes
24
+ # Association.descendants doesn't work well with lazy classloading :(
25
+ [
26
+ ActiveRecord::Associations::Builder::Association,
27
+ ActiveRecord::Associations::Builder::BelongsTo,
28
+ ActiveRecord::Associations::Builder::HasAndBelongsToMany,
29
+ ActiveRecord::Associations::Builder::HasMany,
30
+ ActiveRecord::Associations::Builder::HasOne,
31
+ ActiveRecord::Associations::Builder::SingularAssociation
32
+ ]
33
+ end
34
+ end
35
+ end
36
+
37
+ Goldiloader::AssociationOptions.register
@@ -0,0 +1,19 @@
1
+ # encoding: UTF-8
2
+
3
+ module Goldiloader
4
+ class AutoIncludeContext < Struct.new(:model_registry, :association_path)
5
+ def self.create_empty
6
+ Goldiloader::AutoIncludeContext.new(Goldiloader::ModelRegistry.new, [])
7
+ end
8
+
9
+ def register_models(models)
10
+ Array.wrap(models).each do |model|
11
+ model.auto_include_context = self
12
+ model_registry.register(model, association_path)
13
+ end
14
+ self
15
+ end
16
+
17
+ alias_method :register_model, :register_models
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: UTF-8
2
+
3
+ module Goldiloader
4
+ class ModelRegistry
5
+ def initialize
6
+ @registry = {}
7
+ end
8
+
9
+ def register(record, association_path)
10
+ key = registry_key(record, association_path)
11
+ @registry[key] ||= []
12
+ @registry[key] << record
13
+ end
14
+
15
+ # Returns all models with the same base class loaded from the same association path
16
+ def peers(record, association_path)
17
+ @registry.fetch(registry_key(record, association_path), [])
18
+ end
19
+
20
+ private
21
+
22
+ def registry_key(record, association_path)
23
+ [record.class.base_class, association_path]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+
3
+ module Goldiloader
4
+ VERSION = '0.0.3'
5
+ end
@@ -0,0 +1,3 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: ":memory:"
data/spec/db/schema.rb ADDED
@@ -0,0 +1,107 @@
1
+ # encoding: UTF-8
2
+
3
+ ActiveRecord::Schema.define(:version => 0) do
4
+
5
+ create_table(:blogs, force: true) do |t|
6
+ t.string :name
7
+ end
8
+
9
+ create_table(:posts, force: true) do |t|
10
+ t.string :title
11
+ t.integer :blog_id
12
+ t.integer :author_id
13
+ end
14
+
15
+ create_table(:users, force: true) do |t|
16
+ t.string :name
17
+ end
18
+
19
+ create_table(:addresses, force: true) do |t|
20
+ t.string :city
21
+ t.integer :user_id
22
+ end
23
+
24
+ create_table(:groups, force: true) do |t|
25
+ t.string :name
26
+ end
27
+
28
+ create_table(:tags, force: true) do |t|
29
+ t.string :name
30
+ t.integer :parent_id
31
+ t.string :owner_type
32
+ t.integer :owner_id
33
+ end
34
+
35
+ create_table(:post_tags, force: true) do |t|
36
+ t.integer :post_id
37
+ t.integer :tag_id
38
+ end
39
+ end
40
+
41
+ class Tag < ActiveRecord::Base
42
+ belongs_to :parent, class_name: 'Tag'
43
+ has_many :children, class_name: 'Tag', foreign_key: :parent_id
44
+
45
+ belongs_to :owner, polymorphic: true
46
+ has_many :post_tags
47
+ has_many :posts, through: :post_tags
48
+
49
+ if Goldiloader::Compatibility.mass_assignment_security_enabled?
50
+ attr_accessible :name
51
+ end
52
+ end
53
+
54
+ class PostTag < ActiveRecord::Base
55
+ belongs_to :post
56
+ belongs_to :tag
57
+ end
58
+
59
+ class Blog < ActiveRecord::Base
60
+ has_many :posts
61
+ has_many :posts_without_auto_include, auto_include: false, class_name: 'Post'
62
+ has_many :posts_fully_load, fully_load: true, class_name: 'Post'
63
+ has_many :authors, through: :posts
64
+
65
+ if Goldiloader::Compatibility.mass_assignment_security_enabled?
66
+ attr_accessible :name
67
+ end
68
+ end
69
+
70
+ class Post < ActiveRecord::Base
71
+ belongs_to :blog
72
+ belongs_to :blog_without_auto_include, auto_include: false, class_name: 'Blog', foreign_key: :blog_id
73
+ belongs_to :author, class_name: 'User'
74
+ has_many :post_tags
75
+ has_many :tags, through: :post_tags
76
+
77
+ if Goldiloader::Compatibility.mass_assignment_security_enabled?
78
+ attr_accessible :title
79
+ end
80
+ end
81
+
82
+ class User < ActiveRecord::Base
83
+ has_many :posts, foreign_key: :author_id
84
+ has_many :tags, as: :owner
85
+ has_one :address
86
+ has_one :address_without_auto_include, auto_include: false, class_name: 'Address'
87
+
88
+ if Goldiloader::Compatibility.mass_assignment_security_enabled?
89
+ attr_accessible :name
90
+ end
91
+ end
92
+
93
+ class Address < ActiveRecord::Base
94
+ belongs_to :user
95
+
96
+ if Goldiloader::Compatibility.mass_assignment_security_enabled?
97
+ attr_accessible :city
98
+ end
99
+ end
100
+
101
+ class Group < ActiveRecord::Base
102
+ has_many :tags, as: :owner
103
+
104
+ if Goldiloader::Compatibility.mass_assignment_security_enabled?
105
+ attr_accessible :name
106
+ end
107
+ end
@@ -0,0 +1,399 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Goldiloader do
6
+ let!(:author1) do
7
+ User.create!(name: 'author1') { |u| u.address = Address.new(city: 'author1-city') }
8
+ end
9
+
10
+ let!(:author2) do
11
+ User.create!(name: 'author2') { |u| u.address = Address.new(city: 'author2-city') }
12
+ end
13
+
14
+ let!(:author3) do
15
+ User.create!(name: 'author3') { |u| u.address = Address.new(city: 'author3-city') }
16
+ end
17
+
18
+ let!(:group1) { Group.create!(name: 'group1') }
19
+
20
+ let!(:parent_tag1) { Tag.create!(name: 'parent1') { |t| t.owner = group1 } }
21
+ let!(:child_tag1) { parent_tag1.children.create!(name: 'parent1-child1') { |t| t.owner = author1 } }
22
+ let!(:child_tag2) { parent_tag1.children.create!(name: 'parent1-child2') { |t| t.owner = group1 } }
23
+ let!(:parent_tag2) { Tag.create!(name: 'parent2') { |t| t.owner = group1 } }
24
+ let!(:child_tag3) { parent_tag2.children.create!(name: 'parent2-child1') { |t| t.owner = author2 } }
25
+
26
+ let!(:blog1) do
27
+ blog1 = Blog.create!(name: 'blog1')
28
+
29
+ blog1.posts.create!(title: 'blog1-post1') do |post|
30
+ post.author = author1
31
+ post.tags << child_tag1 << child_tag2
32
+ end
33
+
34
+ blog1.posts.create!(title: 'blog1-post2') do |post|
35
+ post.author = author2
36
+ post.tags << child_tag1
37
+ end
38
+
39
+ blog1
40
+ end
41
+
42
+ let!(:blog2) do
43
+ blog2 = Blog.create!(name: 'blog2')
44
+
45
+ blog2.posts.create!(title: 'blog2-post1') do |post|
46
+ post.author = author3
47
+ post.tags << child_tag1
48
+ end
49
+
50
+ blog2.posts.create!(title: 'blog2-post2') do |post|
51
+ post.author = author1
52
+ post.tags << child_tag3
53
+ end
54
+
55
+ blog2
56
+ end
57
+
58
+ before do
59
+ [Address, Blog, Post, Tag, User, Group].each do |klass|
60
+ allow(klass).to receive(:find_by_sql).and_call_original
61
+ end
62
+
63
+ ActiveRecord::Base.logger.info('Test setup complete')
64
+ end
65
+
66
+ it "auto eager loads has_many associations" do
67
+ blogs = Blog.order(:name).to_a
68
+
69
+ # Sanity check that associations aren't loaded yet
70
+ blogs.each do |blog|
71
+ expect(blog.association(:posts)).to_not be_loaded
72
+ end
73
+
74
+ # Force the first blogs first post to load
75
+ blogs.first.posts.to_a
76
+
77
+ blogs.each do |blog|
78
+ expect(blog.association(:posts)).to be_loaded
79
+ end
80
+
81
+ expect(blogs.first.posts.map(&:title)).to match_array(['blog1-post1', 'blog1-post2'])
82
+ expect(blogs.second.posts.map(&:title)).to match_array(['blog2-post1', 'blog2-post2'])
83
+
84
+ expect(Post).to have_received(:find_by_sql).once
85
+ end
86
+
87
+ it "auto eager loads belongs_to associations" do
88
+ posts = Post.order(:title).to_a
89
+
90
+ # Sanity check that associations aren't loaded yet
91
+ posts.each do |blog|
92
+ expect(blog.association(:blog)).to_not be_loaded
93
+ end
94
+
95
+ # Force the first post's blog to load
96
+ posts.first.blog
97
+
98
+ posts.each do |blog|
99
+ expect(blog.association(:blog)).to be_loaded
100
+ end
101
+
102
+ expect(posts.map(&:blog).map(&:name)).to eq(['blog1', 'blog1', 'blog2', 'blog2'])
103
+ expect(Blog).to have_received(:find_by_sql).once
104
+ end
105
+
106
+ it "auto eager loads has_one associations" do
107
+ users = User.order(:name).to_a
108
+
109
+ # Sanity check that associations aren't loaded yet
110
+ users.each do |user|
111
+ expect(user.association(:address)).to_not be_loaded
112
+ end
113
+
114
+ # Force the first user's address to load
115
+ users.first.address
116
+
117
+ users.each do |blog|
118
+ expect(blog.association(:address)).to be_loaded
119
+ end
120
+
121
+ expect(users.map(&:address).map(&:city)).to match_array(['author1-city', 'author2-city', 'author3-city'])
122
+ expect(Address).to have_received(:find_by_sql).once
123
+ end
124
+
125
+ it "auto eager loads nested associations" do
126
+ blogs = Blog.order(:name).to_a
127
+ blogs.first.posts.to_a.first.author
128
+
129
+ blogs.flat_map(&:posts).each do |blog|
130
+ expect(blog.association(:author)).to be_loaded
131
+ end
132
+
133
+ expect(blogs.first.posts.first.author).to eq author1
134
+ expect(blogs.first.posts.second.author).to eq author2
135
+ expect(blogs.second.posts.first.author).to eq author3
136
+ expect(blogs.second.posts.second.author).to eq author1
137
+ expect(Post).to have_received(:find_by_sql).once
138
+ end
139
+
140
+ it "auto eager loads has_many through associations" do
141
+ blogs = Blog.order(:name).to_a
142
+ blogs.first.authors.to_a
143
+
144
+ blogs.each do |blog|
145
+ expect(blog.association(:authors)).to be_loaded
146
+ end
147
+
148
+ expect(blogs.first.authors).to match_array([author1, author2])
149
+ expect(blogs.second.authors).to match_array([author3, author1])
150
+ expect(User).to have_received(:find_by_sql).once
151
+ end
152
+
153
+ it "auto eager loads associations when the model is loaded via find" do
154
+ blog = Blog.find(blog1.id)
155
+ blog.posts.to_a.first.author
156
+
157
+ blog.posts.each do |blog|
158
+ expect(blog.association(:author)).to be_loaded
159
+ end
160
+ end
161
+
162
+ it "auto eager loads polymorphic associations" do
163
+ tags = Tag.where('parent_id IS NOT NULL').order(:name).to_a
164
+ tags.first.owner
165
+
166
+ tags.each do |tag|
167
+ expect(tag.association(:owner)).to be_loaded
168
+ end
169
+
170
+ expect(tags.first.owner).to eq author1
171
+ expect(tags.second.owner).to eq group1
172
+ expect(tags.third.owner).to eq author2
173
+ end
174
+
175
+ it "auto eager loads associations of polymorphic associations" do
176
+ tags = Tag.where('parent_id IS NOT NULL').order(:name).to_a
177
+ users = tags.map(&:owner).select {|owner| owner.is_a?(User) }.sort_by(&:name)
178
+ users.first.posts.to_a
179
+
180
+ users.each do |user|
181
+ expect(user.association(:posts)).to be_loaded
182
+ end
183
+
184
+ expect(users.first.posts).to eq Post.where(author_id: author1.id)
185
+ expect(users.second.posts).to eq Post.where(author_id: author2.id)
186
+ end
187
+
188
+ it "only auto eager loads associations loaded through the same path" do
189
+ root_tags = Tag.where(parent_id: nil).order(:name).to_a
190
+ root_tags.first.children.to_a
191
+
192
+ # Make sure we loaded all child tags
193
+ root_tags.each do |tag|
194
+ expect(tag.association(:children)).to be_loaded
195
+ end
196
+
197
+ # Force a load of a root tag's owner
198
+ root_tags.first.owner
199
+
200
+ # All root tag owners should be loaded
201
+ root_tags.each do |tag|
202
+ expect(tag.association(:owner)).to be_loaded
203
+ end
204
+
205
+ # Child tag owners should not be loaded
206
+ child_tags = root_tags.flat_map(&:children)
207
+ child_tags.each do |tag|
208
+ expect(tag.association(:owner)).to_not be_loaded
209
+ end
210
+ end
211
+
212
+ context "when a has_many association has in-memory changes" do
213
+ let!(:blogs) { Blog.order(:name).to_a }
214
+ let(:blog) { blogs.first }
215
+ let(:other_blog) { blogs.last }
216
+
217
+ before do
218
+ blog.posts.create(title: 'blog1-new-post')
219
+ end
220
+
221
+ it "returns the correct models for the modified has_many association" do
222
+ expect(blog.posts).to match_array Post.where(blog_id: blog.id)
223
+ end
224
+
225
+ it "doesn't auto eager load peers when accessing the modified has_many association" do
226
+ blog.posts.to_a
227
+ expect(other_blog.association(:posts)).to_not be_loaded
228
+ end
229
+
230
+ it "returns the correct models for the modified has_many association when accessing a peer" do
231
+ other_blog.posts.to_a
232
+ expect(blog.posts).to match_array Post.where(blog_id: blog.id)
233
+ end
234
+ end
235
+
236
+ context "when a has_many through association has in-memory changes" do
237
+ let!(:posts) { Post.order(:title).to_a }
238
+ let(:post) { posts.first }
239
+ let(:other_post) { posts.last }
240
+
241
+ before do
242
+ tag = Tag.create(name: 'new-tag')
243
+ post.post_tags.create(tag: tag)
244
+ end
245
+
246
+ it "returns the correct models for the modified has_many through association" do
247
+ expect(post.tags).to match_array PostTag.where(post_id: post.id).includes(:tag).map(&:tag)
248
+ end
249
+
250
+ it "doesn't auto eager load peers when accessing the modified has_many through association" do
251
+ post.tags.to_a
252
+ expect(other_post.association(:tags)).to_not be_loaded
253
+ end
254
+
255
+ it "returns the correct models for the modified has_many through association when accessing a peer" do
256
+ other_post.tags.to_a
257
+ expect(post.tags).to match_array PostTag.where(post_id: post.id).includes(:tag).map(&:tag)
258
+ end
259
+ end
260
+
261
+ context "with fully_load false" do
262
+
263
+ it "doesn't auto eager loads a has_many association when size is called" do
264
+ blogs = Blog.order(:name).to_a
265
+ blogs.first.posts.size
266
+
267
+ blogs.each do |blog|
268
+ expect(blog.association(:posts)).to_not be_loaded
269
+ end
270
+ end
271
+
272
+ it "doesn't auto eager loads a has_many association when exists? is called" do
273
+ blogs = Blog.order(:name).to_a
274
+ blogs.first.posts.exists?
275
+
276
+ blogs.each do |blog|
277
+ expect(blog.association(:posts)).to_not be_loaded
278
+ end
279
+ end
280
+
281
+ it "doesn't auto eager loads a has_many association when last is called" do
282
+ blogs = Blog.order(:name).to_a
283
+ blogs.first.posts.last
284
+
285
+ blogs.each do |blog|
286
+ expect(blog.association(:posts)).to_not be_loaded
287
+ end
288
+ end
289
+
290
+ it "doesn't auto eager loads a has_many association when ids is called" do
291
+ blogs = Blog.order(:name).to_a
292
+ blogs.first.post_ids
293
+
294
+ blogs.each do |blog|
295
+ expect(blog.association(:posts)).to_not be_loaded
296
+ end
297
+ end
298
+ end
299
+
300
+ context "with fully_load true" do
301
+
302
+ it "auto eager loads a has_many association when size is called" do
303
+ blogs = Blog.order(:name).to_a
304
+ blogs.first.posts_fully_load.size
305
+
306
+ blogs.each do |blog|
307
+ expect(blog.association(:posts_fully_load)).to be_loaded
308
+ end
309
+ end
310
+
311
+ it "auto eager loads a has_many association when exists? is called" do
312
+ blogs = Blog.order(:name).to_a
313
+ blogs.first.posts_fully_load.exists?
314
+
315
+ blogs.each do |blog|
316
+ expect(blog.association(:posts_fully_load)).to be_loaded
317
+ end
318
+ end
319
+
320
+ it "auto eager loads a has_many association when last is called" do
321
+ blogs = Blog.order(:name).to_a
322
+ blogs.first.posts_fully_load.last
323
+
324
+ blogs.each do |blog|
325
+ expect(blog.association(:posts_fully_load)).to be_loaded
326
+ end
327
+ end
328
+
329
+ it "auto eager loads a has_many association when ids is called" do
330
+ blogs = Blog.order(:name).to_a
331
+ blogs.first.posts_fully_load_ids
332
+
333
+ blogs.each do |blog|
334
+ expect(blog.association(:posts_fully_load)).to be_loaded
335
+ end
336
+ end
337
+
338
+ end
339
+
340
+ context "with auto_include disabled" do
341
+
342
+ it "doesn't auto eager load has_many associations" do
343
+ blogs = Blog.order(:name).to_a
344
+
345
+ # Force the first blogs first post to load
346
+ posts = blogs.first.posts_without_auto_include.to_a
347
+ expect(posts).to match_array Post.where(blog_id: blogs.first.id)
348
+
349
+ blogs.drop(1).each do |blog|
350
+ expect(blog.association(:posts_without_auto_include)).to_not be_loaded
351
+ end
352
+ end
353
+
354
+ it "doesn't auto eager load has_one associations" do
355
+ users = User.order(:name).to_a
356
+
357
+ # Force the first user's address to load
358
+ user = users.first
359
+ address = user.address_without_auto_include
360
+ expect(address).to eq Address.where(user_id: user.id).first
361
+
362
+ users.drop(1).each do |blog|
363
+ expect(blog.association(:address_without_auto_include)).to_not be_loaded
364
+ end
365
+ end
366
+
367
+ it "doesn't auto eager load belongs_to associations" do
368
+ posts = Post.order(:title).to_a
369
+ # Force the first post's blog to load
370
+ post = posts.first
371
+ blog = post.blog_without_auto_include
372
+ expect(blog).to eq Blog.where(id: post.blog_id).first
373
+
374
+ posts.drop(1).each do |blog|
375
+ expect(blog.association(:blog_without_auto_include)).to_not be_loaded
376
+ end
377
+ end
378
+
379
+ it "still auto eager loads nested associations" do
380
+ posts = Post.order(:title).to_a
381
+ # Force the first post's blog to load
382
+ blog = posts.first.blog_without_auto_include
383
+
384
+ # Load another blogs posts
385
+ other_blog = posts.last.blog_without_auto_include
386
+ other_blog.posts.to_a
387
+
388
+ blog.posts.to_a.first.tags.to_a
389
+
390
+ blog.posts.each do |post|
391
+ expect(post.association(:tags)).to be_loaded
392
+ end
393
+
394
+ other_blog.posts.each do |post|
395
+ expect(post.association(:tags)).to_not be_loaded
396
+ end
397
+ end
398
+ end
399
+ end
@@ -0,0 +1,51 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'simplecov'
4
+ require 'coveralls'
5
+
6
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
7
+ SimpleCov::Formatter::HTMLFormatter,
8
+ Coveralls::SimpleCov::Formatter
9
+ ]
10
+ SimpleCov.start do
11
+ add_filter 'spec'
12
+ end
13
+
14
+ require 'logger'
15
+ require 'database_cleaner'
16
+ require 'goldiloader'
17
+ require 'yaml'
18
+
19
+ spec_dir = File.dirname(__FILE__)
20
+ Dir["#{spec_dir}/support/**/*.rb"].sort.each { |f| require f }
21
+
22
+ FileUtils.makedirs('log')
23
+
24
+ ActiveRecord::Base.logger = Logger.new('log/test.log')
25
+ ActiveRecord::Base.logger.level = Logger::DEBUG
26
+ ActiveRecord::Migration.verbose = false
27
+
28
+ db_adapter = ENV.fetch('ADAPTER', 'sqlite3')
29
+ config = YAML.load(File.read('spec/db/database.yml'))
30
+ ActiveRecord::Base.establish_connection(config[db_adapter])
31
+ require 'db/schema'
32
+
33
+ RSpec.configure do |config|
34
+ config.order = 'random'
35
+
36
+ config.before(:suite) do
37
+ DatabaseCleaner.clean_with(:truncation)
38
+ end
39
+
40
+ config.before(:each) do
41
+ DatabaseCleaner.strategy = :transaction
42
+ end
43
+
44
+ config.before(:each) do
45
+ DatabaseCleaner.start
46
+ end
47
+
48
+ config.after(:each) do
49
+ DatabaseCleaner.clean
50
+ end
51
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'active_support/version'
4
+ require 'active_record/version'
5
+
6
+ module Goldiloader
7
+ module Compatibility
8
+
9
+ def self.mass_assignment_security_enabled?
10
+ ::ActiveRecord::VERSION::MAJOR < 4 || defined?(::ActiveRecord::MassAssignmentSecurity)
11
+ end
12
+
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,193 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: goldiloader
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Joel Turkel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ - - <=
21
+ - !ruby/object:Gem::Version
22
+ version: '4.1'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ - - <=
31
+ - !ruby/object:Gem::Version
32
+ version: '4.1'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activesupport
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '3.2'
40
+ - - <=
41
+ - !ruby/object:Gem::Version
42
+ version: '4.1'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - '>='
48
+ - !ruby/object:Gem::Version
49
+ version: '3.2'
50
+ - - <=
51
+ - !ruby/object:Gem::Version
52
+ version: '4.1'
53
+ - !ruby/object:Gem::Dependency
54
+ name: coveralls
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ - !ruby/object:Gem::Dependency
68
+ name: database_cleaner
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '1.2'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '1.2'
81
+ - !ruby/object:Gem::Dependency
82
+ name: rake
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: rspec
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: '2'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ~>
107
+ - !ruby/object:Gem::Version
108
+ version: '2'
109
+ - !ruby/object:Gem::Dependency
110
+ name: simplecov
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ~>
114
+ - !ruby/object:Gem::Version
115
+ version: 0.7.1
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ~>
121
+ - !ruby/object:Gem::Version
122
+ version: 0.7.1
123
+ - !ruby/object:Gem::Dependency
124
+ name: sqlite3
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - '>='
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ type: :development
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ description: Automatically eager loads Rails associations as associations are traversed
138
+ email:
139
+ - jturkel@salsify.com
140
+ executables: []
141
+ extensions: []
142
+ extra_rdoc_files: []
143
+ files:
144
+ - .gitignore
145
+ - .rspec
146
+ - .travis.yml
147
+ - Gemfile
148
+ - LICENSE.txt
149
+ - README.md
150
+ - Rakefile
151
+ - goldiloader.gemspec
152
+ - lib/goldiloader.rb
153
+ - lib/goldiloader/active_record_patches.rb
154
+ - lib/goldiloader/association_loader.rb
155
+ - lib/goldiloader/association_options.rb
156
+ - lib/goldiloader/auto_include_context.rb
157
+ - lib/goldiloader/model_registry.rb
158
+ - lib/goldiloader/version.rb
159
+ - spec/db/database.yml
160
+ - spec/db/schema.rb
161
+ - spec/goldiloader/goldiloader_spec.rb
162
+ - spec/spec_helper.rb
163
+ - spec/support/compatibility.rb
164
+ homepage: https://github.com/salsify/goldiloader
165
+ licenses:
166
+ - MIT
167
+ metadata: {}
168
+ post_install_message:
169
+ rdoc_options: []
170
+ require_paths:
171
+ - lib
172
+ required_ruby_version: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - '>='
175
+ - !ruby/object:Gem::Version
176
+ version: '0'
177
+ required_rubygems_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - '>='
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ requirements: []
183
+ rubyforge_project:
184
+ rubygems_version: 2.0.14
185
+ signing_key:
186
+ specification_version: 4
187
+ summary: Automatic Rails association eager loading
188
+ test_files:
189
+ - spec/db/database.yml
190
+ - spec/db/schema.rb
191
+ - spec/goldiloader/goldiloader_spec.rb
192
+ - spec/spec_helper.rb
193
+ - spec/support/compatibility.rb