💭

トップレベルのモデル名と、STIなネストした側のモデル名が一致すると class_name オプションが必須

2024/11/26に公開

rails 7.3.1, 7.2.1, 8.0.0 で起きることを確認している。

Bar::Foo.foo すると自身のインスタンスが返ってしまう

以下の関係のあるモデルがあるとする。

ActiveRecord::Schema[8.0].define(version: 2024_11_25_145426) do
  create_table "bars", force: :cascade do |t|
    t.integer "foo_id", null: false
    t.string "name", null: false
    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", null: false
    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

Bar::Foo インスタンスが belongs_to の関連を使って .foo にアクセスすると、トップレベルの Foo ではなく、Bar という名前空間内の Foo にアクセスしようとした結果、自身にアクセスしてしまう。
特にエラーにはならないので、値を確認しないと見落とす可能性がある。

irb(main):001:0: > 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: > 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>

解決策

belongs_toclass_name オプションをつけて ::Foo を指定すると解決する。
先にも書いたが、特にエラーにはならず、値を確認しないと気が付かないため注意する

 class Bar < ApplicationRecord
-  belongs_to :foo
+  belongs_to :foo, class_name: '::Foo'
 end

rails console で関連を確認すると、トップレベルの Foo が返っていることが確認できる。

irb(main):001:0: > 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: > 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>
あしたのチーム Tech Blog

Discussion