From 68013b30d2f383177128b356c98ad449dcaf43f7 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Wed, 6 Dec 2023 11:20:31 -0500 Subject: [PATCH] Infer `:inverse_of` for `has_many ..., through:` Closes [#49574][] Issue #49574 outlines how an Active Record join model accessed through a `has_many ..., through:` association is unable to infer an appropriate `:inverse_of` association by pluralizing a predictably pluralizable class name. This commit resolves that issue by also checking a model's reflections for a pluralized inverse name in addition to whatever's provided through the `:as` option or inferred in the singular. The issue shares a code sample: ```ruby ActiveRecord::Schema.define do create_table "listings", force: :cascade do |t| t.bigint "list_id", null: false t.bigint "pin_id", null: false end create_table "lists", force: :cascade do |t| end create_table "pins", force: :cascade do |t| end end class Pin < ActiveRecord::Base has_many :listings end class List < ActiveRecord::Base has_many :listings has_many :pins, through: :listings end class Listing < ActiveRecord::Base belongs_to :list belongs_to :pin end class BugTest < Minitest::Test def test_association_stuff list = List.create! pin = list.pins.new pin.save! assert_equal [pin], list.reload.pins assert_equal 1, list.reload.pins.count end end ``` Unfortunately, there isn't a one-to-one mapping in the test suite's `test/model` directory for this type of structure. The most similar associations were between the `Book`, `Subscription`, and `Subscriber`. For the sake of ease, this commit wraps the test block in a new `skipping_validations_for` helper method to ignore validations declared on the `Subscription` join table model. [#49574]: https://github.com/rails/rails/issues/49574 --- activerecord/lib/active_record/reflection.rb | 5 +++-- ...has_one_through_disable_joins_associations_test.rb | 1 + .../cases/associations/inverse_associations_test.rb | 11 +++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0bfa38a2f6f..4f7c8396c58 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -708,9 +708,10 @@ module ActiveRecord def automatic_inverse_of if can_find_inverse_of_automatically?(self) inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym + plural_inverse_name = ActiveSupport::Inflector.pluralize(inverse_name) begin - reflection = klass._reflect_on_association(inverse_name) + reflection = klass._reflect_on_association(inverse_name) || klass._reflect_on_association(plural_inverse_name) rescue NameError => error raise unless error.name.to_s == class_name @@ -720,7 +721,7 @@ module ActiveRecord end if valid_inverse_reflection?(reflection) - inverse_name + reflection.name end end end diff --git a/activerecord/test/cases/associations/has_one_through_disable_joins_associations_test.rb b/activerecord/test/cases/associations/has_one_through_disable_joins_associations_test.rb index b997e3d6bde..5aeea253fab 100644 --- a/activerecord/test/cases/associations/has_one_through_disable_joins_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_disable_joins_associations_test.rb @@ -8,6 +8,7 @@ require "models/project" require "models/developer" require "models/company" require "models/computer" +require "models/contract" require "models/club" require "models/membership" diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 4e0f96bd6e7..fbe11bb7247 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -24,6 +24,7 @@ require "models/user" require "models/room" require "models/contract" require "models/subscription" +require "models/subscriber" require "models/book" require "models/branch" require "models/cpk" @@ -199,6 +200,16 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association" end + def test_belongs_to_should_find_inverse_has_many_automatically + book = Book.create! + subscriber = book.subscribers.new nick: "Nickname" + + subscriber.save! + + assert_equal [subscriber], book.reload.subscribers + assert_equal 1, book.reload.subscribers.count + end + def test_polymorphic_and_has_many_through_relationships_should_not_have_inverses sponsor_reflection = Sponsor.reflect_on_association(:sponsorable)