RuboCopの Rails/InverseOf に引っかかった


Rails/InverseOfは何の警告か?

アソシエーションを定義する際に明示的に :inverse_of を指定すべき箇所で指定されていないことを警告している。

:inverse_ofとは何者か?

Railsガイドより。
ActiveRecordで、双方向の関連付けを明示的に宣言するもの。

class User
  has_many :posts
end

class Post
  belongs_to :user
end

inverse_ofが効いていない場合、下例のようになる。

user = User.first
post = user.posts.first
user.name == post.user.name #=> true
user.name = "hogehoge"
user.name == post.user.name #=> false

別々のオブジェクトとして扱われるため、postをたどってuserのnameを変更できない。

inverse_ofが効いている場合、双方向に関連付けが行われて同じオブジェクトになる。

今まで書いた記憶がない。。。

Rails4.1以降、Railsが自動でつけてくれるようになった。
それ以前は都度SQLを発行してインスタンスを生成していた。
そのため、以下のようなことが起こり得た。

・ 同じレコードを参照しているはずなのに、データを取得するためにSQLが発行される
・ 更新したレコードの内容が、アソシエーション経由だと反映されない

しかし、自動で付けてくれるならなぜ警告されたんだ。

:inverse_ofを明示的に指定すべきパターンとは?

foreign_keyやscopeが指定されている場合など、inverse_ofを明示的に指定しないとこのオプションが効かないパターンがある。

foreign_key

次のように、foreign_keyを指定して関連付けされている場合、inverse_ofが自動で適用されないため明示する必要がある。

model
class User < ApplicationRecord
  has_many :posts, foreign_key: :id
end

class Post < ApplicationRecord
  belongs_to :user
end
user = User.first
post = user.posts.first
# user.name と post.user.name は同じオブジェクトを見ていて欲しい

user.name = "変更" # user.nameを変更
user.name == post.user.name #=> false

scope

次のように、scopeを含む場合は自動でinverse_ofが適用されないため、明示する必要がある。

model
class User < ApplicationRecord
  has_many :posts, -> () { order(:created_at) }
end

class Post < ApplicationRecord
  belongs_to :user
end
user = User.first
post = user.posts.first
# user.name と post.user.name は同じオブジェクトを見ていて欲しい

user.name = "変更" # user.nameを変更
user.name == post.user.name #=> false

どう書くか?

has_many - belongs_toの場合は、
belongs_toで紐付けされているモデルを設定すれば良い。

model
class User < ApplicationRecord
  has_many :posts, -> () { order(:created_at) }, inverse_of: :user
end

class Post < ApplicationRecord
  belongs_to :user
end