mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Move the code which builds a scope for through associations into a generic AssociationScope class which is capable of building a scope for any association.
This commit is contained in:
parent
67b17d029a
commit
6490d65234
9 changed files with 162 additions and 215 deletions
|
@ -140,9 +140,10 @@ module ActiveRecord
|
||||||
autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
|
autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
|
||||||
end
|
end
|
||||||
|
|
||||||
autoload :Preloader, 'active_record/associations/preloader'
|
autoload :Preloader, 'active_record/associations/preloader'
|
||||||
autoload :JoinDependency, 'active_record/associations/join_dependency'
|
autoload :JoinDependency, 'active_record/associations/join_dependency'
|
||||||
autoload :AliasTracker, 'active_record/associations/alias_tracker'
|
autoload :AssociationScope, 'active_record/associations/association_scope'
|
||||||
|
autoload :AliasTracker, 'active_record/associations/alias_tracker'
|
||||||
|
|
||||||
# Clears out the association cache.
|
# Clears out the association cache.
|
||||||
def clear_association_cache #:nodoc:
|
def clear_association_cache #:nodoc:
|
||||||
|
|
|
@ -93,23 +93,9 @@ module ActiveRecord
|
||||||
# by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
|
# by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
|
||||||
# actually gets built.
|
# actually gets built.
|
||||||
def construct_scope
|
def construct_scope
|
||||||
@association_scope = association_scope if klass
|
if klass
|
||||||
end
|
@association_scope = AssociationScope.new(self).scope
|
||||||
|
|
||||||
def association_scope
|
|
||||||
scope = klass.unscoped
|
|
||||||
scope = scope.create_with(creation_attributes)
|
|
||||||
scope = scope.apply_finder_options(options.slice(:readonly, :include))
|
|
||||||
scope = scope.where(interpolate(options[:conditions]))
|
|
||||||
if select = select_value
|
|
||||||
scope = scope.select(select)
|
|
||||||
end
|
end
|
||||||
scope = scope.extending(*Array.wrap(options[:extend]))
|
|
||||||
scope.where(construct_owner_conditions)
|
|
||||||
end
|
|
||||||
|
|
||||||
def aliased_table
|
|
||||||
klass.arel_table
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set the inverse association, if possible
|
# Set the inverse association, if possible
|
||||||
|
@ -174,42 +160,24 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_value
|
|
||||||
options[:select]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Implemented by (some) subclasses
|
|
||||||
def creation_attributes
|
def creation_attributes
|
||||||
{ }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns a hash linking the owner to the association represented by the reflection
|
|
||||||
def construct_owner_attributes(reflection = reflection)
|
|
||||||
attributes = {}
|
attributes = {}
|
||||||
if reflection.macro == :belongs_to
|
|
||||||
attributes[reflection.association_primary_key] = owner[reflection.foreign_key]
|
if [:has_one, :has_many].include?(reflection.macro) && !options[:through]
|
||||||
else
|
|
||||||
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
|
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
|
||||||
|
|
||||||
if reflection.options[:as]
|
if reflection.options[:as]
|
||||||
attributes[reflection.type] = owner.class.base_class.name
|
attributes[reflection.type] = owner.class.base_class.name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
attributes
|
|
||||||
end
|
|
||||||
|
|
||||||
# Builds an array of arel nodes from the owner attributes hash
|
attributes
|
||||||
def construct_owner_conditions(table = aliased_table, reflection = reflection)
|
|
||||||
conditions = construct_owner_attributes(reflection).map do |attr, value|
|
|
||||||
table[attr].eq(value)
|
|
||||||
end
|
|
||||||
table.create_and(conditions)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sets the owner attributes on the given record
|
# Sets the owner attributes on the given record
|
||||||
def set_owner_attributes(record)
|
def set_owner_attributes(record)
|
||||||
if owner.persisted?
|
if owner.persisted?
|
||||||
construct_owner_attributes.each { |key, value| record[key] = value }
|
creation_attributes.each { |key, value| record[key] = value }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
149
activerecord/lib/active_record/associations/association_scope.rb
Normal file
149
activerecord/lib/active_record/associations/association_scope.rb
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
module ActiveRecord
|
||||||
|
module Associations
|
||||||
|
class AssociationScope #:nodoc:
|
||||||
|
attr_reader :association, :alias_tracker
|
||||||
|
|
||||||
|
delegate :klass, :owner, :reflection, :interpolate, :to => :association
|
||||||
|
delegate :through_reflection_chain, :through_conditions, :options, :source_options, :to => :reflection
|
||||||
|
|
||||||
|
def initialize(association)
|
||||||
|
@association = association
|
||||||
|
@alias_tracker = AliasTracker.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def scope
|
||||||
|
scope = klass.unscoped
|
||||||
|
scope = scope.extending(*Array.wrap(options[:extend]))
|
||||||
|
|
||||||
|
# It's okay to just apply all these like this. The options will only be present if the
|
||||||
|
# association supports that option; this is enforced by the association builder.
|
||||||
|
scope = scope.apply_finder_options(options.slice(
|
||||||
|
:readonly, :include, :order, :limit, :joins, :group, :having, :offset))
|
||||||
|
|
||||||
|
if options[:through] && !options[:include]
|
||||||
|
scope = scope.includes(source_options[:include])
|
||||||
|
end
|
||||||
|
|
||||||
|
if select = select_value
|
||||||
|
scope = scope.select(select)
|
||||||
|
end
|
||||||
|
|
||||||
|
add_constraints(scope)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def select_value
|
||||||
|
select_value = options[:select]
|
||||||
|
|
||||||
|
if reflection.collection?
|
||||||
|
select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*"
|
||||||
|
end
|
||||||
|
|
||||||
|
if reflection.macro == :has_and_belongs_to_many
|
||||||
|
select_value ||= reflection.klass.arel_table[Arel.star]
|
||||||
|
end
|
||||||
|
|
||||||
|
select_value
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_constraints(scope)
|
||||||
|
tables = construct_tables
|
||||||
|
|
||||||
|
through_reflection_chain.each_with_index do |reflection, i|
|
||||||
|
table, foreign_table = tables.shift, tables.first
|
||||||
|
|
||||||
|
if reflection.source_macro == :has_and_belongs_to_many
|
||||||
|
join_table = tables.shift
|
||||||
|
|
||||||
|
scope = scope.joins(inner_join(
|
||||||
|
join_table, reflection,
|
||||||
|
table[reflection.active_record_primary_key].
|
||||||
|
eq(join_table[reflection.association_foreign_key])
|
||||||
|
))
|
||||||
|
|
||||||
|
table, foreign_table = join_table, tables.first
|
||||||
|
end
|
||||||
|
|
||||||
|
if reflection.source_macro == :belongs_to
|
||||||
|
key = reflection.association_primary_key
|
||||||
|
foreign_key = reflection.foreign_key
|
||||||
|
else
|
||||||
|
key = reflection.foreign_key
|
||||||
|
foreign_key = reflection.active_record_primary_key
|
||||||
|
end
|
||||||
|
|
||||||
|
if reflection == through_reflection_chain.last
|
||||||
|
scope = scope.where(table[key].eq(owner[foreign_key]))
|
||||||
|
|
||||||
|
through_conditions[i].each do |condition|
|
||||||
|
if options[:through] && condition.is_a?(Hash)
|
||||||
|
condition = { table.name => condition }
|
||||||
|
end
|
||||||
|
|
||||||
|
scope = scope.where(interpolate(condition))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
constraint = table[key].eq foreign_table[foreign_key]
|
||||||
|
|
||||||
|
join = inner_join(foreign_table, reflection, constraint, *through_conditions[i])
|
||||||
|
scope = scope.joins(join)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def construct_tables
|
||||||
|
tables = []
|
||||||
|
through_reflection_chain.each do |reflection|
|
||||||
|
tables << alias_tracker.aliased_table_for(
|
||||||
|
table_name_for(reflection),
|
||||||
|
table_alias_for(reflection, reflection != self.reflection)
|
||||||
|
)
|
||||||
|
|
||||||
|
if reflection.source_macro == :has_and_belongs_to_many
|
||||||
|
tables << alias_tracker.aliased_table_for(
|
||||||
|
(reflection.source_reflection || reflection).options[:join_table],
|
||||||
|
table_alias_for(reflection, true)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
tables
|
||||||
|
end
|
||||||
|
|
||||||
|
def table_name_for(reflection)
|
||||||
|
if reflection == self.reflection
|
||||||
|
# If this is a polymorphic belongs_to, we want to get the klass from the
|
||||||
|
# association because it depends on the polymorphic_type attribute of
|
||||||
|
# the owner
|
||||||
|
klass.table_name
|
||||||
|
else
|
||||||
|
reflection.table_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def table_alias_for(reflection, join = false)
|
||||||
|
name = alias_tracker.pluralize(reflection.name)
|
||||||
|
name << "_#{self.reflection.name}"
|
||||||
|
name << "_join" if join
|
||||||
|
name
|
||||||
|
end
|
||||||
|
|
||||||
|
def inner_join(table, reflection, *conditions)
|
||||||
|
conditions = sanitize_conditions(reflection, conditions)
|
||||||
|
table.create_join(table, table.create_on(conditions))
|
||||||
|
end
|
||||||
|
|
||||||
|
def sanitize_conditions(reflection, conditions)
|
||||||
|
conditions = conditions.map do |condition|
|
||||||
|
condition = reflection.klass.send(:sanitize_sql, interpolate(condition), reflection.table_name)
|
||||||
|
condition = Arel.sql(condition) unless condition.is_a?(Arel::Node)
|
||||||
|
condition
|
||||||
|
end
|
||||||
|
|
||||||
|
conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -331,11 +331,6 @@ module ActiveRecord
|
||||||
@scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args)
|
@scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def association_scope
|
|
||||||
options = reflection.options.slice(:order, :limit, :joins, :group, :having, :offset)
|
|
||||||
super.apply_finder_options(options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_target
|
def load_target
|
||||||
if find_target?
|
if find_target?
|
||||||
targets = []
|
targets = []
|
||||||
|
@ -373,14 +368,6 @@ module ActiveRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def select_value
|
|
||||||
super || uniq_select_value
|
|
||||||
end
|
|
||||||
|
|
||||||
def uniq_select_value
|
|
||||||
options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*"
|
|
||||||
end
|
|
||||||
|
|
||||||
def custom_counter_sql
|
def custom_counter_sql
|
||||||
if options[:counter_sql]
|
if options[:counter_sql]
|
||||||
interpolate(options[:counter_sql])
|
interpolate(options[:counter_sql])
|
||||||
|
|
|
@ -26,10 +26,6 @@ module ActiveRecord
|
||||||
record
|
record
|
||||||
end
|
end
|
||||||
|
|
||||||
def association_scope
|
|
||||||
super.joins(construct_joins)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def count_records
|
def count_records
|
||||||
|
@ -48,24 +44,6 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def construct_joins
|
|
||||||
right = join_table
|
|
||||||
left = reflection.klass.arel_table
|
|
||||||
|
|
||||||
condition = left[reflection.klass.primary_key].eq(
|
|
||||||
right[reflection.association_foreign_key])
|
|
||||||
|
|
||||||
right.create_join(right, right.create_on(condition))
|
|
||||||
end
|
|
||||||
|
|
||||||
def construct_owner_conditions
|
|
||||||
super(join_table)
|
|
||||||
end
|
|
||||||
|
|
||||||
def select_value
|
|
||||||
super || reflection.klass.arel_table[Arel.star]
|
|
||||||
end
|
|
||||||
|
|
||||||
def invertible_for?(record)
|
def invertible_for?(record)
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
|
@ -94,8 +94,6 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
alias creation_attributes construct_owner_attributes
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,14 +39,8 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def association_scope
|
|
||||||
super.order(options[:order])
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
alias creation_attributes construct_owner_attributes
|
|
||||||
|
|
||||||
# The reason that the save param for replace is false, if for create (not just build),
|
# The reason that the save param for replace is false, if for create (not just build),
|
||||||
# is because the setting of the foreign keys is actually handled by the scoping when
|
# is because the setting of the foreign keys is actually handled by the scoping when
|
||||||
# the record is instantiated, and so they are set straight away and do not need to be
|
# the record is instantiated, and so they are set straight away and do not need to be
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
require 'enumerator'
|
|
||||||
|
|
||||||
module ActiveRecord
|
module ActiveRecord
|
||||||
# = Active Record Through Association
|
# = Active Record Through Association
|
||||||
module Associations
|
module Associations
|
||||||
|
@ -26,77 +24,8 @@ module ActiveRecord
|
||||||
scope
|
scope
|
||||||
end
|
end
|
||||||
|
|
||||||
def association_scope
|
|
||||||
scope = join_to(super)
|
|
||||||
|
|
||||||
unless options[:include]
|
|
||||||
scope = scope.includes(source_options[:include])
|
|
||||||
end
|
|
||||||
|
|
||||||
scope
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# This scope affects the creation of the associated records (not the join records). At the
|
|
||||||
# moment we only support creating on a :through association when the source reflection is a
|
|
||||||
# belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so
|
|
||||||
# this scope has can legitimately be empty.
|
|
||||||
def creation_attributes
|
|
||||||
{ }
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: Needed?
|
|
||||||
def aliased_through_table
|
|
||||||
name = through_reflection.table_name
|
|
||||||
|
|
||||||
reflection.table_name == name ?
|
|
||||||
through_reflection.klass.arel_table.alias(name + "_join") :
|
|
||||||
through_reflection.klass.arel_table
|
|
||||||
end
|
|
||||||
|
|
||||||
def construct_owner_conditions
|
|
||||||
end
|
|
||||||
|
|
||||||
def join_to(scope)
|
|
||||||
joins = []
|
|
||||||
tables = tables().dup # FIXME: Ugly
|
|
||||||
|
|
||||||
through_reflection_chain.each_with_index do |reflection, i|
|
|
||||||
table, foreign_table = tables.shift, tables.first
|
|
||||||
|
|
||||||
if reflection.source_macro == :has_and_belongs_to_many
|
|
||||||
join_table = tables.shift
|
|
||||||
|
|
||||||
joins << inner_join(
|
|
||||||
join_table,
|
|
||||||
table[reflection.active_record_primary_key].
|
|
||||||
eq(join_table[reflection.association_foreign_key])
|
|
||||||
)
|
|
||||||
|
|
||||||
table, foreign_table = join_table, tables.first
|
|
||||||
end
|
|
||||||
|
|
||||||
if reflection.source_macro == :belongs_to
|
|
||||||
key = reflection.association_primary_key
|
|
||||||
foreign_key = reflection.foreign_key
|
|
||||||
else
|
|
||||||
key = reflection.foreign_key
|
|
||||||
foreign_key = reflection.active_record_primary_key
|
|
||||||
end
|
|
||||||
|
|
||||||
if reflection == through_reflection_chain.last
|
|
||||||
constraint = table[key].eq owner[foreign_key]
|
|
||||||
scope = scope.where(constraint).where(reflection_conditions(i))
|
|
||||||
else
|
|
||||||
constraint = table[key].eq foreign_table[foreign_key]
|
|
||||||
joins << inner_join(foreign_table, constraint, reflection_conditions(i))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
scope.joins(joins)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Construct attributes for :through pointing to owner and associate. This is used by the
|
# Construct attributes for :through pointing to owner and associate. This is used by the
|
||||||
# methods which create and delete records on the association.
|
# methods which create and delete records on the association.
|
||||||
#
|
#
|
||||||
|
@ -133,63 +62,6 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def alias_tracker
|
|
||||||
@alias_tracker ||= AliasTracker.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def tables
|
|
||||||
@tables ||= begin
|
|
||||||
tables = []
|
|
||||||
through_reflection_chain.each do |reflection|
|
|
||||||
tables << alias_tracker.aliased_table_for(
|
|
||||||
reflection.table_name,
|
|
||||||
table_alias_for(reflection, reflection != self.reflection)
|
|
||||||
)
|
|
||||||
|
|
||||||
if reflection.macro == :has_and_belongs_to_many ||
|
|
||||||
(reflection.source_reflection &&
|
|
||||||
reflection.source_reflection.macro == :has_and_belongs_to_many)
|
|
||||||
|
|
||||||
tables << alias_tracker.aliased_table_for(
|
|
||||||
(reflection.source_reflection || reflection).options[:join_table],
|
|
||||||
table_alias_for(reflection, true)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
tables
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def table_alias_for(reflection, join = false)
|
|
||||||
name = alias_tracker.pluralize(reflection.name)
|
|
||||||
name << "_#{self.reflection.name}"
|
|
||||||
name << "_join" if join
|
|
||||||
name
|
|
||||||
end
|
|
||||||
|
|
||||||
def inner_join(table, *conditions)
|
|
||||||
table.create_join(
|
|
||||||
table,
|
|
||||||
table.create_on(table.create_and(conditions.flatten.compact)))
|
|
||||||
end
|
|
||||||
|
|
||||||
def reflection_conditions(index)
|
|
||||||
reflection = through_reflection_chain[index]
|
|
||||||
conditions = through_conditions[index]
|
|
||||||
|
|
||||||
unless conditions.empty?
|
|
||||||
Arel::Nodes::And.new(process_conditions(conditions, reflection))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_conditions(conditions, reflection)
|
|
||||||
conditions.map do |condition|
|
|
||||||
condition = reflection.klass.send(:sanitize_sql, interpolate(condition), reflection.table_name)
|
|
||||||
condition = Arel.sql(condition) unless condition.is_a?(Arel::Node)
|
|
||||||
condition
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: Think about this in the context of nested associations
|
# TODO: Think about this in the context of nested associations
|
||||||
def stale_state
|
def stale_state
|
||||||
if through_reflection.macro == :belongs_to
|
if through_reflection.macro == :belongs_to
|
||||||
|
|
|
@ -270,9 +270,9 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def through_conditions
|
def through_conditions
|
||||||
through_conditions = [Array.wrap(options[:conditions])]
|
conditions = [options[:conditions]].compact
|
||||||
through_conditions.first << { type => active_record.base_class.name } if options[:as]
|
conditions << { type => active_record.base_class.name } if options[:as]
|
||||||
through_conditions
|
[conditions]
|
||||||
end
|
end
|
||||||
|
|
||||||
def source_reflection
|
def source_reflection
|
||||||
|
|
Loading…
Reference in a new issue