はじめに
HappinessChainで学習しているAisakaです。
苦手なアソシエーションの理解を深めるため、それぞれの使い方を纏めてみました。
注意点
この記事では以下の内容については解説していません。
- N+1問題
- アソシエーションのオプション(
dependent
等)
N+1問題ってなに?って方には次の記事が参考になるかと思います。
アソシエーションとはなにか?
アソシエーションとは、Railsにおいてモデル(データベースのテーブル)間の関連付けを定義する機能です。これにより、関連するデータを簡単に扱えるようになります。
例えば、映画レビューシステムでは「映画」と「レビュー」という2つの主要な要素があります。アソシエーションを使うことで、これらの関係を簡単に表現し、操作することができます。
目次
has_many
1. 使用シーン
has_many
はあるモデルが他のモデルを複数持っている関係を表現するときに使います。
- 映画が複数のレビューを持っている
- ユーザーが複数の投稿を持っている
- 著者が複数の本を書いている
2. アソシエーションの設定
関連付けするモデルを複数形で指定します。
class Movie < ApplicationRecord
has_many :reviews
end
3. 使い方
has_many
関連付けにより映画が持っているレビューを全て取得することができます。
# 映画のレビューを全て取得
movie = Movie.first
movie.reviews
特定の映画に属するレビューを作成することができます。
関連付けによりmovie_id
は自動で設定されます。
# 映画のレビューを作成
movie = Movie.first
movie.reviews.create(commnet: 'great!')
4. SQL文
movie.reviews
によって発行されるSQL文です。
?
にはmovie.id
の値が入ります。
SELECT "reviews".* FROM "reviews" WHERE "reviews"."movie_id" = ?
has_many
を使わない場合は次のように書けます。
# 映画のレビューを全て取得
movie = Movie.first
Review.where(movie_id: movie.id)
has_many
関連付けのmovie.reviews
の方がより直感的ですね。
belongs_to
1. 使用シーン
belongs_to
はあるモデルが他のモデルに属している関係を表現するときに使います。
- レビューが映画に属している
- 投稿がユーザーに属している
- 本が著者に属している
2. アソシエーションの設定
関連付けするモデルを単数形で指定します。
class Review < ApplicationRecord
belongs_to :movie
end
3. 使い方
belongs_to
関連付けによりレビューの所有者(映画)を取得することができます。
# レビューの所有者(映画)を取得
review = Review.first
review.movie
4. SQL文
review.movie
によって発行されるSQL文です。
?
にはreview.movie_id
の値が入ります。
SELECT "movies".* FROM "movies" WHERE "movies"."id" = ? LIMIT 1
belongs_to
関連付けを使わない場合は次のように書けます。
# レビューの所有者(映画)を取得
review = Review.first
Movie.find(review.movie_id)
has_many :through
1. 使用シーン
has_many :through
は2つのモデルが中間テーブルを介して多対多の関係にあるときに使います。
- 医者が複数の患者を持ち、患者も複数の医者を持つ(予約テーブルを介して)
- 学生が複数の授業を取り、授業も複数の学生を持つ(履修テーブルを介して)
2. アソシエーションの設定
今までの内容を踏まえると、医者・患者・予約の関係は次のようになるかと思います。
class Doctor < ApplicationRecord
has_many :appointments # 医者は複数の予約を持つ
end
class Appointment < ApplicationRecord
belongs_to :doctor # 予約は1人の医者に属している
belongs_to :patient # 予約は1人の患者に属している
end
class Patient < ApplicationRecord
has_many :appointments # 患者は複数の予約を持つ
end
この関連付けを使って医者の患者リストを取得してみます。
doctor = Doctor.first
Patient.where(id: doctor.appointments.select(:patient_id))
取得できるにはできますが、もっと直感的にdoctor.patients
としたいですよね。
それを叶えてくれるのがhas_many :through
です。
先程のアソシエーションにhas_many :through
を追加しましょう。
class Doctor < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments # 追加
end
class Appointment < ApplicationRecord
belongs_to :doctor
belongs_to :patient
end
class Patient < ApplicationRecord
has_many :appointments
has_many :doctors, through: :appointments # 追加
end
この関連付けにより中間テーブル(Appointment)を介して、レコードを取得することが可能になります。
3. 使い方
has_many :through
関連付けにより中間テーブルを通してお互いのリストを取得できます。
# 医者の患者リストを取得
doctor = Doctor.first
doctor.patients
# 患者の医者リストを取得
patient = Patient.first
patient.doctors
4. SQL文
doctor.patients
によって発行されるSQL文です。
SELECT "patients".*
FROM "patients"
INNER JOIN "appointments" ON "appointments"."patient_id" = "patients"."id"
WHERE "appointments"."doctor_id" = ?
動作は次のようになります。
- patientsテーブルから全てのカラム
(*)
を選択します - 結合条件に従ってappointmentsテーブルと内部結合
(INNER JOIN)
します - WHERE句で特定の医者
(appointments.doctor_id = ?)
に関連する患者のみをフィルタリングします -
?
にはdoctor.idの値が入ります
has_one
1. 使用シーン
has_one
はあるモデルが他のモデルを1つだけ持っている関係(1対1)を表現するときに使います。
- 患者は1つの保険証を持っている
- ユーザーは1つのプロフィールを持っている
2. アソシエーションの設定
患者と保険証の関係を例にアソシエーションを設定していきます。
belongs_to
: 外部キーを持つモデルに記述する (保険証モデル)
has_one
: 外部キーを持たないモデルに記述する (患者モデル)
class Patient < ApplicationRecord
has_one :insurance_card
end
class InsuranceCard < ApplicationRecord
belongs_to :patient
end
3. 使い方
has_one
関連付けにより、所有しているモデルのデータを取得することができます。
# 患者の保険証を取得 (has_one)
patient = Patient.first
patient.insurance_card
# 保険証の所有者を取得 (belongs_to)
insurance_card = InsuranceCard.first
insurance_card.patient
4. SQL文
patient.insurance_card
によって発行されるSQL文です。
?
にはpatient.idの値が入ります。
SELECT "insurance_cards".*
FROM "insurance_cards"
WHERE "insurance_cards"."patient_id" = ?
LIMIT 1
has_one
関連付けを使わない場合は次のように書けます。
patient = Patient.first
InsuranceCard.find_by(patient_id: patient.id)
自己結合
1. 使用シーン
自己結合の多対多関連は、同じモデル内で複数の関連を持つ場合に使います。
- SNSのフォロー・フォロワーの関係
- ECサイトの商品と関連商品の関係
2. アソシエーションの設定
SNSのフォロー・フォロワーの関係は、ユーザーが複数のユーザーをフォローでき、同時に複数のユーザーからフォローされる関係にあります。
これは多対多の関係なので has_many :through の時のように中間テーブル(Relationshipテーブル)を用意します。
少し大げさかもしれませんが、今までの内容を踏まえると次のようになります。
class User < ApplicationRecord
has_many :relationships # 自分がフォローしている関係(=自分が発信元)
has_many :relationships # 自分をフォローしている関係(=自分が受信先)
has_many :users, through: :relationships # 自分がフォローしているユーザーのリスト
has_many :users, through: :relationships # 自分をフォローしているユーザーのリスト
end
class Relationship < ApplicationRecord
belongs_to :user # フォローしている人(フォロワー)への参照
belongs_to :user # フォローされている人(フォロイー)への参照
end
このアソシエーションは勿論動作しません。
理由はフォローする人とフォローされる人を区別できていないからです。
同じモデル内で複数の関連を持つ場合には、それぞれを区別できるように名前を付けます。
class User < ApplicationRecord
has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id"
has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id"
has_many :following, through: :active_relationships, source: :followed
has_many :followers, through: :passive_relationships, source: :follower
end
class Relationship < ApplicationRecord
belongs_to :follower, class_name: "User" # フォローする人
belongs_to :followed, class_name: "User" # フォローされる人
end
ポイントは次の4つです。
- 関連に役割を表す明確な名前をつけ、同じテーブルの異なる参照を区別する
-
foreign_key
オプションで、中間テーブルのどのカラムを起点にするか指定する -
class_name
オプションで、関連先のクラスを指定し、Railsに参照すべきモデルを教える -
source
オプションで、has_many :through
関連において中間テーブルを通じてどの関連を参照するかを指定する
3. 使い方
has_many :through
によって簡単にユーザーリストを取得できます。
# フォローしているユーザーリストを取得
user = User.first
user.following
# フォローされているユーザーリストを取得
user = User.first
user.followers
# ユーザーフォローする
user1 = User.first
user2 = User.second
user1.active_relationships.create(followed: user2)
4. SQL文
user.following
で発行されるSQL文です。
SELECT "users".*
FROM "users"
INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id"
WHERE "relationships"."follower_id" = ?
動作は次のようになります。
- usersテーブルから全てのカラム
(*)
を選択します - 結合条件に従ってrelationshipsテーブルと内部結合
(INNER JOIN)
します - WHERE句で
relationships.follower_id
がuser1.idと一致するユーザーのみをフィルタリングします -
?
にはuser.idの値が入ります
ポリモーフィック
1. 使用シーン
ポリモーフィック関連は、1つのモデルが複数の異なるモデルに属する場合に使用します。
- 属性とキャラクター・武器・場所の関係
- 通知といいね・コメント、DMの関係
2. アソシエーションの設定
ゲーム内のキャラクターや武器が複数の属性を保有している関係をポリモーフィック関連で表してみます。
属性のマイグレーションファイルは次のように想定しています。
class CreateElements < ActiveRecord::Migration[7.0]
def change
create_table :elements do |t|
t.string :name, null: false
t.references :elementable, polymorphic: true, null: false
t.timestamps
end
end
end
マイグレーションファイルの:elementable
から実際に作成されるカラムは、elementable_type
とelementable_id
になります。
create_table "elements", force: :cascade do |t|
t.string :name # fire, water, windなどが入る
t.string :elementable_type
t.integer :elementable_id
t.timestamps
end
Element
モデルは関連付けられたレコードがどのモデルに属しているかを次のステップで特定します。
-
elementable_type
カラムを使用して、関連するモデルを特定します - 特定したモデルから
elementable_id
とidが一致するレコードを特定します
アソシエーションは次のように設定します。
class Element < ApplicationRecord
belongs_to :elementable, polymorphic: true
end
class Character < ApplicationRecord
has_many :elements, as: :elementable
end
class Weapon < ApplicationRecord
has_many :elements, as: :elementable
end
class Location < ApplicationRecord
has_many :elements, as: :elementable
end
as:
には関連付けられるオブジェクトの情報を保存するカラムを指定します。
Railsはas:
で指定された名前を次のように推測します。
- 型を保存するカラム:
指定した名前
+_type
- IDを保存するカラム:
指定した名前
+_id
これによってElementsテーブルには次のようなデータが保存されます。
id | name | elementable_type | elementable_id |
---|---|---|---|
1 | fire | Character | 2 |
2 | water | Character | 2 |
3 | wind | Location | 1 |
例えばCharacter
テーブルのid:2(elementable_id
)の属性は炎・水であることが分かりますね。
今回は1つのゲームオブジェクトが複数の属性を保有するケースなので、has_many
を使用していますが、1つの属性を持たせたい場合はhas_one
を使用します。
class Element < ApplicationRecord
belongs_to :elementable, polymorphic: true
end
class Character < ApplicationRecord
has_one :element, as: :elementable
end
class Weapon < ApplicationRecord
has_one :element, as: :elementable
end
class Location < ApplicationRecord
has_one :element, as: :elementable
end
3. 使い方
オブジェクトの作成や取得にポリモーフィックを意識する必要はありません。
has_many
やhas_one
と同じ要領でデータを取得できます。
# キャラクターの属性を取得する
character = Character.first
character.elements
# キャラクターの属性を作成する
character = Character.first
character.elements.create(name: 'fire')
炎属性のキャラクターリストを取得する場合などは、テーブルを結合して取得します。
Character.joins(:elements).where(elements: { name: 'fire' })
4. 注意点
ポリモーフィック関連では複数テーブルのレコードを保存するため、レコード数がその分増加します。それによってクエリのパフォーマンスが低下する可能性があるので、ポリモーフィックの採用は慎重に検討した方が良さそうです。
さいごに
この他にもhas_one :through
やhas_and_belongs_to_many
といったアソシエーションが存在します。気になる方はRailsガイドで調べてみてください!
参考