単純なアソシエーションと第3のモデルを利用したアソシエーション
何をまとめたいか
本投稿は単純なアソシエーションとブックマーク機能やお気に入り機能などを作れるような第3のモデルを利用したアソシエーションについてまとめていきます。
まだまだ知識浅薄であるため間違いがあったらご教示いただけると幸いです。
前提
猫型ロボット(nekogata_robot)は道具(item)を持っています。
また猫型ロボットは道具の保管方法としてポケット(pocket)を使用している時もあります。
それに伴い、モデルはnekogata_robot
・item
・pocket
にて考えていきます。それぞれ
$ rails g model Nekogata_robot name:string
$ rails g model Item nekogata_robot:references name:string
#1
$ rails g model pocket nekogata_robot:references item:references
#2
でモデルを作っておきます#1と#2は後から触れていきますがアソシエーションにおいて非常に重要です。
キホン!
様々な関連付けがありますが、まずはキホン的なものから。
猫型ロボット(nekogata_robot)は道具(item)を持っているためアソシエーションが成り立ちます。
nekogata_robotモデルとitemモデルを使用してアソシエーションを行なっていきます。
まず外部キー制約を考慮してitemモデルを作っていきます。
(既にnekogata_robotモデルは作成されている前提)
$ rails g model Item nekogata_robot:references
を行うと…
class CreateItems < ActiveRecord::Migration[6.1] def change create_table :items do |t| t.references :nekogata_robot, null: false, foreign_key: true t.string :name t.timestamps end end end
というマイグレーションファイルができます。これで$ rails db:migrate
をすると外部キーnekogata_robot_id
がカラムとして追加されます。(もちろんname
も追加されます)
これによってnekogata_robotモデルのid
(主キー)とitemモデルのnekogata_robot_id
(外部キー)によってデータベースにおける関連付けがなされます。
次にアソシエーションの関係をコードに書いていきます。
「一匹の猫型ロボットがたくさんの道具を持っている」
つまりnekogata_robotモデルに対してitemモデルは「1対多」の関係。
models/nekogata_robot.rb
has_many :items
models/item.rb
belongs_to :nekogata_robot, dependent: :destroy
猫型ロボットが壊れてしまったら道具の保有者がいなくなるので、そうした際は保有していた道具ごと削除する必要があります。
そのような場合にdependent: :destroy
は親モデルに連動して削除されるという機能をもたらします。
こうしてとりあえずのアソシエーションが完成したわけですが、実際に使用する際には
nekogata_robot.items.name
(猫型ロボットの持っている道具の名前を探す)
items.nekogata_robot.name
(道具を持っている猫型ロボットの名前を探す)
といった呼び出し方ができます。
以上がアソシエーションの基本です。
第3のモデルを利用したアソシエーション
ただこの猫型ロボットが持っている道具は単純に家に保管しているかもしれないですが、持ち運びをする場合にはポケットに保管していつでも道具を使えるようにしているかもしれないですよね?
また、猫型ロボットは一匹だけではなく、他にもたくさんの種類がいるかもしれないですよね?
すなわち以下の図のような関係をどのようにアソシエーションするかということです。
この「多対多」の考え方についてもアソシエーションによって関連づけをすることができます。
「猫型ロボットはポケットに道具を持っている」
「道具は猫型ロボットのポケットに収納されている」
すなわちnekogata_robotモデルとitemモデルの間には「pocketモデル」があるのでこれをhas_many :through
を使って関連づけをしていきます。
(この章の表題の第3のモデルとはpocketモデルのことだったのです!)
pocketモデルを作成するためには…
$ rails g model Pocket nekogata_robot:references item:references
を行います。
class CreatePockets < ActiveRecord::Migration[6.1] def change create_table :pockets do |t| t.references :nekogata_robot, null: false, foreign_key: true t.references :item, null: false, foreign_key: true t.timestamps end end end
というマイグレーションファイルができるので、例の如く$ rails db:migrate
をします。
するとnekogata_robot_id
カラムとitem_id
カラムが作成されます。
このpocketモデル内のnekogata_robot_id
にnekogata_robotモデルのid(猫型ロボットを特定するid)、
item_idにitemモデルのid
(道具を特定するid)を入れた状態でcreate
などをすることで保存していくと、データベースのpocketモデルにどの猫型ロボットがどの道具をポケットに収納しているのかというデータが保管されます。
同様に第3のモデルを考慮した際のコードも追加していきます。それぞれ
models/nekogata_robot.rb
has_many :pockets, dependent: :destroy has_many :pocket_items_boards, through: :pockets, source: :item
models/item.rb
has_many :pockets, dependent: :destroy has_many :pocket_items_boards, through: :pockets, source: :item
models/pocket.rb
belongs_to :nekogata_robot belongs_to :items
となります。上2つは全く同じコードです。
has_many :pockets, dependent: :destroy
当然アソシエーションを作るためにこの記述は必要となり、猫型ロボットか道具のいずれかが消えてしまえばポケットは存在する意味がないので消すべきです。
ただこの記述だけだと、それぞれがpocketメソッドとアソシエーションを結んでいるが、第3のモデルを使ったアソシエーションは依然使うことができない状態です。
そして次の記述が今回のポイント!
has_many :pocket_items_boards, through: :pockets, source: :item
2つ重要な要素があるのでそれぞれ確認をしていきます。
①through: :pockets
第3のモデルであるpocketsモデルを通じてそれぞれアソシエーションを組む際にはこの記述が必要不可欠です。この記述によって以下のようなコードでポケットを絡めたデータの呼び出しが可能になります。
nekogata_robot.pocket_items.name
(猫型ロボットがポケットに収納している道具の名前を探す)
ただこれだけでは物足りないと思います。安心してください、まだ特殊能力が付与されてます!
それがcollection << (object, …)
というメソッド!
Railsガイドには以下のような説明があります。
collection<<
メソッドは、1つ以上のオブジェクトをコレクションに追加します。このとき、追加されるオブジェクトの外部キーは、呼び出し側モデルの主キーに設定されます。
Railsガイドだけ見てもわかりにくいので実際に使ってみるとわかりやすいです。
models/nekogata_robot.rb
def pocket_create(item) pocket_items << item end
このようなメソッドを作成し、コントローラでnekogata_robot
とitem
をそれぞれ定義してあげた上で…
nekogata_robot.pocket_items(item)
のようなコードを使えば、レシーバのnekogata_robotよりnekogata_robot_id
が、引数のitemよりitem_id
がそれぞれ値が入力されてpocketモデルに新しいデータが作り出されます。これは
pocekt.create(nekogata_robot: params[:nekogata_robot], item: params[:item])
を行っていることと等しいため、コードの省略に貢献するでしょう!
②has_many :pocket_items, ~ source: :items
source
は基本的にアソシエーションを行う際に使います。
具体的にいうとsource
では実際にthrough
で指定したモデルを通じて関連づけをされる小モデルが指定されます。
なのでここではpocketモデルを通じて関連づけをされる子モデルitemモデルが入ります。なお複数形なので注意しましょう。
そして結論なのですが、has_many :pocket_items
ではsource: :items
で指定したitems
の代わりに、実際に使用する際にはpocket_items
を使うようにするという言い換えを定義しています。
なぜこんなことする必要があるのでしょうか?
それはこの言い換えを行わないとitemsが重複するからです。
キホン!でも説明したように、既にnekogata_robotと単純なアソシエーション関係にあるため…
nekogata_robot.items.name
(猫型ロボットの持っている道具の名前を探す)
のような使い方をされていました。
そして今またここでitemsを定義してしまうと、itemsは単純なアソシエーションによるものなのか?はたまた第三者のモデルを使ったアソシエーションによるものなのか?
2つの意味を持ってしまうため訳が分からなくなります。
そういった事態を避けるためにこのsource
を使用しています。なのでthrough
を使ったアソシエーションではsource
は非常に重要な意味を持つのです。
結論
単純なアソシエーションなら
has_many :〇〇
とbelongs_to :〇〇
第3のモデルを使う場合は単純なアソシエーションに加えて
has_many :〇〇, through: :〇〇, source: :〇〇
を書いてあげればOK!
なお今回の事例ではド〇えもんのポケットを例にして記入したため、22世紀まではこの機能を実装することはまずあり得ないと思います。
実際にはブックマーク機能やお気に入り機能を作る際に非常に役に立つっぽいです!
参考
https://qiita.com/imotan/items/036ceffb79e294d8a063
https://qiita.com/tomoharutt/items/e548186c763079327ed1#through
https://railsguides.jp/association_basics.html#has-many%E3%81%A7%E8%BF%BD%E5%8A%A0%E3%81%95%E3%82%8C%E3%82%8B%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89-collection