diff options
Diffstat (limited to 'activerecord/lib/active_record/associations/through_association.rb')
-rw-r--r-- | activerecord/lib/active_record/associations/through_association.rb | 219 |
1 files changed, 106 insertions, 113 deletions
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index d2112fb2b6..c840a16160 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -5,140 +5,133 @@ module ActiveRecord protected - def target_scope - super & @reflection.through_reflection.klass.scoped - end - - def association_scope - scope = super.joins(construct_joins).where(conditions) - unless @reflection.options[:include] - scope = scope.includes(@reflection.source_reflection.options[:include]) + def target_scope + super & @reflection.through_reflection.klass.scoped end - scope - end - - # 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 - - def aliased_through_table - name = @reflection.through_reflection.table_name - - @reflection.table_name == name ? - @reflection.through_reflection.klass.arel_table.alias(name + "_join") : - @reflection.through_reflection.klass.arel_table - end - - def construct_owner_conditions - super(aliased_through_table, @reflection.through_reflection) - end - - def construct_joins - right = aliased_through_table - left = @reflection.klass.arel_table - - conditions = [] - - if @reflection.source_reflection.macro == :belongs_to - reflection_primary_key = @reflection.source_reflection.options[:primary_key] || - @reflection.klass.primary_key - source_primary_key = @reflection.source_reflection.foreign_key - if @reflection.options[:source_type] - column = @reflection.source_reflection.foreign_type - conditions << - right[column].eq(@reflection.options[:source_type]) - end - else - reflection_primary_key = @reflection.source_reflection.foreign_key - source_primary_key = @reflection.source_reflection.options[:primary_key] || - @reflection.through_reflection.klass.primary_key - if @reflection.source_reflection.options[:as] - column = "#{@reflection.source_reflection.options[:as]}_type" - conditions << - left[column].eq(@reflection.through_reflection.klass.name) + + def association_scope + scope = super.joins(construct_joins) + scope = add_conditions(scope) + unless @reflection.options[:include] + scope = scope.includes(@reflection.source_reflection.options[:include]) end + scope end - conditions << - left[reflection_primary_key].eq(right[source_primary_key]) + private - right.create_join( - right, - right.create_on(right.create_and(conditions))) - end - - # Construct attributes for :through pointing to owner and associate. - def construct_join_attributes(associate) - # TODO: revisit this to allow it for deletion, supposing dependent option is supported - raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) + # 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 - join_attributes = { - @reflection.source_reflection.foreign_key => - associate.send(@reflection.source_reflection.association_primary_key) - } + def aliased_through_table + name = @reflection.through_reflection.table_name - if @reflection.options[:source_type] - join_attributes.merge!(@reflection.source_reflection.foreign_type => associate.class.base_class.name) + @reflection.table_name == name ? + @reflection.through_reflection.klass.arel_table.alias(name + "_join") : + @reflection.through_reflection.klass.arel_table end - if @reflection.through_reflection.options[:conditions].is_a?(Hash) - join_attributes.merge!(@reflection.through_reflection.options[:conditions]) + def construct_owner_conditions + super(aliased_through_table, @reflection.through_reflection) end - join_attributes - end + def construct_joins + right = aliased_through_table + left = @reflection.klass.arel_table + + conditions = [] + + if @reflection.source_reflection.macro == :belongs_to + reflection_primary_key = @reflection.source_reflection.association_primary_key + source_primary_key = @reflection.source_reflection.foreign_key + + if @reflection.options[:source_type] + column = @reflection.source_reflection.foreign_type + conditions << + right[column].eq(@reflection.options[:source_type]) + end + else + reflection_primary_key = @reflection.source_reflection.foreign_key + source_primary_key = @reflection.source_reflection.active_record_primary_key + + if @reflection.source_reflection.options[:as] + column = "#{@reflection.source_reflection.options[:as]}_type" + conditions << + left[column].eq(@reflection.through_reflection.klass.name) + end + end - def conditions - @conditions = build_conditions unless defined?(@conditions) - @conditions - end + conditions << + left[reflection_primary_key].eq(right[source_primary_key]) - def build_conditions - through_conditions = build_through_conditions - source_conditions = @reflection.source_reflection.options[:conditions] - uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? + right.create_join( + right, + right.create_on(right.create_and(conditions))) + end - if through_conditions || source_conditions || uses_sti - all = [] - all << interpolate_sql(sanitize_sql(source_conditions)) if source_conditions - all << through_conditions if through_conditions - all << build_sti_condition if uses_sti + # Construct attributes for :through pointing to owner and associate. + def construct_join_attributes(associate) + # TODO: revisit this to allow it for deletion, supposing dependent option is supported + raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) - all.map { |sql| "(#{sql})" } * ' AND ' - end - end - - def build_through_conditions - conditions = @reflection.through_reflection.options[:conditions] - if conditions.is_a?(Hash) - interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( - @reflection.quoted_table_name, - @reflection.through_reflection.quoted_table_name) - elsif conditions - interpolate_sql(sanitize_sql(conditions)) + join_attributes = { + @reflection.source_reflection.foreign_key => + associate.send(@reflection.source_reflection.association_primary_key) + } + + if @reflection.options[:source_type] + join_attributes.merge!(@reflection.source_reflection.foreign_type => associate.class.base_class.name) + end + + join_attributes end - end - def build_sti_condition - @reflection.through_reflection.klass.send(:type_condition).to_sql - end + # The reason that we are operating directly on the scope here (rather than passing + # back some arel conditions to be added to the scope) is because scope.where([x, y]) + # has a different meaning to scope.where(x).where(y) - the first version might + # perform some substitution if x is a string. + def add_conditions(scope) + unless @reflection.through_reflection.klass.descends_from_active_record? + scope = scope.where(@reflection.through_reflection.klass.send(:type_condition)) + end - alias_method :sql_conditions, :conditions + scope = scope.where(@reflection.source_reflection.options[:conditions]) + scope.where(through_conditions) + end - def stale_state - if @reflection.through_reflection.macro == :belongs_to - @owner[@reflection.through_reflection.foreign_key].to_s + # If there is a hash of conditions then we make sure the keys are scoped to the + # through table name if left ambiguous. + def through_conditions + conditions = @reflection.through_reflection.options[:conditions] + + if conditions.is_a?(Hash) + Hash[conditions.map { |key, value| + unless value.is_a?(Hash) || key.to_s.include?('.') + key = aliased_through_table.name + '.' + key.to_s + end + + [key, value] + }] + else + conditions + end end - end - def foreign_key_present? - @reflection.through_reflection.macro == :belongs_to && - !@owner[@reflection.through_reflection.foreign_key].nil? - end + def stale_state + if @reflection.through_reflection.macro == :belongs_to + @owner[@reflection.through_reflection.foreign_key].to_s + end + end + + def foreign_key_present? + @reflection.through_reflection.macro == :belongs_to && + !@owner[@reflection.through_reflection.foreign_key].nil? + end end end end |