ネストした ActiveRecord クラスで内側のクラスと同名のクラスがある場合は class_name が必要

2024-10-27

ネストした内側のクラスと同名クラスの関連がある場合は class_name が必要

Ruby on Rails(rails: 7.3.1, 7.2.1 で確認した)を使って開発をしており、以下のような ActiveRecord のクラスがあるとする。

# db/schema.rb
ActiveRecord::Schema[7.1].define(version: 2024_10_27_143736) do
  create_table "bars", force: :cascade do |t|
    t.integer "foo_id", null: false
    t.string "name"
    t.string "type"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["foo_id"], name: "index_bars_on_foo_id"
  end

  create_table "foos", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  add_foreign_key "bars", "foos"
end
# app/models/foo.rb
class Foo < ApplicationRecord
  has_many :bars, dependent: :destroy
end
class Bar < ApplicationRecord
  belongs_to :foo
end
# app/models/bar/foo.rb
class Bar
  class Foo < ::Bar
  end
end

この場合上記のように、Foo has_many Bars である場合、Foo 側には has_many :bars、Bar 側には belongs_to :foo と記述する。

ただし、STI を使っていて、かつ、Bar の子クラスの内側のクラス名(Bar::Foo の ‘Foo’)と同名のクラスとの関連がある場合(今回は ::Foo)、class_name オプションが必須になる。

まずは class_name オプションをつけていない場合、Bar::Foo インスタンスから Foo を参照しようとすると、自身のインスタンスを返えしてしまう。

 rails c
irb(main):001:0: development  > Bar::Foo.create name: 'bar:foo', foo: ::Foo.first
  Foo Load (0.0ms)  SELECT "foos".* FROM "foos" ORDER BY "foos"."id" ASC LIMIT ?  [["LIMIT", 1]]
  TRANSACTION (0.0ms)  begin transaction
  Bar::Foo Create (0.3ms)  INSERT INTO "bars" ("foo_id", "name", "type", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) RETURNING "id"  [["foo_id", 1], ["name", "bar:foo"], ["type", "Bar::Foo"], ["created_at", "2024-10-27 14:42:36.572358"], ["updated_at", "2024-10-27 14:42:36.572358"]]
  TRANSACTION (0.1ms)  commit transaction
:
#<Bar::Foo:0x0000000105965f90
 id: 1,
 foo_id: 1,
 name: "bar:foo",
 type: "Bar::Foo",
 created_at: Sun, 27 Oct 2024 14:42:36.572358000 UTC +00:00,
 updated_at: Sun, 27 Oct 2024 14:42:36.572358000 UTC +00:00>
irb(main):002:0: development  > bar_foo = Bar::Foo.first
  Bar::Foo Load (0.3ms)  SELECT "bars".* FROM "bars" WHERE "bars"."type" = ? ORDER BY "bars"."id" ASC LIMIT ?  [["type", "Bar::Foo"], ["LIMIT", 1]]
:
#<Bar::Foo:0x0000000105a4d5c0
...
irb(main):003:0: development  > bar_foo.foo
  Bar::Foo Load (0.2ms)  SELECT "bars".* FROM "bars" WHERE "bars"."type" = ? AND "bars"."id" = ? LIMIT ?  [["type", "Bar::Foo"], ["id", 1], ["LIMIT", 1]]
:
#<Bar::Foo:0x0000000105b2d850
 id: 1,
 foo_id: 1,
 name: "bar:foo",
 type: "Bar::Foo",
 created_at: Sun, 27 Oct 2024 14:42:36.572358000 UTC +00:00,
 updated_at: Sun, 27 Oct 2024 14:42:36.572358000 UTC +00:00>

Bar の関連を以下のように書き換える。すると、Bar::Foo インスタンスから foo を参照するとちゃんと ::Foo インスタンスが返るようになる。

diff --git a/app/models/bar.rb b/app/models/bar.rb
index 80704b2..8769eea 100644
--- a/app/models/bar.rb
+++ b/app/models/bar.rb
@@ -1,3 +1,3 @@
 class Bar < ApplicationRecord
-  belongs_to :foo
+  belongs_to :foo, class_name: '::Foo'
 end

変更前は bar_foo.fooBar::Foo インスタンスを返していたが、期待どおりの ::Foo インスタンスを返すようになっている。

 rails c
irb(main):001:0: development  > bar_foo = Bar::Foo.first
  Bar::Foo Load (0.1ms)  SELECT "bars".* FROM "bars" WHERE "bars"."type" = ? ORDER BY "bars"."id" ASC LIMIT ?  [["type", "Bar::Foo"], ["LIMIT", 1]]
:
#<Bar::Foo:0x00000001075f4648
...
irb(main):002:0: development  > bar_foo.foo
  Foo Load (0.1ms)  SELECT "foos".* FROM "foos" WHERE "foos"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
:  #<Foo:0x0000000105bfffd0 id: 1, name: "foo", created_at: Sun, 27 Oct 2024 14:41:15.574606000 UTC +00:00, updated_at: Sun, 27 Oct 2024 14:41:15.574606000 UTC +00:00>