概要
ActiveRecordでは「has_many, has_one, belongs_to」をモデルに定義することでテーブル間同士の関連性を表すことができます。
この関連性ですが、単純なテーブル同士の関係だけではなく、条件を定義することで任意の関連性を表現出来るので、そのことについてまとめます。
基本
開発チームとチームメンバーをDBで管理する場合を考えてみます。
開発チームには複数のチームメンバーがいるので、開発チームとチームメンバーの関係は1対nになります。
名前をteams
, members
とします。
モデルで定義すると以下のようになります。
class Team < ApplicationRecord
has_many :members
end
class Member < ApplicationRecord
belongs_to :team
end
条件付きの関連性
突然ですが、開発チームはグローバルな多国籍チームでした。チームメンバーの国籍をnationality
というカラムで新しく定義することにします。
この例において、以下のように日本国籍のチームメンバー、アメリカ国籍のチームメンバーなどを開発チームのモデルに関連性として定義することができます。モデルのスコープに近いでしょうか。
class Team < ApplicationRecord
has_many :members
has_many :japanese_members, -> { merge(Members.where(nationality: 'japanese')) }, class_name: 'Member', inverse_of: :team,
end
応用
またまた突然ですが、開発メンバーの中には複数のチームに所属するメンバーがいることが分かりました。
そのため開発チームとチームメンバーの関連性は多対多です。team_members
という中間テーブルを定義してこれを表現することにします。
モデル定義は以下のようになります。
class Team < ApplicationRecord
has_many :team_members
has_many :members, through: team_members
end
class TeamMember < ApplicationRecord
belongs_to :team
belongs_to :member
end
class Member < ApplicationRecord
has_many :team_members
has_many :teams, through: team_members
end
この場合で、先ほどの日本国籍のチームメンバーをモデルに関連性として定義すると以下のようになります。
class Team < ApplicationRecord
has_many :team_members
has_many :members, through: team_members
has_many :japanese_members, -> { merge(Members.where(nationality: 'japanese')) }, through: team_members, source: 'team'
end
おまけ
チームにはリーダーが1人だけいることとします。
リーダーかどうかはis_leader
というbool型のカラムで定義することにします。
teamにhas_oneでリーダーのメンバーを定義しようとすると以下のようになります。
class Team < ApplicationRecord
has_many :team_members
has_many :members, through: team_members
has_one :lead_team_member, -> { merge(TeamMembers.joins(:team).merge(Members.where(is_leader: true))) }, class_name: 'TeamMember', inverse_of: :team
has_one :lead_member, source: 'member', through: lead_team_member
end
has_manyで定義した中間テーブルを通して(throughして)、直接has_oneを定義することはできないので、まずteam_membersに対してhas_oneを定義します。
次に上記で定義したhas_oneの関連を通して(throughして)、最終的にmemberテーブルに対してhas_oneの関連性を持たせることができます。
mergeやwhereがネストしたりして可読性が落ちるので(かつテストも書きづらくなるので)、一つ一つスコープで定義するとよりわかりやすく簡潔に書けるかと思います。