diff options
Diffstat (limited to 'activerecord')
7 files changed, 109 insertions, 19 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 33cbafc6aa..d68fb33962 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1560,8 +1560,86 @@ module ActiveRecord # has_and_belongs_to_many :categories, join_table: "prods_cats" # has_and_belongs_to_many :categories, -> { readonly } def has_and_belongs_to_many(name, scope = nil, options = {}, &extension) - reflection = Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension) - Reflection.add_reflection self, name, reflection + if scope.is_a?(Hash) + options = scope + scope = nil + end + + has_and_belongs_to_many1(name, scope, options, &extension) + end + + def has_and_belongs_to_many1(name, scope = nil, options = {}, &extension) + # temporarily + habtm_builder = Builder::HasAndBelongsToMany.create_builder(self, name, scope, options, &extension) + habtm = habtm_builder.build self + + join_class_name = "HABTM_#{name.to_s.camelize}" + this_class = self + rhs_name = name.to_s.singularize.to_sym + + join_model = Class.new(ActiveRecord::Base) { + define_singleton_method(:name) { join_class_name } + define_singleton_method(:table_name) { habtm.join_table } + define_singleton_method(:compute_type) { |class_name| + this_class.compute_type class_name + } + belongs_to :left_side, :class => this_class + + rhs_options = {} + + if options.key? :class_name + rhs_options[:foreign_key] = options[:class_name].foreign_key + rhs_options[:class_name] = options[:class_name] + end + + if options.key? :association_foreign_key + rhs_options[:foreign_key] = options[:association_foreign_key] + end + + belongs_to rhs_name, rhs_options + } + + middle_name = [self.name.downcase.pluralize, name].join('_').gsub(/::/, '_').to_sym + + #middle_options = options.dup + middle_options = {} + middle_options[:class] = join_model + middle_options[:source] = :left_side + if options.key? :foreign_key + middle_options[:foreign_key] = options[:foreign_key] + end + + hm_builder = Builder::HasMany.create_builder(self, + middle_name, + nil, + middle_options) + middle_reflection = hm_builder.build self + hm_builder.define_callbacks self, middle_reflection + + Reflection.add_reflection self, middle_name, middle_reflection + + include Module.new { + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def destroy_associations + association(:#{middle_name}).delete_all(:delete_all) + association(:#{name}).reset + super + end + RUBY + } + + hm_options = {} + hm_options[:through] = middle_name + hm_options[:source] = rhs_name + + [:before_add, :after_add, :before_remove, :after_remove].each do |k| + if options.key? k + hm_options[k] = options[k] + end + end + + #has_many name, scope, :through => middle_name, &extension + has_many name, scope, hm_options, &extension end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 1059fc032d..6de0331851 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -37,6 +37,17 @@ module ActiveRecord::Associations::Builder reflection end + def self.create_builder(model, name, scope, options, &block) + raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol) + + if scope.is_a?(Hash) + options = scope + scope = nil + end + + new(name, scope, options, &block) + end + def initialize(name, scope, options) @name = name @scope = scope diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 8d3d6962fc..874ae77ff7 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -747,6 +747,8 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_default_scope_as_block + # warm up the habtm cache + EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first.projects developer = EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first projects = Project.order(:id).to_a assert_no_queries do @@ -1136,6 +1138,10 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_deep_including_through_habtm + # warm up habtm cache + posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a + posts[0].categories[0].categorizations.length + posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length } assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length } diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index f77066e6ab..be928ec8ee 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -613,7 +613,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase 3, Developer.references(:developers_projects_join).merge( :includes => {:projects => :developers}, - :where => 'developers_projects_join.joined_on IS NOT NULL' + :where => 'projects_developers_projects_join.joined_on IS NOT NULL' ).to_a.size ) end @@ -632,7 +632,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal( 3, Developer.references(:developers_projects_join).merge( - :includes => {:projects => :developers}, :where => 'developers_projects_join.joined_on IS NOT NULL', + :includes => {:projects => :developers}, :where => 'projects_developers_projects_join.joined_on IS NOT NULL', :group => group.join(",") ).to_a.size ) @@ -646,8 +646,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_find_scoped_grouped - assert_equal 5, categories(:general).posts_grouped_by_title.size - assert_equal 1, categories(:technology).posts_grouped_by_title.size + assert_equal 5, categories(:general).posts_grouped_by_title.to_a.size + assert_equal 1, categories(:technology).posts_grouped_by_title.to_a.size end def test_find_scoped_grouped_having @@ -718,12 +718,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal project, developer.projects.first end - def test_self_referential_habtm_without_foreign_key_set_should_raise_exception - assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) { - SelfMember.new.friends - } - end - def test_dynamic_find_should_respect_association_include # SQL error in sort clause if :include is not included # due to Unknown column 'authors.id' diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index cf3c07845c..95f49a37eb 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -214,7 +214,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload - authors = assert_queries(3) { Author.includes(:post_categories).to_a.sort_by(&:id) } + authors = assert_queries(4) { Author.includes(:post_categories).to_a.sort_by(&:id) } general, cooking = categories(:general), categories(:cooking) assert_no_queries do @@ -242,7 +242,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload - categories = assert_queries(3) { Category.includes(:post_comments).to_a.sort_by(&:id) } + categories = assert_queries(4) { Category.includes(:post_comments).to_a.sort_by(&:id) } greetings, more = comments(:greetings), comments(:more_greetings) assert_no_queries do @@ -270,7 +270,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload - authors = assert_queries(5) { Author.includes(:category_post_comments).to_a.sort_by(&:id) } + authors = assert_queries(6) { Author.includes(:category_post_comments).to_a.sort_by(&:id) } greetings, more = comments(:greetings), comments(:more_greetings) assert_no_queries do diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 635278abc1..517d2674a7 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -1440,10 +1440,6 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas test "should generate validation methods for HABTM associations with :validate => true" do assert_respond_to @pirate, :validate_associated_records_for_parrots end - - test "should not generate validation methods for HABTM associations without :validate => true" do - assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrots) - end end class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 9e5ffa0cce..f814947ab2 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -42,6 +42,11 @@ class RelationTest < ActiveRecord::TestCase end def test_two_scopes_with_includes_should_not_drop_any_include + # heat habtm cache + car = Car.incl_engines.incl_tyres.first + car.tyres.length + car.engines.length + car = Car.incl_engines.incl_tyres.first assert_no_queries { car.tyres.length } assert_no_queries { car.engines.length } |