mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #38541 from kddeisz/strict-loading-associations
Support strict_loading on association declarations
This commit is contained in:
commit
3dff30c14f
8 changed files with 140 additions and 2 deletions
|
@ -1,3 +1,21 @@
|
||||||
|
* Add support for `strict_loading` mode on association declarations.
|
||||||
|
|
||||||
|
Raise an error if attempting to load a record from an association that has been marked as `strict_loading` unless it was explicitly eager loaded.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```
|
||||||
|
>> class Developer < ApplicationRecord
|
||||||
|
>> has_many :projects, strict_loading: true
|
||||||
|
>> end
|
||||||
|
>>
|
||||||
|
>> dev = Developer.first
|
||||||
|
>> dev.projects.first
|
||||||
|
=> ActiveRecord::StrictLoadingViolationError: The projects association is marked as strict_loading and cannot be lazily loaded.
|
||||||
|
```
|
||||||
|
|
||||||
|
*Kevin Deisz*
|
||||||
|
|
||||||
* Add support for `strict_loading` mode to prevent lazy loading of records.
|
* Add support for `strict_loading` mode to prevent lazy loading of records.
|
||||||
|
|
||||||
Raise an error if a parent record is marked as `strict_loading` and attempts to lazily load its associations. This is useful for finding places you may want to preload an association and avoid additional queries.
|
Raise an error if a parent record is marked as `strict_loading` and attempts to lazily load its associations. This is useful for finding places you may want to preload an association and avoid additional queries.
|
||||||
|
|
|
@ -1355,6 +1355,8 @@ module ActiveRecord
|
||||||
# Specifies a module or array of modules that will be extended into the association object returned.
|
# Specifies a module or array of modules that will be extended into the association object returned.
|
||||||
# Useful for defining methods on associations, especially when they should be shared between multiple
|
# Useful for defining methods on associations, especially when they should be shared between multiple
|
||||||
# association objects.
|
# association objects.
|
||||||
|
# [:strict_loading]
|
||||||
|
# Enforces strict loading every time the associated record is loaded through this association.
|
||||||
#
|
#
|
||||||
# Option examples:
|
# Option examples:
|
||||||
# has_many :comments, -> { order("posted_on") }
|
# has_many :comments, -> { order("posted_on") }
|
||||||
|
@ -1365,6 +1367,7 @@ module ActiveRecord
|
||||||
# has_many :tags, as: :taggable
|
# has_many :tags, as: :taggable
|
||||||
# has_many :reports, -> { readonly }
|
# has_many :reports, -> { readonly }
|
||||||
# has_many :subscribers, through: :subscriptions, source: :user
|
# has_many :subscribers, through: :subscriptions, source: :user
|
||||||
|
# has_many :comments, strict_loading: true
|
||||||
def has_many(name, scope = nil, **options, &extension)
|
def has_many(name, scope = nil, **options, &extension)
|
||||||
reflection = Builder::HasMany.build(self, name, scope, options, &extension)
|
reflection = Builder::HasMany.build(self, name, scope, options, &extension)
|
||||||
Reflection.add_reflection self, name, reflection
|
Reflection.add_reflection self, name, reflection
|
||||||
|
@ -1494,6 +1497,8 @@ module ActiveRecord
|
||||||
# When set to +true+, the association will also have its presence validated.
|
# When set to +true+, the association will also have its presence validated.
|
||||||
# This will validate the association itself, not the id. You can use
|
# This will validate the association itself, not the id. You can use
|
||||||
# +:inverse_of+ to avoid an extra query during validation.
|
# +:inverse_of+ to avoid an extra query during validation.
|
||||||
|
# [:strict_loading]
|
||||||
|
# Enforces strict loading every time the associated record is loaded through this association.
|
||||||
#
|
#
|
||||||
# Option examples:
|
# Option examples:
|
||||||
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
|
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
|
||||||
|
@ -1506,6 +1511,7 @@ module ActiveRecord
|
||||||
# has_one :club, through: :membership
|
# has_one :club, through: :membership
|
||||||
# has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
|
# has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
|
||||||
# has_one :credit_card, required: true
|
# has_one :credit_card, required: true
|
||||||
|
# has_one :credit_card, strict_loading: true
|
||||||
def has_one(name, scope = nil, **options)
|
def has_one(name, scope = nil, **options)
|
||||||
reflection = Builder::HasOne.build(self, name, scope, options)
|
reflection = Builder::HasOne.build(self, name, scope, options)
|
||||||
Reflection.add_reflection self, name, reflection
|
Reflection.add_reflection self, name, reflection
|
||||||
|
@ -1639,6 +1645,8 @@ module ActiveRecord
|
||||||
# [:default]
|
# [:default]
|
||||||
# Provide a callable (i.e. proc or lambda) to specify that the association should
|
# Provide a callable (i.e. proc or lambda) to specify that the association should
|
||||||
# be initialized with a particular record before validation.
|
# be initialized with a particular record before validation.
|
||||||
|
# [:strict_loading]
|
||||||
|
# Enforces strict loading every time the associated record is loaded through this association.
|
||||||
#
|
#
|
||||||
# Option examples:
|
# Option examples:
|
||||||
# belongs_to :firm, foreign_key: "client_of"
|
# belongs_to :firm, foreign_key: "client_of"
|
||||||
|
@ -1653,6 +1661,7 @@ module ActiveRecord
|
||||||
# belongs_to :company, touch: :employees_last_updated_at
|
# belongs_to :company, touch: :employees_last_updated_at
|
||||||
# belongs_to :user, optional: true
|
# belongs_to :user, optional: true
|
||||||
# belongs_to :account, default: -> { company.account }
|
# belongs_to :account, default: -> { company.account }
|
||||||
|
# belongs_to :account, strict_loading: true
|
||||||
def belongs_to(name, scope = nil, **options)
|
def belongs_to(name, scope = nil, **options)
|
||||||
reflection = Builder::BelongsTo.build(self, name, scope, options)
|
reflection = Builder::BelongsTo.build(self, name, scope, options)
|
||||||
Reflection.add_reflection self, name, reflection
|
Reflection.add_reflection self, name, reflection
|
||||||
|
@ -1815,6 +1824,8 @@ module ActiveRecord
|
||||||
#
|
#
|
||||||
# Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
|
# Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
|
||||||
# <tt>:autosave</tt> to <tt>true</tt>.
|
# <tt>:autosave</tt> to <tt>true</tt>.
|
||||||
|
# [:strict_loading]
|
||||||
|
# Enforces strict loading every time an associated record is loaded through this association.
|
||||||
#
|
#
|
||||||
# Option examples:
|
# Option examples:
|
||||||
# has_and_belongs_to_many :projects
|
# has_and_belongs_to_many :projects
|
||||||
|
@ -1822,6 +1833,7 @@ module ActiveRecord
|
||||||
# has_and_belongs_to_many :nations, class_name: "Country"
|
# has_and_belongs_to_many :nations, class_name: "Country"
|
||||||
# has_and_belongs_to_many :categories, join_table: "prods_cats"
|
# has_and_belongs_to_many :categories, join_table: "prods_cats"
|
||||||
# has_and_belongs_to_many :categories, -> { readonly }
|
# has_and_belongs_to_many :categories, -> { readonly }
|
||||||
|
# has_and_belongs_to_many :categories, strict_loading: true
|
||||||
def has_and_belongs_to_many(name, scope = nil, **options, &extension)
|
def has_and_belongs_to_many(name, scope = nil, **options, &extension)
|
||||||
habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
|
habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
|
||||||
|
|
||||||
|
@ -1852,7 +1864,7 @@ module ActiveRecord
|
||||||
hm_options[:through] = middle_reflection.name
|
hm_options[:through] = middle_reflection.name
|
||||||
hm_options[:source] = join_model.right_reflection.name
|
hm_options[:source] = join_model.right_reflection.name
|
||||||
|
|
||||||
[:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend].each do |k|
|
[:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend, :strict_loading].each do |k|
|
||||||
hm_options[k] = options[k] if options.key? k
|
hm_options[k] = options[k] if options.key? k
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -211,6 +211,10 @@ module ActiveRecord
|
||||||
raise StrictLoadingViolationError, "#{owner.class} is marked as strict_loading and #{klass} cannot be lazily loaded."
|
raise StrictLoadingViolationError, "#{owner.class} is marked as strict_loading and #{klass} cannot be lazily loaded."
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if reflection.strict_loading?
|
||||||
|
raise StrictLoadingViolationError, "The #{reflection.name} association is marked as strict_loading and cannot be lazily loaded."
|
||||||
|
end
|
||||||
|
|
||||||
scope = self.scope
|
scope = self.scope
|
||||||
return scope.to_a if skip_statement_cache?(scope)
|
return scope.to_a if skip_statement_cache?(scope)
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
||||||
self.extensions = []
|
self.extensions = []
|
||||||
|
|
||||||
VALID_OPTIONS = [
|
VALID_OPTIONS = [
|
||||||
:class_name, :anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of
|
:class_name, :anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading
|
||||||
].freeze # :nodoc:
|
].freeze # :nodoc:
|
||||||
|
|
||||||
def self.build(model, name, scope, options, &block)
|
def self.build(model, name, scope, options, &block)
|
||||||
|
|
|
@ -309,6 +309,7 @@ module ActiveRecord
|
||||||
def find_from_target?
|
def find_from_target?
|
||||||
loaded? ||
|
loaded? ||
|
||||||
owner.strict_loading? ||
|
owner.strict_loading? ||
|
||||||
|
reflection.strict_loading? ||
|
||||||
owner.new_record? ||
|
owner.new_record? ||
|
||||||
target.any? { |record| record.new_record? || record.changed? }
|
target.any? { |record| record.new_record? || record.changed? }
|
||||||
end
|
end
|
||||||
|
|
|
@ -306,6 +306,10 @@ module ActiveRecord
|
||||||
active_record_primary_key
|
active_record_primary_key
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def strict_loading?
|
||||||
|
options[:strict_loading]
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
def actual_source_reflection # FIXME: this is a horrible name
|
def actual_source_reflection # FIXME: this is a horrible name
|
||||||
self
|
self
|
||||||
|
|
|
@ -3,9 +3,14 @@
|
||||||
require "cases/helper"
|
require "cases/helper"
|
||||||
require "models/developer"
|
require "models/developer"
|
||||||
require "models/computer"
|
require "models/computer"
|
||||||
|
require "models/mentor"
|
||||||
|
require "models/project"
|
||||||
|
require "models/ship"
|
||||||
|
|
||||||
class StrictLoadingTest < ActiveRecord::TestCase
|
class StrictLoadingTest < ActiveRecord::TestCase
|
||||||
fixtures :developers
|
fixtures :developers
|
||||||
|
fixtures :projects
|
||||||
|
fixtures :ships
|
||||||
|
|
||||||
def test_strict_loading
|
def test_strict_loading
|
||||||
Developer.all.each { |d| assert_not d.strict_loading? }
|
Developer.all.each { |d| assert_not d.strict_loading? }
|
||||||
|
@ -67,4 +72,88 @@ class StrictLoadingTest < ActiveRecord::TestCase
|
||||||
dev.audit_logs.first
|
dev.audit_logs.first
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_raises_on_lazy_loading_a_strict_loading_belongs_to_relation
|
||||||
|
mentor = Mentor.create!(name: "Mentor")
|
||||||
|
|
||||||
|
developer = Developer.first
|
||||||
|
developer.update_column(:mentor_id, mentor.id)
|
||||||
|
|
||||||
|
assert_raises ActiveRecord::StrictLoadingViolationError do
|
||||||
|
developer.strict_loading_mentor
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_does_not_raise_on_eager_loading_a_strict_loading_belongs_to_relation
|
||||||
|
mentor = Mentor.create!(name: "Mentor")
|
||||||
|
|
||||||
|
Developer.first.update_column(:mentor_id, mentor.id)
|
||||||
|
developer = Developer.includes(:strict_loading_mentor).first
|
||||||
|
|
||||||
|
assert_nothing_raised { developer.strict_loading_mentor }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_raises_on_lazy_loading_a_strict_loading_has_one_relation
|
||||||
|
developer = Developer.first
|
||||||
|
ship = Ship.first
|
||||||
|
|
||||||
|
ship.update_column(:developer_id, developer.id)
|
||||||
|
|
||||||
|
assert_raises ActiveRecord::StrictLoadingViolationError do
|
||||||
|
developer.strict_loading_ship
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_does_not_raise_on_eager_loading_a_strict_loading_has_one_relation
|
||||||
|
Ship.first.update_column(:developer_id, Developer.first.id)
|
||||||
|
developer = Developer.includes(:strict_loading_ship).first
|
||||||
|
|
||||||
|
assert_nothing_raised { developer.strict_loading_ship }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_raises_on_lazy_loading_a_strict_loading_has_many_relation
|
||||||
|
developer = Developer.first
|
||||||
|
|
||||||
|
AuditLog.create(
|
||||||
|
3.times.map do
|
||||||
|
{ developer_id: developer.id, message: "I am message" }
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_raises ActiveRecord::StrictLoadingViolationError do
|
||||||
|
developer.strict_loading_opt_audit_logs.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_does_not_raise_on_eager_loading_a_strict_loading_has_many_relation
|
||||||
|
developer = Developer.first
|
||||||
|
|
||||||
|
AuditLog.create(
|
||||||
|
3.times.map do
|
||||||
|
{ developer_id: developer.id, message: "I am message" }
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
developer = Developer.includes(:strict_loading_opt_audit_logs).first
|
||||||
|
|
||||||
|
assert_nothing_raised { developer.strict_loading_opt_audit_logs.first }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_raises_on_lazy_loading_a_strict_loading_habtm_relation
|
||||||
|
developer = Developer.first
|
||||||
|
developer.projects << Project.first
|
||||||
|
|
||||||
|
assert_not developer.strict_loading_projects.loaded?
|
||||||
|
|
||||||
|
assert_raises ActiveRecord::StrictLoadingViolationError do
|
||||||
|
developer.strict_loading_projects.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_does_not_raise_on_eager_loading_a_strict_loading_habtm_relation
|
||||||
|
Developer.first.projects << Project.first
|
||||||
|
developer = Developer.includes(:strict_loading_projects).first
|
||||||
|
|
||||||
|
assert_nothing_raised { developer.strict_loading_projects.first }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,7 @@ class Developer < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
belongs_to :mentor
|
belongs_to :mentor
|
||||||
|
belongs_to :strict_loading_mentor, strict_loading: true, foreign_key: :mentor_id, class_name: "Mentor"
|
||||||
|
|
||||||
accepts_nested_attributes_for :projects
|
accepts_nested_attributes_for :projects
|
||||||
|
|
||||||
|
@ -45,6 +46,12 @@ class Developer < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
has_and_belongs_to_many :strict_loading_projects,
|
||||||
|
join_table: :developers_projects,
|
||||||
|
association_foreign_key: :project_id,
|
||||||
|
class_name: "Project",
|
||||||
|
strict_loading: true
|
||||||
|
|
||||||
has_and_belongs_to_many :special_projects, join_table: "developers_projects", association_foreign_key: "project_id"
|
has_and_belongs_to_many :special_projects, join_table: "developers_projects", association_foreign_key: "project_id"
|
||||||
has_and_belongs_to_many :sym_special_projects,
|
has_and_belongs_to_many :sym_special_projects,
|
||||||
join_table: :developers_projects,
|
join_table: :developers_projects,
|
||||||
|
@ -53,11 +60,14 @@ class Developer < ActiveRecord::Base
|
||||||
|
|
||||||
has_many :audit_logs
|
has_many :audit_logs
|
||||||
has_many :strict_loading_audit_logs, -> { strict_loading }, class_name: "AuditLog"
|
has_many :strict_loading_audit_logs, -> { strict_loading }, class_name: "AuditLog"
|
||||||
|
has_many :strict_loading_opt_audit_logs, strict_loading: true, class_name: "AuditLog"
|
||||||
has_many :contracts
|
has_many :contracts
|
||||||
has_many :firms, through: :contracts, source: :firm
|
has_many :firms, through: :contracts, source: :firm
|
||||||
has_many :comments, ->(developer) { where(body: "I'm #{developer.name}") }
|
has_many :comments, ->(developer) { where(body: "I'm #{developer.name}") }
|
||||||
has_many :ratings, through: :comments
|
has_many :ratings, through: :comments
|
||||||
|
|
||||||
has_one :ship, dependent: :nullify
|
has_one :ship, dependent: :nullify
|
||||||
|
has_one :strict_loading_ship, strict_loading: true, class_name: "Ship"
|
||||||
|
|
||||||
belongs_to :firm
|
belongs_to :firm
|
||||||
has_many :contracted_projects, class_name: "Project"
|
has_many :contracted_projects, class_name: "Project"
|
||||||
|
|
Loading…
Reference in a new issue