Ruby on Rails(以降、Rails)では、ActiveRecordのhas_manyやbelongs_toを使って、複数のモデルを関連付けられることはご存知でしょう。
この記事では、モデル間の関連付けをサラッとおさらいし、
・has_manyと一緒に使うinverse_ofオプションって何?
・最新のRailsならinverse_ofオプションって使わないでしょ?
といった疑問や誤った理解をしている方に、inverse_ofオプションを使わなくなったワケと、inverse_ofオプションの使いどころを説明します。
それでは行ってみましょう!
モデル間の関連付けについて
まずは、モデル間の関連付けについて説明しておきましょう。
ActiveRecordに用意されている関連付け機能を利用すると、2つのモデルの間に関係があることを明示できます。
もっとも使われる関連付けが、has_manyとbelongs_toです。
User(ユーザー)とPost(投稿)を、has_manyとbelongs_toで関連付けた場合、以下のコマンドでUser(ユーザー)を削除すると、関係があるPost(投稿)が自動的に削除されるようになります。
user.destroy
本来は、userに関連があるpostを走査して1つずつ削除する必要があるところを、Railsが肩代わりして削除してくれる仕組みになっています。
もちろん、その他にも便利な機能が提供されています。
has_manyとbelongs_toについては、以下の記事で紹介されていますので、ぜひご覧ください。
inverse_ofオプションとは
この記事で扱うinverse_ofオプションは、has_manyとあわせて使うオプションです。
Userにhas_manyを指定すると、1つのuserに複数のpostが関連付けられます。
このとき、inverse_ofオプションを指定すると、postから、userのデータを参照できるようになります。
少し具体的に書いてみましょう。
以下のように設定すると、
user.name = "山田太郎"
以下の方法で「山田太郎」にアクセスできるという仕組みです。
post.user.name
postの情報を表示するときに、投稿者であるuserの名前を簡単に取得できて便利そうですね。
動作を理解するためのWebアプリを作成する
関連付けとinverse_ofオプションの雰囲気がわかったところで、動作を確認するために、Rails 5.1をインストールしてWebアプリを作りましょう。
(1)Railsをインストールします。
私は、以下の記事を参考に、VirtualBoxで作成した仮想パソコンにインストールしたLinux Mintに、Railsの開発環境を作成しました。
基本的には記事の手順に従って操作しますが、app/samurai/sample1ディレクトリを作成する代わりに、app/samurai/inverse_of-demoディレクトリを作成しました。
Railsを起動して、ブラウザで画面が表示されることを確認したら、いったんRailsを終了してから次に進みます。
Linux Mintのインストールについては、以下の記事で詳しく説明しています。
(2)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。
cd app/samurai/inverse_of-demo bin/rails generate scaffold User name:string bin/rails generate scaffold Post user_id:integer title:string month:integer deleted:boolean bin/rails db:migrate
(3)app/models/user.rbを以下のように編集します。
変更前:
class User < ApplicationRecord end
変更後:
class User < ApplicationRecord has_many :posts end
(4)app/models/post.rbを以下のように編集します。
変更前:
class Post < ApplicationRecord end
変更後:
class Post < ApplicationRecord belongs_to :user end
これで、準備完了です。
inverse_ofオプションの動作を確認しよう
では、実際にコードを紹介しつつ、動作を確認していきましょう。
inverse_ofオプションが不必要なケース
実は、Rails 4.1以降では、簡単なモデルではinverse_ofオプションを指定する必要がなくなりました。
has_manyで2つのモデルを関連付けた場合は、inverse_ofオプションを使うことが当然のようになったため、inverse_ofオプションを省略できるようになった、と私は理解しています。
そのため、「最新のRailsならinverse_ofオプションは使わない」という誤解があるのでしょう。
まずは、inverse_ofオプションを使わなくても、post.user.nameで、user.nameにアクセスできることを確認しましょう。
(1)以下のコマンドを入力します。
bin/rails console
(2)以下のコードを1行ずつ順番に入力します。
user = User.new post = user.posts.build user.name = "山田太郎" post.user.name
実行結果:
=> "山田太郎"
確かにuser.nameに設定した値を、post(post.user.name)から参照できていますね。
post.user.nameから参照するときに、データベースにアクセスしていないこともポイントです。
(3)次の説明で混乱しないように、以下のコードを1行ずつ順番に入力してRailsコンソールを終了します。
user.save exit
実は、Railsの古いバージョンで同じ動作をさせるためには、inverse_ofオプションが必要な場合がありました。
詳しくは、以下の記事で紹介されていますので、Railsの古いバージョンをお使いの方は、そちらをご覧ください。
参照:https://qiita.com/itp926/items/9cac175d3b35945b8f7e
inverse_ofオプションが必要なケース
さて、Rails 5.1でもinverse_ofオプションが必要なケースがあります。
たとえば、has_manyに「-> () { where(deleted: false) }」のような条件式を指定した場合が該当します。
まずは、inverse_ofオプションを指定しないで動かしてみましょう。
(1)app/models/user.rbを以下のように編集します。
変更前:
class User < ApplicationRecord has_many :posts end
変更後:
class User < ApplicationRecord has_many :posts, -> () { where(deleted: false) } end
(2)以下のコマンドを入力します。
bin/rails console
先ほどと同じコードを実行してみましょう。
(3)以下のコードを1行ずつ順番に入力します。
user = User.new post = user.posts.build user.name = "山田太郎" post.user.name
実行結果:
Traceback (most recent call last): 1: from (irb):4 NoMethodError (undefined method `name' for nil:NilClass)
post.user.nameではエラーが発生し、”山田太郎”が表示されません。
(4)以下のコードを入力して、Railsコンソールを終了します。
exit
ここで、inverse_ofオプションを指定しましょう。
(5)app/models/user.rbを以下のように編集します。
変更前:
class User < ApplicationRecord has_many :posts, -> () { where(deleted: false) } end
変更後:
class User < ApplicationRecord has_many :posts, -> () { where(deleted: false) }, inverse_of: user end
(6)以下のコマンドを入力します。
bin/rails console
(7)以下のコードを1行ずつ順番に入力します。
先ほどと同じコードです。
user = User.new post = user.posts.build user.name = "山田太郎" post.user.name
実行結果:
=> "山田太郎"
今度は、user.nameに設定した値を、post(post.user.name)から参照できました。
まとめ
今回は、ActiveRecordのinverse_ofオプションの使いどころを説明しました。
Rails 4.1以降は、簡単なモデルであればinverse_ofオプションを使わなくてもpost.user.nameで、nameにアクセスできました。
しかし、条件式を指定した場合など、少し複雑になったときには、旧バージョンと同じようにinverse_ofオプションを使う必要があります。
モデルによってinverse_ofオプションを付けたり付けなかったりすることに疑問を持っていた方は、なるほど!と思っていただけたのではないでしょうか。
今回のように簡単なコードで試しながら、動作を理解していきましょう。
それでは!