diff options
Diffstat (limited to 'activerecord')
8 files changed, 58 insertions, 77 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index d4f4041910..f780029a18 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,10 @@ +* Previously, when building records using a `has_many :through` association, + if the child records were deleted before the parent was saved, they would + still be persisted. Now, if child records are deleted before the parent is saved + on a `has_many :through` association, the child records will not be persisted. + + *Tobias Kraze* + * Merging two relations representing nested joins no longer transforms the joins of the merged relation into LEFT OUTER JOIN. Example to clarify: 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 53ffb3b68d..2fd20b4368 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -109,6 +109,11 @@ module ActiveRecord record end + def remove_records(existing_records, records, method) + super + delete_through_records(records) + end + def target_reflection_has_associated_record? !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?) end 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 95f16c622e..005410d598 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -34,26 +34,10 @@ module ActiveRecord table = tables.shift klass = reflection.klass - join_keys = reflection.join_keys - key = join_keys.key - foreign_key = join_keys.foreign_key + join_scope = reflection.join_scope(table, foreign_table, foreign_klass) - constraint = build_constraint(klass, table, key, foreign_table, foreign_key) - - rel = reflection.join_scope(table) - - if rel && !rel.arel.constraints.empty? - binds += rel.bound_attributes - constraint = constraint.and rel.arel.constraints - end - - if reflection.type - value = foreign_klass.base_class.name - column = klass.columns_hash[reflection.type.to_s] - - binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name)) - constraint = constraint.and klass.arel_attribute(reflection.type, table).eq(Arel::Nodes::BindParam.new) - end + binds.concat join_scope.bound_attributes + constraint = join_scope.arel.constraints joins << table.create_join(table, table.create_on(constraint), join_type) @@ -64,34 +48,6 @@ module ActiveRecord JoinInformation.new joins, binds end - # Builds equality condition. - # - # Example: - # - # class Physician < ActiveRecord::Base - # has_many :appointments - # end - # - # If I execute `Physician.joins(:appointments).to_a` then - # klass # => Physician - # table # => #<Arel::Table @name="appointments" ...> - # key # => physician_id - # foreign_table # => #<Arel::Table @name="physicians" ...> - # foreign_key # => id - # - def build_constraint(klass, table, key, foreign_table, foreign_key) - constraint = table[key].eq(foreign_table[foreign_key]) - - if klass.finder_needs_type_condition? - constraint = table.create_and([ - constraint, - klass.send(:type_condition, table) - ]) - end - - constraint - end - def table tables.first end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 9f77f38b35..208d1b2670 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -147,7 +147,7 @@ module ActiveRecord def preloaders_for_one(association, records, scope) grouped_records(association, records).flat_map do |reflection, klasses| klasses.map do |rhs_klass, rs| - loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope) + loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope) loader.run self loader end @@ -159,6 +159,7 @@ module ActiveRecord records.each do |record| next unless record assoc = record.association(association) + next unless assoc.klass klasses = h[assoc.reflection] ||= {} (klasses[assoc.klass] ||= []) << record end @@ -180,20 +181,11 @@ module ActiveRecord end end - class NullPreloader # :nodoc: - def self.new(klass, owners, reflection, preload_scope); self; end - def self.run(preloader); end - def self.preloaded_records; []; end - def self.owners; []; end - end - # Returns a class containing the logic needed to load preload the data # and attach it to a relation. For example +Preloader::Association+ or # +Preloader::HasManyThrough+. The class returned implements a `run` method # that accepts a preloader. - def preloader_for(reflection, owners, rhs_klass) - return NullPreloader unless rhs_klass - + def preloader_for(reflection, owners) if owners.first.association(reflection.name).loaded? return AlreadyLoaded end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index c6aa2ed720..3d3ec862a3 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -171,7 +171,7 @@ module ActiveRecord JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: def join_keys - get_join_keys klass + @join_keys ||= get_join_keys(klass) end # Returns a list of scopes that should be applied for this Reflection @@ -185,12 +185,25 @@ module ActiveRecord end deprecate :scope_chain - def join_scope(table) + def join_scope(table, foreign_table, foreign_klass) predicate_builder = predicate_builder(table) scope_chain_items = join_scopes(table, predicate_builder) klass_scope = klass_join_scope(table, predicate_builder) - scope_chain_items.inject(klass_scope || scope_chain_items.shift, &:merge!) + key = join_keys.key + foreign_key = join_keys.foreign_key + + klass_scope.where!(table[key].eq(foreign_table[foreign_key])) + + if klass.finder_needs_type_condition? + klass_scope.where!(klass.send(:type_condition, table)) + end + + if type + klass_scope.where!(type => foreign_klass.base_class.name) + end + + scope_chain_items.inject(klass_scope, &:merge!) end def join_scopes(table, predicate_builder) # :nodoc: @@ -207,8 +220,7 @@ module ActiveRecord scope.joins_values = scope.left_outer_joins_values = [].freeze } else - relation = build_scope(table, predicate_builder) - klass.send(:build_default_scope, relation) + klass.default_scoped(build_scope(table, predicate_builder)) end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 1d661fa8ed..5ec2dfcfc9 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -147,8 +147,7 @@ module ActiveRecord def last(limit = nil) return find_last(limit) if loaded? || limit_value - result = limit(limit) - result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key + result = ordered_relation.limit(limit) result = result.reverse_order! limit ? result.reverse : result.first @@ -535,11 +534,7 @@ module ActiveRecord if loaded? records[index, limit] || [] else - relation = if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) - else - self - end + relation = ordered_relation if limit_value.nil? || index < limit_value relation = relation.offset(offset_index + index) unless index.zero? @@ -554,11 +549,7 @@ module ActiveRecord if loaded? records[-index] else - relation = if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) - else - self - end + relation = ordered_relation relation.to_a[-index] # TODO: can be made more performant on large result sets by @@ -572,5 +563,13 @@ module ActiveRecord def find_last(limit) limit ? records.last(limit) : records.last end + + def ordered_relation + if order_values.empty? && primary_key + order(arel_attribute(primary_key).asc) + else + self + end + end end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index a61fdd6454..388f471bf5 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -29,8 +29,7 @@ module ActiveRecord end end - def default_scoped # :nodoc: - scope = relation + def default_scoped(scope = relation) # :nodoc: build_default_scope(scope) || scope end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 9156f6d57a..1c2138a3d0 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -319,6 +319,17 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_includes post.single_people, person end + def test_build_then_remove_then_save + post = posts(:thinking) + post.people.build(first_name: "Bob") + ted = post.people.build(first_name: "Ted") + post.people.delete(ted) + post.save! + post.reload + + assert_equal ["Bob"], post.people.collect(&:first_name) + end + def test_both_parent_ids_set_when_saving_new post = Post.new(title: "Hello", body: "world") person = Person.new(first_name: "Sean") |