🤝

Active Recordのアソシエーションにおける :inverse_of の役割

2022/01/24に公開1

:inverse_ofオプションの役割がよくわからなかったので調べてみた。

Railsガイドを読むと、:inverse_ofオプションを指定することで、双方向関連付けを明示的に宣言できることがわかる。

通常の関連付けは、双方向で設定します。2つのモデルの両方に関連を定義する必要があります。

という前提の上、例にあるAuthorモデルとBookモデルを単純に一対多で関連付けているような場合は、2つのモデルが双方向の関連を共有していることをActive Recordが自動的に認識してくれるらしい。
ただし、:throughオプションで中間テーブルを利用する場合や、:foreign_keyオプションで外部キーの名前を指定する場合など、双方関連付けが自動認識されないケースがあるとのこと。
そこで、Active Recordが双方向の関連付けを認識できるようにするには、:inverse_ofオプションを使う。

:inverse_ofの指定によって挙動が変わる例

APIドキュメントの説明では、多対多の関係において、:inverse_ofオプションの設定によりsaveメソッドの挙動が異なる場合について書かれている。
ということで、実際に以下の関連付けを試してみた。

class Post < ApplicationRecord
  has_many :taggings
  has_many :tags, through: :taggings
end

class Tagging < ApplicationRecord
  belongs_to :post
  belongs_to :tag, inverse_of: :taggings
end

class Tag < ApplicationRecord
  has_many :taggings
  has_many :posts, through: :taggings
end

The last line ought to save the through record (a Tagging). This will only work if the :inverse_of is set

APIドキュメントに書かれている通り、belongs_toの側で逆方向の関連付け(tagから見たtaggingsへの関連)を明示することで、tagの新規登録時に、taggingsへの登録も同時に行われるようになる。

rails console
Running via Spring preloader in process 443
Loading development environment (Rails 6.1.3.1)
irb(main):001:0> post = Post.first
   (1.5ms)  SELECT sqlite_version(*)
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT ?  [["LIMIT", 1]]
=>
#<Post:0x00007f97195ec998
...
irb(main):002:0> tag = post.tags.build name: 'ruby'
=> #<Tag:0x00007f971093a158 id: nil, name: "ruby", created_at: nil, updated_at: nil>
irb(main):003:0> tag.save
  TRANSACTION (0.1ms)  begin transaction
  Tag Create (0.6ms)  INSERT INTO "tags" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "ruby"], ["created_at", "2022-01-24 08:53:36.394773"], ["updated_at", "2022-01-24 08:53:36.394773"]]
  TRANSACTION (1.8ms)  commit transaction
=> true

一方、:inverse_ofオプションを指定しなければ、逆方向の関連は認識されない。
そのため、postに対してtagを新規で登録する際に、taggingsへの登録は行われない。

class Tagging < ApplicationRecord
  belongs_to :post
  belongs_to :tag
end
rails console
Running via Spring preloader in process 957
Loading development environment (Rails 6.1.3.1)
irb(main):001:0> post = Post.first
   (1.8ms)  SELECT sqlite_version(*)
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT ?  [["LIMIT", 1]]
=>
#<Post:0x00007f97195455f8
...
irb(main):002:0> tag = post.tags.build name: 'ruby'
=> #<Tag:0x00007f97196b6518 id: nil, name: "ruby", created_at: nil, updated_at: nil>
irb(main):003:0> tag.save
  TRANSACTION (0.1ms)  begin transaction
  Tag Create (0.5ms)  INSERT INTO "tags" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "ruby"], ["created_at", "2022-01-24 09:00:56.115941"], ["updated_at", "2022-01-24 09:00:56.115941"]]
  TRANSACTION (1.4ms)  commit transaction
=> true

taggingsへの登録は別で行いたい場合など、敢えて無効にすることもあるのかもしれない。

参考リンク

https://railsguides.jp/association_basics.html#双方向関連付け
https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to
https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsinverseof

GitHubで編集を提案

Discussion

Junichi ItoJunichi Ito

@pofkuma さん、こんにちは。

tagの新規登録時に、taggingsへの登録も同時に行われるようになる。

とありますが、その下の実行結果にはtag.saveしたあとにINSERT INTO "taggings"が表示されていないので、説明と矛盾しているように見えます。もしかして貼り付けるログを間違えたりしていないでしょうか?

ご確認よろしくお願いします🙏