はじめに
HappinessChain で日々勉強中のAkiraです!
本記事は HappinessChain Advent Calendar 2023 14日目の記事になります!
Railsを学習していると自身のアソシエーションに関する理解が浅いために複雑な設定をしようとすると手が止まるということがありました。
本記事では何故極端に浅い理解になってしまったのか、どのように咀嚼することである程度理解できるようになったのかを書いていこうと思います。
何故理解が浅くなってしまったのか?
これは各種教材等でインプットする際に has_many
や belongs_to
の記述を "呪文" や "おまじない" のように捉えてしまっていたからだと思います。
故に複雑な設定をしなければいけない場合、オプションが何を指して何をしようとしているのか、という思考ができず手が止まったというわけです。
例を挙げると has_many :books
←このようなシンプルな記述でも books:
とは何を指しているのかという所まで理解することで、より汎用的 & 自分のアイデアで活用できるようになるものと考えました。
has_many
、 has_one
の基礎
それでは実際に has_many
や has_one
のシンプルな記述が何をしたくてそうなっているのかについて書いていきます。
先ほども例に挙げたシンプルなこのコード has_many :books
この引数で渡している books:
これが何を意味しているのか文章にすると、「Bookモデルは私の情報(レコード)に対して複数の情報(レコード)が紐づいている」ということになります。
そしてこれを設定すると次のように紐づく情報を参照できるわけですね model.books
ですがここで頭を固くしてほしくないのが後に説明するオプション等を使用すると model.sold_books
(売れた本たち) のように参照に使用する名前(関連名)を自由に設定できるということです。
よって、ここで設定している books:
とは命名規則等に従っていればrailsが良しなに設定してくれることを利用したシンプルな記述というわけです。
少し複雑なアソシエーション
ここまではアソシエーションを学ぼうとすると最初に通る基礎中の基礎で非常にシンプルなアソシエーション設定だったと思います。
それでは本題です。次からは少々複雑なアソシエーション設定を考えてみます。
下記のようなER図を想定します。
この図は従業員(employee)は上司と部下の関係(hierarchicals)を持っているということになります。
それではこの図を元にアソシエーションを設定してみましょう。
従来のシンプルな書き方だと以下のようになりそうです。
class Employee < ApplicationRecord
has_many :hierarchicals
end
class Hierarchical < ApplicationRecord
belongs_to :employee
end
ですがこれでirbを叩き情報を取ろうとすると "employee_id" が存在しないという旨のエラーが出ます。
ここでER図のhierarchicalsテーブルの外部キーを確認すると
manager_employee_id と assistant_employee_id となっていることが確認できます。
このことから、上記の書き方のように設定するとRailsは hierarchicalsテーブルの employee_id
を自動で探しに行くということが分かります。
ここまでの情報を元にすると上記エラーは明示的に外部キーを指定することで解決しそうです。
そのために一旦次のオプションを使ってみます。
class Employee < ApplicationRecord
has_many :hierarchicals, foreign_key: :manager_employee_id
end
こうして再度irbを叩いてみると無事に情報を取れるということが確認できます。
foreign_keyオプション
追加したオプションは foreign_key
というオプションです。
このオプションは名の通り、"外部キーを明示的に指定する" というオプションになります。
今回はこのオプションを使い、外部キーを明示的に指定してあげたことで「employeeとhierarchicalがmanager_employee_idで結びつく」という結果になったわけです。
実際に今回のアソシエーションを元にインスタンスを生成するとmanager_employee_idにemployee.idが設定されることが確認できると思います。
class_name オプション
これで従業員の "部下" に対するアソシエーションは設定できましたね。
それでは "上司" に対するアソシエーションも設定していきたい...と思うのですがここで問題がでてきます。
下記のコードは先述のことも踏まえ、上司に対するアソシエーション設定を追記したものです。
class Employee < ApplicationRecord
# 部下に対するアソシエーション
has_many :hierarchicals, foreign_key: :manager_employee_id
# 上司に対するアソシエーション
has_many :hierarchicals, foreign_key: :assistant_employee_id
end
気づきましたでしょうか。そう、関連名が同じなのです。
この状態では関連名を呼び出す際、部下なんだか上司なんだかわからないという状況になります。
これを解決するのが class_name
オプションです。class_name
を適用したコードは下記になります。
class Employee < ApplicationRecord
# 部下に対するアソシエーション
has_many :assistants, foreign_key: :manager_employee_id, class_name: 'Hierarchical'
# 上司に対するアソシエーション
has_many :managers, foreign_key: :assistant_employee_id, class_name: 'Hierarchical'
end
これで上司と部下の関連名がそれぞれ assistants と managers に変更され class_name: 'Hierarchical'
という記述が追加されました。
これは何をしているのかというと変更前までは :hierarchicals
とすることでRailsが関連名を追えていたのですが関連名を変更したことによりRailsは関連先を追うことができなくなります。その際に自身で関連先を明示的に指定するのが class_name
オプションと言うわけです。
この設定で 部下に対するアソシエーションを 従業員A.assistants
で、
上司に対するアソシエーションを 従業員B.managers
で取得できるようになりました。
throughオプション
ここまでで部下と上司に対するアソシエーションは設定できました。
しかし実際に帰ってくるデータとしては関係性を表したレコードのみです。本来なら「従業員Aの部下一覧」のようにしたいはず。
ということで through
オプションを使い実現していきます。
throughオプションは中間テーブルを "通して" 関連テーブルの情報を取得するといった際に使います。
※下記画像のようなイメージ。
それでは実際にどう書くのかコードをみていきます。
class Employee < ApplicationRecord
# 部下に対するアソシエーション
has_many :assistants, foreign_key: :manager_employee_id, class_name: 'Hierarchical'
# 上記中間テーブル(部下)を通して関連先の情報を取得
has_many :assistant_employees, through: :assistants, source: :assistant_employee
# 上司に対するアソシエーション
has_many :managers, foreign_key: :assistant_employee_id, class_name: 'Hierarchical'
# 上記中間テーブル(上司)を通して関連先の情報を取得
has_many :manager_employees, through: :managers, source: :manager_employee
end
同時に今回はHierarchicalも記述します。
class Hierarchical < ApplicationRecord
belongs_to :assistant_employee, class_name: 'Employee'
belongs_to :manager_employee, class_name: 'Employee'
end
これで設定は以上です。
詳しく見ていきましょう。
まず、各関連名は assistant or manager_employees(部下or上司_従業員)
という形になっています。
そして肝心の through: assistants or managers
の部分ですが、先ほど設定した関連名を指定しています。このことから、先ほど設定した記述が中間テーブルの役割を果たしているということが理解できると思います。
そして新しいオプション source
が登場していますが、これは class_name
のように関連名が参照と一致しないため明示的に指定するためのオプションです。
今回部下の方では、source: :assistant_employee
、上司の方では、source: :manager_employee
と設定していますね。
これで部下の情報を取得すると assistant_employee_id
を見に行くといった形になるのです。
また、belongs_to
で使用している class_name
オプションも先ほど解説した内容で理解できると思います。
おわりに
以上がアソシエーションで設定で使用する基本的なオプションです。
自分が理解するのに苦戦した内容をなるべくわかりやすく解説したつもりですが間違っている点などあればご指摘頂ければ幸いです。