1
0
Fork 0
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:
Jon Leighton 2011-03-10 19:04:00 +00:00
parent 67b17d029a
commit 6490d65234
9 changed files with 162 additions and 215 deletions

View file

@ -142,6 +142,7 @@ module ActiveRecord
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 :AssociationScope, 'active_record/associations/association_scope'
autoload :AliasTracker, 'active_record/associations/alias_tracker' autoload :AliasTracker, 'active_record/associations/alias_tracker'
# Clears out the association cache. # Clears out the association cache.

View file

@ -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
@association_scope = AssociationScope.new(self).scope
end end
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
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

View 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

View file

@ -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])

View file

@ -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

View file

@ -94,8 +94,6 @@ module ActiveRecord
end end
end end
end end
alias creation_attributes construct_owner_attributes
end end
end end
end end

View file

@ -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

View file

@ -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

View file

@ -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