🔗

Railsで自己結合(同一テーブル)アソシエーションを実装する

2023/12/31に公開

こんにちは。
株式会社スペースマーケットでエンジニアをやっているtchmrです。

2023年も残るところ僅かとなってきました。
今年は公私ともにいろいろなチャレンジをしてとても刺激的な一年でしたが、来年も変化に富んだ楽しい日々を過ごしていきたいと思います。

本記事の内容

AテーブルとBテーブルでアソシエーションを定義するのがオーソドックスですが、本記事では同一テーブル内でアソシエーションを定義する方法を見ていきたいと思います。

実現したいこと

以下の要件を実現するための一環としてレンタルスペースのホストに親子の概念を持たせたいという要件があるとします。

  • 子ホストは自身のスペースのみを管理することができる。
  • 親ホストは自身に加えて子ホストのスペースを管理することができる。

実現方法

結論だけ知りたいという方はここだけご覧ください。

owners

id name
1 親ホスト
2 子ホストA
3 子ホストB

owner_relations

id parent_owner_id child_owner_id
1 1 2
2 1 3

アソシエーション実装

# app/model/owner.rb
class Owner < ApplicationRecord
  has_many :child_owner_relations, class_name: 'OwnerRelation', foreign_key: 'parent_owner_id'
  has_one :parent_owner_relation, class_name: 'OwnerRelation', foreign_key: 'child_owner_id'
  has_many :child_owners, through: :child_owner_relations, source: :child_owner
  has_one :parent_owner, through: :parent_owner_relation, source: :parent_owner
end
Owner.find(1).child_owners.ids
=> [2,3]

Owner.find(2).parent_owner.id
=> 1

解説・考察

アソシエーションの各オプションの意味について(ざっくり)

  • class_name: アソシエーション名が関連モデル名と異なる場合に検索するモデル(テーブル)を明示的に指定する
  • source: 多対多の関連先のモデルを指定する
  • foreign_key: アソシエーション名が関連モデル名と異なる場合に外部キーを明示的に指定する
  • through: 中間テーブルのモデルを経由する際に指定する(結合が実行される)

https://railsguides.jp/association_basics.html#has-oneのオプション-source

発行されるSQL

Owner.find(1).child_owners
# SELECT `owners`.* FROM `owners` INNER JOIN `owner_relations` 
# ON `owners`.`id` = `owner_relations`.`child_owner_id` 
# WHERE `owner_relations`.`parent_owner_id` = 1

Owner.find(2).parent_owner
# SELECT `owners`.* FROM `owners` INNER JOIN `owner_relations` 
# ON `owners`.`id` = `owner_relations`.`parent_owner_id` 
# WHERE `owner_relations`.`child_owner_id` = 2 LIMIT 1

外部キーや結合キーなどをオプションで指定できていることが見て取れますね。

ちなみに、よく定義するhas_many :spacesのようなシンプルなアソシエーションはアソシエーション名が関連モデル名(Railsのルール通り)なのでclass_nameforeign_keyが省略されています。

# app/model/owner.rb
class Owner < ApplicationRecord
  has_many :spaces
  # 以下と同義
  # has_many :spaces, class_name: 'Space', foreign_key: 'owner_id'
end

まとめ

同一モデル内でアソシエーションを設定する方法について見てきました。
いろいろなオプションが出てきてそこそこ複雑になりやすいので、実装時は発行されるクエリを確認しながらアソシエーションを定義していくとやりやすいかなと思います。

スペースマーケット Engineer Blog

Discussion