【Ruby on Rails】rubocopの警告「Rails/InverseOf」から学ぶinverse_ofオプション

私は現在、未経験からのエンジニア転職に向けてプログラミングスクールで学習をしている、いしかわと申します。

個人開発中に`rubocop`から`Rails/InverseOf`と警告されたことをきっかけに`Active Record`の`inverse_ofオプション`を用いた`双方向関連付け`について学習した内容をアウトプットとしてこちらの記事にしました。
どなたかの参考になれば幸いです。

:::note warn
プログラミング初学者なので、内容に誤り等ある可能性があります
誤りがありましたら教えてくださると幸いです
:::

環境
ruby 3.2.2
Ruby on Rails 7.0.8

# 双方向関連付けとは
皆さんが良く知る以下のアソシエーション関連付けは`双方向関連付け`と呼ぶことができます
※まずこれすら知らずにアソシエーションだなんだかんだと言っていた私に石を投げてください
```ruby
class User < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :user
end
```
`user`は複数の`books`を持つ１対多の関係です
上記の関連付けにより双方向からデータを参照(または編集)することができます
```ruby
user = User.first
book = user.books.first

# object_idの一致
user.object_id #=> 100
book.user.object_id #=> 100

user.name #=> "hoge"
book.user.name #=> "hoge"

#bookオブジェクトから辿ったuserオブジェクトのnameも変更される
user.name = "ishikawa"
user.name #=> "ishikawa"
book.user.name #=> "ishikawa"
```
関連付けられている`Userオブジェクト`は`Bookオブジェクト`から辿っても`Userオブジェクト`から辿っても**自動的**に`同一オブジェクト`になるようになっています。

## RubocopがRails/InverseOfを吐き出す
個人開発を行なっているとき、関連付けを行うと`Rails/InverseOf`と`rubocop`に警告されました
`Rails/InverseOf`とはなんぞやと調べると
>「アソシエーションを定義する際に明示的に`:inverse_of`を指定すべき箇所で指定されていないことを警告している」

とのこと
調べると以下のように関連付けを行った場合、自動的に双方向関連付けと認識されない**可能性**があるらしい
私は以下のようにアソシエーションを行っていました
```ruby
class Group < ApplicationRecord
  belongs_to :owner, class_name: 'User', foreign_key: 'user_id'
end

class User < ApplicationRecord
  has_many :owned_groups, class_name: 'Group'
end
```
コンソールで確認してみます
```ruby
# groupを適当に特定
irb(main):001> group = Group.first
  Group Load (0.4ms)  SELECT "groups".* FROM "groups" ORDER BY "groups"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> 
#<Group:0x0000000107718218

# アソシエーションしているuserであるgroup.ownerを確認
irb(main):003> group.owner
  User Load (0.8ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, email: "test@example.com", created_at: "2023-12-02 04:51:56.263247000 +0900", updated_at: "2023-12-02 04:51:56.263247000 +0900">

# userとprofileを関連付けしてprofileのnameカラムで名前を管理しています
# group.ownerのnameを確認
irb(main):005> group.owner.profile.name
  Profile Load (0.7ms)  SELECT "profiles".* FROM "profiles" WHERE "profiles"."user_id" = $1 LIMIT $2  [["user_id", 1], ["LIMIT", 1]]
=> "hoge"

# group.ownerのnameを"変更"に変更。確認。
irb(main):006> group.owner.profile.name = "変更"
irb(main):007> group.owner.profile.name
=> "変更"
irb(main):008> group.owner.profile.save
#...なんやかんやあって
=> true

# userのnameを確認するために先ほどのownerのuserを特定
irb(main):009> user = User.first
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<User id: 1, email: "test@example.com", created_at: "2023-12-02 04:51:56.263247000 +0900", updated_at: ...
# 特定したuserのnameを確認
irb(main):010> user.profile.name
  Profile Load (0.4ms)  SELECT "profiles".* FROM "profiles" WHERE "profiles"."user_id" = $1 LIMIT $2  [["user_id", 1], ["LIMIT", 1]]
=> "変更"
```
なんやて工藤。うまくいっとるやないか。
## 双方向関連付けが自動認識されないケース
調べるとigaigaさんが[このような記事](https://zenn.dev/igaiga/books/rails-practice-note/viewer/ar_inverse_of#%E5%8F%8C%E6%96%B9%E5%90%91%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91%E3%81%8C%E8%87%AA%E5%8B%95%E8%AA%8D%E8%AD%98%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E3%82%B1%E3%83%BC%E3%82%B9)を書かれていました。

私は`foregin_keyオプション`をつけていませんが双方向関連付けがうまくいっているように思えます。
しかし[こちらの記事](https://qiita.com/k-mashi/items/d7e1d83538d87952bfe8)のようにうまくいっていない例もあります。
ここでigaigaさんが行っていたように`user1`と`user2`を特定し`object_id`が一致するか確認することにしましました
```ruby
# user1の特定
irb(main):001> user1 = User.first
  User Load (0.6ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<User id: 1, email: "test@example.com", created_at: "2023-12-02 04:51:56.263247000 +0900", updated_at: ...
irb(main):002> user1.owned_groups
  Group Load (0.4ms)  SELECT "groups".* FROM "groups" WHERE "groups"."user_id" = $1  [["user_id", 1]]
=> 
[#<Group:0x000000010b7562a8
  #...複数のgroupが表示される

# user2の特定
irb(main):004> user2 = user1.owned_groups.first.owner
=> #<User id: 1, email: "test@example.com", created_at: "2023-12-02 04:51:56.263247000 +0900", updated_at: ...
irb(main):004> user2.owned_groups
  Group Load (0.4ms)  SELECT "groups".* FROM "groups" WHERE "groups"."user_id" = $1  [["user_id", 1]]
=> 
[#<Group:0x0000000109b7ac00
  #...複数のgroupが表示される

# user1とuser2のobject_idが一致するか確認
irb(main):005> user1.object_id
=> 114560
irb(main):006> user2.object_id
=> 128480
irb(main):007> user1.object_id == user2.object_id
=> false
```
`object_id`を確認すると異なるIDでした。
私は
```ruby
irb(main):006> group.owner.profile.name = "変更"
=> "変更"
irb(main):007> group.owner.profile.name
=> "変更"
irb(main):008> group.owner.profile.save
#...なんやかんやあって
=> true
```
この処理がうまくいったので双方向関連付けが成功したと思っていたのですが、値のみが一致しており同一のオブジェクトではなかったということです。
この内容についてChatGPTに確認してみると
```bash
Railsにおいて、同一のデータベースレコードを参照しているオブジェクトでも
それらが異なる時点でインスタンス化されたり、異なるコンテキストでロードされたりすると
オブジェクトのobject_idは異なるものになります。
これは、オブジェクトが異なるリクエストや異なる操作によってメモリにロードされた結果として発生します。

この挙動は、Railsがデータベースからオブジェクトをロードする際に
既にメモリ上に存在するオブジェクトを再利用しないためです
（特定のキャッシュメカニズムやスコープが使用されていない限り）。
したがって、userとgroup.ownerが異なるobject_idを持つのは
これらが異なるタイミングやコンテキストでインスタンス化された結果であり
それぞれが独立したオブジェクトとして存在しているためです。

結論として、userオブジェクトとgroup.ownerオブジェクトは
メモリ上では異なるインスタンスですが
同じデータベースレコードの値（このケースではprofile.name）を共有している
という理解が正しいです。

双方向関連付け（bi-directional association）の主な目的は
関連するオブジェクト間でのデータの一貫性を保つことにあります。
つまり、あるオブジェクトを変更した際に、その変更が関連オブジェクトにも適切に
反映される場合、双方向関連付けが正しく機能していると言えます。
```
つまり`inverse_ofオプション`をつけなくても「双方向関連付けが失敗している」ということにはならなそうです…難しい…
しかし再度[igaigaさんの記事](https://zenn.dev/igaiga/books/rails-practice-note/viewer/ar_inverse_of#%E5%8F%8C%E6%96%B9%E5%90%91%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91%E3%82%92%E6%89%8B%E5%8B%95%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)を確認するとこのように以下のように書かれていました。
>双方向関連付けがなされていないときには、これに起因するバグや余分なクエリ発行を防ぐために、inverse_ofを書いておいた方が安全です。余分なクエリが発行されているコード例は次のようなコードです。
```ruby
# 双方向関連付け(inverse_of)あり
user = User.first
user.books #クエリ発行
user2 = user.books.first.author #クエリが発行されない(双方向関連付けの効果)
user2.books #クエリが発行されない(前回のuser.booksのキャッシュ利用)

user.object_id  #=> 124660
user2.object_id #=> 124660

# 双方向関連付け(inverse_of)なし
user = User.first
user.books #クエリ発行
user2 = user.books.first.author #クエリ発行(余分)
user2.books #クエリ発行(余分)

user.object_id  #=> 22680
user2.object_id #=> 24120
```

`inverse_ofオプション`をつけることで余計なクエリ発行を防ぐことができるのでやはり`foregin_keyオプション`をつけるときは`inverse_ofオプション`をつけたほうがいいという結果に落ち着きました（初めからちゃんと記事読め)

# inverse_ofオプションを使用した双方向関連付け
よって今回は以下のようにアソシエーションを設定しました
```ruby
class Group < ApplicationRecord
  belongs_to :owner, class_name: 'User', foreign_key: 'user_id', inverse_of: :owned_groups
end

class User < ApplicationRecord
  has_many :owned_groups, class_name: 'Group', inverse_of: :owner
end
```
そしてコンソールでigaigaさんの記事のように`object_id`が一致するか確認しました
```ruby
# user1の特定
irb(main):001> user1 = User.first
  User Load (0.6ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<User id: 1, email: "test@example.com", created_at: "2023-12-02 04:51:56.263247000 +0900", updated_at: ...
irb(main):002> user1.owned_groups
  Group Load (0.7ms)  SELECT "groups".* FROM "groups" WHERE "groups"."user_id" = $1  [["user_id", 1]]
=> 
[#<Group:0x000000010635a8e8
#複数グループが表示される...

# user2の特定
# クエリが発行されていない！！！
irb(main):004> user2 = user1.owned_groups.first.owner
=> #<User id: 1, email: "test@example.com", created_at: "2023-12-02 04:51:56.263247000 +0900", updated_at: ...
irb(main):005> user2.owned_groups
=> 
[#<Group:0x000000010635a8e8
#複数グループが表示される...

# user1とuser2のobject_idの一致確認
irb(main):006> user1.object_id
=> 153520
irb(main):007> user2.object_id
=> 153520
irb(main):008> user1.object_id == user2.object_id
=> true
```
このような結果になりすっきりできました！！！！！

# 結論
`inverse_ofオプション`はRails4.1以降では自動でつけてくれるようになったそうですが、`:conditions, :through, :polymorphic, :foreign_key` などを使用した場合は`inverse_ofオプション`をつけたほうが無駄なクエリの発行を防げるためので、つけたほうが良さそうです。

# 参考記事
https://railsguides.jp/association_basics.html
https://zenn.dev/igaiga/books/rails-practice-note/viewer/ar_inverse_of
https://qiita.com/itp926/items/9cac175d3b35945b8f7e
https://techtechmedia.com/inverse-of-rails/