diff options
Diffstat (limited to 'activerecord/lib/active_record/associations')
15 files changed, 382 insertions, 415 deletions
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb new file mode 100644 index 0000000000..634dee2289 --- /dev/null +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -0,0 +1,85 @@ +require 'active_support/core_ext/string/conversions' + +module ActiveRecord + module Associations + # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and + # ActiveRecord::Associations::ThroughAssociationScope + class AliasTracker # :nodoc: + attr_reader :aliases, :table_joins + + # table_joins is an array of arel joins which might conflict with the aliases we assign here + def initialize(table_joins = []) + @aliases = Hash.new + @table_joins = table_joins + end + + def aliased_table_for(table_name, aliased_name = nil) + table_alias = aliased_name_for(table_name, aliased_name) + + if table_alias == table_name + Arel::Table.new(table_name) + else + Arel::Table.new(table_name).alias(table_alias) + end + end + + def aliased_name_for(table_name, aliased_name = nil) + aliased_name ||= table_name + + initialize_count_for(table_name) if aliases[table_name].nil? + + if aliases[table_name].zero? + # If it's zero, we can have our table_name + aliases[table_name] = 1 + table_name + else + # Otherwise, we need to use an alias + aliased_name = connection.table_alias_for(aliased_name) + + initialize_count_for(aliased_name) if aliases[aliased_name].nil? + + # Update the count + aliases[aliased_name] += 1 + + if aliases[aliased_name] > 1 + "#{truncate(aliased_name)}_#{aliases[aliased_name]}" + else + aliased_name + end + end + end + + def pluralize(table_name) + ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name + end + + private + + def initialize_count_for(name) + aliases[name] = 0 + + unless Arel::Table === table_joins + # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase + quoted_name = connection.quote_table_name(name).downcase + + aliases[name] += table_joins.map { |join| + # Table names + table aliases + join.left.downcase.scan( + /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ + ).size + }.sum + end + + aliases[name] + end + + def truncate(name) + name[0..connection.table_alias_length-3] + end + + def connection + ActiveRecord::Base.connection + end + end + end +end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 25b4b9d90d..27c446b12c 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -93,23 +93,9 @@ module ActiveRecord # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which # actually gets built. def construct_scope - @association_scope = association_scope if klass - 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) + if klass + @association_scope = AssociationScope.new(self).scope end - scope = scope.extending(*Array.wrap(options[:extend])) - scope.where(construct_owner_conditions) - end - - def aliased_table - klass.arel_table end # Set the inverse association, if possible @@ -174,42 +160,24 @@ module ActiveRecord end end - def select_value - options[:select] - end - - # Implemented by (some) subclasses def creation_attributes - { } - end - - # Returns a hash linking the owner to the association represented by the reflection - def construct_owner_attributes(reflection = reflection) attributes = {} - if reflection.macro == :belongs_to - attributes[reflection.association_primary_key] = owner[reflection.foreign_key] - else + + if [:has_one, :has_many].include?(reflection.macro) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] if reflection.options[:as] attributes[reflection.type] = owner.class.base_class.name end end - attributes - end - # Builds an array of arel nodes from the owner attributes hash - 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) + attributes end # Sets the owner attributes on the given record def set_owner_attributes(record) if owner.persisted? - construct_owner_attributes.each { |key, value| record[key] = value } + creation_attributes.each { |key, value| record[key] = value } end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb new file mode 100644 index 0000000000..ab102b2b8f --- /dev/null +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -0,0 +1,120 @@ +module ActiveRecord + module Associations + class AssociationScope #:nodoc: + include JoinHelper + + attr_reader :association, :alias_tracker + + delegate :klass, :owner, :reflection, :interpolate, :to => :association + delegate :chain, :conditions, :options, :source_options, :active_record, :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 + + 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(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 == chain.last + scope = scope.where(table[key].eq(owner[foreign_key])) + + 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 = join(foreign_table, constraint) + + scope = scope.joins(join) + + unless conditions[i].empty? + scope = scope.where(sanitize(conditions[i], table)) + end + end + end + + scope + end + + def alias_suffix + reflection.name + 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 + + end + end +end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index e40b32826a..4b48757da7 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -7,24 +7,24 @@ module ActiveRecord::Associations::Builder def build reflection = super check_validity(reflection) - redefine_destroy + define_after_destroy_method reflection end private - def redefine_destroy - # Don't use a before_destroy callback since users' before_destroy - # callbacks will be executed after the association is wiped out. + def define_after_destroy_method name = self.name - model.send(:include, Module.new { - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def destroy # def destroy - super # super - #{name}.clear # posts.clear - end # end - RUBY - }) + model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) + def #{after_destroy_method_name} + association(#{name.to_sym.inspect}).delete_all + end + eoruby + model.after_destroy after_destroy_method_name + end + + def after_destroy_method_name + "has_and_belongs_to_many_after_destroy_for_#{name}" end # TODO: These checks should probably be moved into the Reflection, and we should not be diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index f3761bd2c7..9f4fc44cc6 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -331,11 +331,6 @@ module ActiveRecord @scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) end - def association_scope - options = reflection.options.slice(:order, :limit, :joins, :group, :having, :offset) - super.apply_finder_options(options) - end - def load_target if find_target? targets = [] @@ -373,14 +368,6 @@ module ActiveRecord 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 if options[:counter_sql] interpolate(options[:counter_sql]) diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 028630977d..217213808b 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -26,10 +26,6 @@ module ActiveRecord record end - def association_scope - super.joins(construct_joins) - end - private def count_records @@ -48,24 +44,6 @@ module ActiveRecord 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) false end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index cebf3e477a..78c5c4b870 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -94,8 +94,6 @@ module ActiveRecord end end end - - alias creation_attributes construct_owner_attributes end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index acac68fda5..9d2b29685b 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -34,7 +34,9 @@ module ActiveRecord end def insert_record(record, validate = true) + ensure_not_nested return if record.new_record? && !record.save(:validate => validate) + through_record(record).save! update_counter(1) record @@ -59,6 +61,8 @@ module ActiveRecord end def build_record(attributes) + ensure_not_nested + record = super(attributes) inverse = source_reflection.inverse_of @@ -93,6 +97,8 @@ module ActiveRecord end def delete_records(records, method) + ensure_not_nested + through = owner.association(through_reflection.name) scope = through.scoped.where(construct_join_attributes(*records)) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index e13f97125f..dfcb116392 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -34,19 +34,14 @@ module ActiveRecord when :destroy target.destroy when :nullify - target.update_attribute(reflection.foreign_key, nil) + target.send("#{reflection.foreign_key}=", nil) + target.save(:validations => false) end end end - def association_scope - super.order(options[:order]) - end - private - alias creation_attributes construct_owner_attributes - # 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 # the record is instantiated, and so they are set straight away and do not need to be diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index d76d729303..fdf8ae1453 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -12,6 +12,8 @@ module ActiveRecord private def create_through_record(record) + ensure_not_nested + through_proxy = owner.association(through_reflection.name) through_record = through_proxy.send(:load_target) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index c7c3cf521c..504f25271c 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -5,18 +5,16 @@ module ActiveRecord autoload :JoinBase, 'active_record/associations/join_dependency/join_base' autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association' - attr_reader :join_parts, :reflections, :table_aliases, :active_record + attr_reader :join_parts, :reflections, :alias_tracker, :active_record def initialize(base, associations, joins) - @active_record = base - @table_joins = joins - @join_parts = [JoinBase.new(base)] - @associations = {} - @reflections = [] - @table_aliases = Hash.new do |h,name| - h[name] = count_aliases_from_table_joins(name.downcase) - end - @table_aliases[base.table_name] = 1 + @active_record = base + @table_joins = joins + @join_parts = [JoinBase.new(base)] + @associations = {} + @reflections = [] + @alias_tracker = AliasTracker.new(joins) + @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1 build(associations) end @@ -45,20 +43,6 @@ module ActiveRecord }.flatten end - def count_aliases_from_table_joins(name) - return 0 if Arel::Table === @table_joins - - # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase - quoted_name = active_record.connection.quote_table_name(name).downcase - - @table_joins.map { |join| - # Table names + table aliases - join.left.downcase.scan( - /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ - ).size - }.sum - end - def instantiate(rows) primary_key = join_base.aliased_primary_key parents = {} diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index ebe39c35fe..4121a5b378 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -2,6 +2,8 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinAssociation < JoinPart # :nodoc: + include JoinHelper + # The reflection of the association represented attr_reader :reflection @@ -18,10 +20,15 @@ module ActiveRecord attr_accessor :join_type # These implement abstract methods from the superclass - attr_reader :aliased_prefix, :aliased_table_name + attr_reader :aliased_prefix + + attr_reader :tables - delegate :options, :through_reflection, :source_reflection, :to => :reflection + delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection delegate :table, :table_name, :to => :parent, :prefix => :parent + delegate :alias_tracker, :to => :join_dependency + + alias :alias_suffix :parent_table_name def initialize(reflection, join_dependency, parent = nil) reflection.check_validity! @@ -37,14 +44,7 @@ module ActiveRecord @parent = parent @join_type = Arel::InnerJoin @aliased_prefix = "t#{ join_dependency.join_parts.size }" - - # This must be done eagerly upon initialisation because the alias which is produced - # depends on the state of the join dependency, but we want it to work the same way - # every time. - allocate_aliases - @table = Arel::Table.new( - table_name, :as => aliased_table_name, :engine => arel_engine - ) + @tables = construct_tables.reverse end def ==(other) @@ -60,219 +60,84 @@ module ActiveRecord end def join_to(relation) - send("join_#{reflection.macro}_to", relation) - end - - def join_relation(joining_relation) - self.join_type = Arel::OuterJoin - joining_relation.joins(self) - end - - attr_reader :table - # More semantic name given we are talking about associations - alias_method :target_table, :table - - protected - - def aliased_table_name_for(name, suffix = nil) - aliases = @join_dependency.table_aliases - - if aliases[name] != 0 # We need an alias - connection = active_record.connection - - name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" - aliases[name] += 1 - name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1 - else - aliases[name] += 1 - end - - name - end + tables = @tables.dup + foreign_table = parent_table + + # The chain starts with the target table, but we want to end with it here (makes + # more sense in this context), so we reverse + chain.reverse.each_with_index do |reflection, i| + table = tables.shift + + case reflection.source_macro + when :belongs_to + key = reflection.association_primary_key + foreign_key = reflection.foreign_key + when :has_and_belongs_to_many + # Join the join table first... + relation.from(join( + table, + table[reflection.foreign_key]. + eq(foreign_table[reflection.active_record_primary_key]) + )) + + foreign_table, table = table, tables.shift + + key = reflection.association_primary_key + foreign_key = reflection.association_foreign_key + else + key = reflection.foreign_key + foreign_key = reflection.active_record_primary_key + end - def pluralize(table_name) - ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name - end + constraint = table[key].eq(foreign_table[foreign_key]) - private + if reflection.klass.finder_needs_type_condition? + constraint = table.create_and([ + constraint, + reflection.klass.send(:type_condition, table) + ]) + end - def allocate_aliases - @aliased_table_name = aliased_table_name_for(table_name) + relation.from(join(table, constraint)) - if reflection.macro == :has_and_belongs_to_many - @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") - elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through] - @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join") - end - end + unless conditions[i].empty? + relation.where(sanitize(conditions[i], table)) + end - def process_conditions(conditions, table_name) - if conditions.respond_to?(:to_proc) - conditions = instance_eval(&conditions) + # The current table in this iteration becomes the foreign table in the next + foreign_table = table end - Arel.sql(sanitize_sql(conditions, table_name)) + relation end - def sanitize_sql(condition, table_name) - active_record.send(:sanitize_sql, condition, table_name) + def join_relation(joining_relation) + self.join_type = Arel::OuterJoin + joining_relation.joins(self) end - def join_target_table(relation, condition) - conditions = [condition] - - # If the target table is an STI model then we must be sure to only include records of - # its type and its sub-types. - unless active_record.descends_from_active_record? - sti_column = target_table[active_record.inheritance_column] - subclasses = active_record.descendants - sti_condition = sti_column.eq(active_record.sti_name) - - conditions << subclasses.inject(sti_condition) { |attr,subclass| - attr.or(sti_column.eq(subclass.sti_name)) - } - end - - # If the reflection has conditions, add them - if options[:conditions] - conditions << process_conditions(options[:conditions], aliased_table_name) - end - - ands = relation.create_and(conditions) - - join = relation.create_join( - target_table, - relation.create_on(ands), - join_type) - - relation.from join + def table + tables.last end - def join_has_and_belongs_to_many_to(relation) - join_table = Arel::Table.new( - options[:join_table] - ).alias(@aliased_join_table_name) - - fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key - klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key - - relation = relation.join(join_table, join_type) - relation = relation.on( - join_table[fk]. - eq(parent_table[reflection.active_record.primary_key]) - ) - - join_target_table( - relation, - target_table[reflection.klass.primary_key]. - eq(join_table[klass_fk]) - ) + def aliased_table_name + table.table_alias || table.name end - def join_has_many_to(relation) - if reflection.options[:through] - join_has_many_through_to(relation) - elsif reflection.options[:as] - join_has_many_polymorphic_to(relation) - else - foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key - primary_key = options[:primary_key] || parent.primary_key - - join_target_table( - relation, - target_table[foreign_key]. - eq(parent_table[primary_key]) - ) - end + def conditions + @conditions ||= reflection.conditions.reverse end - alias :join_has_one_to :join_has_many_to - - def join_has_many_through_to(relation) - join_table = Arel::Table.new( - through_reflection.klass.table_name - ).alias @aliased_join_table_name - jt_conditions = [] - first_key = second_key = nil + private - if through_reflection.macro == :belongs_to - jt_primary_key = through_reflection.foreign_key - jt_foreign_key = through_reflection.association_primary_key + def interpolate(conditions) + if conditions.respond_to?(:to_proc) + instance_eval(&conditions) else - jt_primary_key = through_reflection.active_record_primary_key - jt_foreign_key = through_reflection.foreign_key - - if through_reflection.options[:as] # has_many :through against a polymorphic join - jt_conditions << - join_table["#{through_reflection.options[:as]}_type"]. - eq(parent.active_record.base_class.name) - end + conditions end - - case source_reflection.macro - when :has_many - second_key = options[:foreign_key] || primary_key - - if source_reflection.options[:as] - first_key = "#{source_reflection.options[:as]}_id" - else - first_key = through_reflection.klass.base_class.to_s.foreign_key - end - - unless through_reflection.klass.descends_from_active_record? - jt_conditions << - join_table[through_reflection.active_record.inheritance_column]. - eq(through_reflection.klass.sti_name) - end - when :belongs_to - first_key = primary_key - - if reflection.options[:source_type] - second_key = source_reflection.association_foreign_key - - jt_conditions << - join_table[reflection.source_reflection.foreign_type]. - eq(reflection.options[:source_type]) - else - second_key = source_reflection.foreign_key - end - end - - jt_conditions << - parent_table[jt_primary_key]. - eq(join_table[jt_foreign_key]) - - if through_reflection.options[:conditions] - jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name) - end - - relation = relation.join(join_table, join_type).on(*jt_conditions) - - join_target_table( - relation, - target_table[first_key].eq(join_table[second_key]) - ) end - def join_has_many_polymorphic_to(relation) - join_target_table( - relation, - target_table["#{reflection.options[:as]}_id"]. - eq(parent_table[parent.primary_key]).and( - target_table["#{reflection.options[:as]}_type"]. - eq(parent.active_record.base_class.name)) - ) - end - - def join_belongs_to_to(relation) - foreign_key = options[:foreign_key] || reflection.foreign_key - primary_key = options[:primary_key] || reflection.klass.primary_key - - join_target_table( - relation, - target_table[primary_key].eq(parent_table[foreign_key]) - ) - end end end end diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb new file mode 100644 index 0000000000..eae546e76e --- /dev/null +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -0,0 +1,56 @@ +module ActiveRecord + module Associations + # Helper class module which gets mixed into JoinDependency::JoinAssociation and AssociationScope + module JoinHelper #:nodoc: + + def join_type + Arel::InnerJoin + end + + private + + def construct_tables + tables = [] + 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) + reflection.table_name + end + + def table_alias_for(reflection, join = false) + name = alias_tracker.pluralize(reflection.name) + name << "_#{alias_suffix}" + name << "_join" if join + name + end + + def join(table, constraint) + table.create_join(table, table.create_on(constraint), join_type) + end + + def sanitize(conditions, table) + conditions = conditions.map do |condition| + condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || 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 diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index d630fc4c63..ad6374d09a 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -19,8 +19,9 @@ module ActiveRecord source_reflection.name, options ).run - through_records.each do |owner, owner_through_records| - owner_through_records.map! { |r| r.send(source_reflection.name) }.flatten! + through_records.each do |owner, records| + records.map! { |r| r.send(source_reflection.name) }.flatten! + records.compact! end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index e1d60ccb17..e6ab628719 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -3,79 +3,24 @@ module ActiveRecord module Associations module ThroughAssociation #:nodoc: - delegate :source_options, :through_options, :source_reflection, :through_reflection, :to => :reflection + delegate :source_reflection, :through_reflection, :chain, :to => :reflection protected + # We merge in these scopes for two reasons: + # + # 1. To get the default_scope conditions for any of the other reflections in the chain + # 2. To get the type conditions for any STI models in the chain def target_scope - super.merge(through_reflection.klass.scoped) - end - - def association_scope - scope = super.joins(construct_joins) - scope = add_conditions(scope) - unless options[:include] - scope = scope.includes(source_options[:include]) + scope = super + chain[1..-1].each do |reflection| + scope = scope.merge(reflection.klass.scoped) end scope end 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 - - 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 - super(aliased_through_table, through_reflection) - end - - def construct_joins - right = aliased_through_table - left = reflection.klass.arel_table - - conditions = [] - - if source_reflection.macro == :belongs_to - reflection_primary_key = source_reflection.association_primary_key - source_primary_key = source_reflection.foreign_key - - if options[:source_type] - column = source_reflection.foreign_type - conditions << - right[column].eq(options[:source_type]) - end - else - reflection_primary_key = source_reflection.foreign_key - source_primary_key = source_reflection.active_record_primary_key - - if source_options[:as] - column = "#{source_options[:as]}_type" - conditions << - left[column].eq(through_reflection.klass.name) - end - end - - conditions << - left[reflection_primary_key].eq(right[source_primary_key]) - - right.create_join( - right, - right.create_on(right.create_and(conditions))) - end - # Construct attributes for :through pointing to owner and associate. This is used by the # methods which create and delete records on the association. # @@ -112,37 +57,8 @@ module ActiveRecord end 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 through_reflection.klass.descends_from_active_record? - scope = scope.where(through_reflection.klass.send(:type_condition)) - end - - scope = scope.where(interpolate(source_options[:conditions])) - scope.where(through_conditions) - end - - # 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 = interpolate(through_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 - + # Note: this does not capture all cases, for example it would be crazy to try to + # properly support stale-checking for nested associations. def stale_state if through_reflection.macro == :belongs_to owner[through_reflection.foreign_key].to_s @@ -153,6 +69,12 @@ module ActiveRecord through_reflection.macro == :belongs_to && !owner[through_reflection.foreign_key].nil? end + + def ensure_not_nested + if reflection.nested? + raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) + end + end end end end |