diff options
Diffstat (limited to 'activerecord/lib/active_record/associations')
7 files changed, 433 insertions, 252 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..6fc2bfdb31 --- /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 # TODO: Is this conditional necessary? + 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/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_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..890e77fca9 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -18,10 +18,13 @@ module ActiveRecord attr_accessor :join_type # These implement abstract methods from the superclass - attr_reader :aliased_prefix, :aliased_table_name + attr_reader :aliased_prefix - delegate :options, :through_reflection, :source_reflection, :to => :reflection + attr_reader :tables + + delegate :options, :through_reflection, :source_reflection, :through_reflection_chain, :to => :reflection delegate :table, :table_name, :to => :parent, :prefix => :parent + delegate :alias_tracker, :to => :join_dependency def initialize(reflection, join_dependency, parent = nil) reflection.check_validity! @@ -38,13 +41,7 @@ module ActiveRecord @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 - ) + setup_tables end def ==(other) @@ -60,7 +57,95 @@ module ActiveRecord end def join_to(relation) - send("join_#{reflection.macro}_to", relation) + # The chain starts with the target table, but we want to end with it here (makes + # more sense in this context) + chain = through_reflection_chain.reverse + + foreign_table = parent_table + index = 0 + + chain.each do |reflection| + table = tables[index] + conditions = [] + + if reflection.source_reflection.nil? + case reflection.macro + when :belongs_to + key = reflection.association_primary_key + foreign_key = reflection.foreign_key + when :has_many, :has_one + key = reflection.foreign_key + foreign_key = reflection.active_record_primary_key + + conditions << polymorphic_conditions(reflection, table) + when :has_and_belongs_to_many + # For habtm, we need to deal with the join table at the same time as the + # target table (because unlike a :through association, there is no reflection + # to represent the join table) + table, join_table = table + + join_key = reflection.foreign_key + join_foreign_key = reflection.active_record.primary_key + + relation = relation.join(join_table, join_type).on( + join_table[join_key]. + eq(foreign_table[join_foreign_key]) + ) + + # We've done the first join now, so update the foreign_table for the second + foreign_table = join_table + + key = reflection.klass.primary_key + foreign_key = reflection.association_foreign_key + end + else + case reflection.source_reflection.macro + when :belongs_to + key = reflection.association_primary_key + foreign_key = reflection.foreign_key + + conditions << source_type_conditions(reflection, foreign_table) + when :has_many, :has_one + key = reflection.foreign_key + foreign_key = reflection.source_reflection.active_record_primary_key + when :has_and_belongs_to_many + table, join_table = table + + join_key = reflection.foreign_key + join_foreign_key = reflection.klass.primary_key + + relation = relation.join(join_table, join_type).on( + join_table[join_key]. + eq(foreign_table[join_foreign_key]) + ) + + foreign_table = join_table + + key = reflection.klass.primary_key + foreign_key = reflection.association_foreign_key + end + end + + conditions << table[key].eq(foreign_table[foreign_key]) + + conditions << reflection_conditions(index, table) + conditions << sti_conditions(reflection, table) + + ands = relation.create_and(conditions.flatten.compact) + + join = relation.create_join( + table, + relation.create_on(ands), + join_type) + + relation = relation.from(join) + + # The current table in this iteration becomes the foreign table in the next + foreign_table = table + index += 1 + end + + relation end def join_relation(joining_relation) @@ -68,42 +153,59 @@ module ActiveRecord 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 + def table + if tables.last.is_a?(Array) + tables.last.first else - aliases[name] += 1 + tables.last end + end - name + def aliased_table_name + table.table_alias || table.name end - def pluralize(table_name) - ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name + protected + + def table_alias_for(reflection, join = false) + name = alias_tracker.pluralize(reflection.name) + name << "_#{parent_table_name}" + name << "_join" if join + name end private - def allocate_aliases - @aliased_table_name = aliased_table_name_for(table_name) + # Generate aliases and Arel::Table instances for each of the tables which we will + # later generate joins for. We must do this in advance in order to correctly allocate + # the proper alias. + def setup_tables + @tables = through_reflection_chain.map do |reflection| + table = alias_tracker.aliased_table_for( + reflection.table_name, + table_alias_for(reflection, reflection != self.reflection) + ) + + # For habtm, we have two Arel::Table instances related to a single reflection, so + # we just store them as a pair in the array. + if reflection.macro == :has_and_belongs_to_many || + (reflection.source_reflection && reflection.source_reflection.macro == :has_and_belongs_to_many) + + join_table = alias_tracker.aliased_table_for( + (reflection.source_reflection || reflection).options[:join_table], + table_alias_for(reflection, true) + ) - 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") + [table, join_table] + else + table + end end + + # The joins are generated from the through_reflection_chain in reverse order, so + # reverse the tables too (but it's important to generate the aliases in the 'forward' + # order, which is why we only do the reversal now. + @tables.reverse! end def process_conditions(conditions, table_name) @@ -118,161 +220,39 @@ module ActiveRecord active_record.send(:sanitize_sql, condition, table_name) end - def join_target_table(relation, condition) - conditions = [condition] + def reflection_conditions(index, table) + reflection.through_conditions.reverse[index].map do |condition| + process_conditions(condition, table.table_alias || table.name) + end + end - # 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) + def sti_conditions(reflection, table) + unless reflection.klass.descends_from_active_record? + sti_column = table[reflection.klass.inheritance_column] + sti_condition = sti_column.eq(reflection.klass.sti_name) + subclasses = reflection.klass.descendants - conditions << subclasses.inject(sti_condition) { |attr,subclass| + # TODO: use IN (...), or possibly AR::Base#type_condition + 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 - 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]) - ) 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]) - ) + def source_type_conditions(reflection, foreign_table) + if reflection.options[:source_type] + foreign_table[reflection.source_reflection.foreign_type]. + eq(reflection.options[:source_type]) end 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 - - if through_reflection.macro == :belongs_to - jt_primary_key = through_reflection.foreign_key - jt_foreign_key = through_reflection.association_primary_key - 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 - 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) + def polymorphic_conditions(reflection, table) + if reflection.options[:as] + table[reflection.type]. + eq(reflection.active_record.base_class.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/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..ed24373cba 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -1,9 +1,12 @@ +require 'enumerator' + module ActiveRecord # = Active Record Through Association module Associations module ThroughAssociation #:nodoc: - delegate :source_options, :through_options, :source_reflection, :through_reflection, :to => :reflection + delegate :source_options, :through_options, :source_reflection, :through_reflection, + :through_reflection_chain, :through_conditions, :to => :reflection protected @@ -13,10 +16,12 @@ module ActiveRecord def association_scope scope = super.joins(construct_joins) - scope = add_conditions(scope) + scope = scope.where(reflection_conditions(0)) + unless options[:include] scope = scope.includes(source_options[:include]) end + scope end @@ -30,6 +35,7 @@ module ActiveRecord { } end + # TODO: Needed? def aliased_through_table name = through_reflection.table_name @@ -39,41 +45,101 @@ module ActiveRecord end def construct_owner_conditions - super(aliased_through_table, through_reflection) + reflection = through_reflection_chain.last + + if reflection.macro == :has_and_belongs_to_many + table = tables[reflection].first + else + table = Array.wrap(tables[reflection]).first + end + + super(table, reflection) end def construct_joins - right = aliased_through_table - left = reflection.klass.arel_table + joins, right_index = [], 1 - conditions = [] + # Iterate over each pair in the through reflection chain, joining them together + through_reflection_chain.each_cons(2) do |left, right| + left_table, right_table = tables[left], tables[right] - if source_reflection.macro == :belongs_to - reflection_primary_key = source_reflection.association_primary_key - source_primary_key = source_reflection.foreign_key + if left.source_reflection.nil? + case left.macro + when :belongs_to + joins << inner_join( + right_table, + left_table[left.association_primary_key], + right_table[left.foreign_key], + reflection_conditions(right_index) + ) + when :has_many, :has_one + joins << inner_join( + right_table, + left_table[left.foreign_key], + right_table[right.association_primary_key], + polymorphic_conditions(left, left), + reflection_conditions(right_index) + ) + when :has_and_belongs_to_many + joins << inner_join( + right_table, + left_table.first[left.foreign_key], + right_table[right.klass.primary_key], + reflection_conditions(right_index) + ) + end + else + case left.source_reflection.macro + when :belongs_to + joins << inner_join( + right_table, + left_table[left.association_primary_key], + right_table[left.foreign_key], + source_type_conditions(left), + reflection_conditions(right_index) + ) + when :has_many, :has_one + if right.macro == :has_and_belongs_to_many + join_table, right_table = tables[right] + end - 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 + joins << inner_join( + right_table, + left_table[left.foreign_key], + right_table[left.source_reflection.active_record_primary_key], + polymorphic_conditions(left, left.source_reflection), + reflection_conditions(right_index) + ) - if source_options[:as] - column = "#{source_options[:as]}_type" - conditions << - left[column].eq(through_reflection.klass.name) + if right.macro == :has_and_belongs_to_many + joins << inner_join( + join_table, + right_table[right.klass.primary_key], + join_table[right.association_foreign_key] + ) + end + when :has_and_belongs_to_many + join_table, left_table = tables[left] + + joins << inner_join( + join_table, + left_table[left.klass.primary_key], + join_table[left.association_foreign_key] + ) + + joins << inner_join( + right_table, + join_table[left.foreign_key], + right_table[right.klass.primary_key], + reflection_conditions(right_index) + ) + end end - end - conditions << - left[reflection_primary_key].eq(right[source_primary_key]) + right_index += 1 + end - right.create_join( - right, - right.create_on(right.create_and(conditions))) + joins end # Construct attributes for :through pointing to owner and associate. This is used by the @@ -112,37 +178,88 @@ 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)) + def alias_tracker + @alias_tracker ||= AliasTracker.new + end + + # TODO: It is decidedly icky to have an array for habtm entries, and no array for others + def tables + @tables ||= begin + Hash[ + through_reflection_chain.map do |reflection| + table = 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) + + join_table = alias_tracker.aliased_table_for( + (reflection.source_reflection || reflection).options[:join_table], + table_alias_for(reflection, true) + ) + + [reflection, [join_table, table]] + else + [reflection, table] + end + end + ] end + end - scope = scope.where(interpolate(source_options[:conditions])) - scope.where(through_conditions) + def table_alias_for(reflection, join = false) + name = alias_tracker.pluralize(reflection.name) + name << "_#{self.reflection.name}" + name << "_join" if join + name 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]) + def inner_join(table, left_column, right_column, *conditions) + conditions << left_column.eq(right_column) - 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 + table.create_join( + table, + table.create_on(table.create_and(conditions.flatten.compact))) + end - [key, value] - }] - else - conditions + def reflection_conditions(index) + reflection = through_reflection_chain[index] + conditions = through_conditions[index].dup + + # TODO: maybe this should go in Reflection#through_conditions directly? + unless reflection.klass.descends_from_active_record? + conditions << reflection.klass.send(:type_condition) + end + + unless conditions.empty? + 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 + + Arel::Nodes::And.new(conditions) end end + def polymorphic_conditions(reflection, polymorphic_reflection) + if polymorphic_reflection.options[:as] + tables[reflection][polymorphic_reflection.type]. + eq(polymorphic_reflection.active_record.base_class.name) + end + end + + def source_type_conditions(reflection) + if reflection.options[:source_type] + tables[reflection.through_reflection][reflection.foreign_type]. + eq(reflection.options[:source_type]) + end + end + + # TODO: Think about this in the context of nested associations def stale_state if through_reflection.macro == :belongs_to owner[through_reflection.foreign_key].to_s @@ -153,6 +270,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 |