2
3

[Rails] モデルの関連付け纏め(アソシエーション)

Posted at

はじめに

HappinessChainで学習しているAisakaです。
苦手なアソシエーションの理解を深めるため、それぞれの使い方を纏めてみました。

注意点

この記事では以下の内容については解説していません。

  • N+1問題
  • アソシエーションのオプション(dependent等)

N+1問題ってなに?って方には次の記事が参考になるかと思います。

アソシエーションとはなにか?

アソシエーションとは、Railsにおいてモデル(データベースのテーブル)間の関連付けを定義する機能です。これにより、関連するデータを簡単に扱えるようになります。

例えば、映画レビューシステムでは「映画」と「レビュー」という2つの主要な要素があります。アソシエーションを使うことで、これらの関係を簡単に表現し、操作することができます。

目次

  1. has_many
  2. belongs_to
  3. has_many :through
  4. has_one
  5. 自己結合
  6. ポリモーフィック

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" = ?

動作は次のようになります。

  1. patientsテーブルから全てのカラム(*)を選択します
  2. 結合条件に従ってappointmentsテーブルと内部結合(INNER JOIN)します
  3. WHERE句で特定の医者(appointments.doctor_id = ?)に関連する患者のみをフィルタリングします
  4. ?には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つです。

  1. 関連に役割を表す明確な名前をつけ、同じテーブルの異なる参照を区別する
  2. foreign_key オプションで、中間テーブルのどのカラムを起点にするか指定する
  3. class_name オプションで、関連先のクラスを指定し、Railsに参照すべきモデルを教える
  4. 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" = ?

動作は次のようになります。

  1. usersテーブルから全てのカラム(*)を選択します
  2. 結合条件に従ってrelationshipsテーブルと内部結合(INNER JOIN)します
  3. WHERE句でrelationships.follower_idがuser1.idと一致するユーザーのみをフィルタリングします
  4. ?には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_typeelementable_idになります。

db/schema.rb
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モデルは関連付けられたレコードがどのモデルに属しているかを次のステップで特定します。

  1. elementable_typeカラムを使用して、関連するモデルを特定します
  2. 特定したモデルから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_manyhas_oneと同じ要領でデータを取得できます。

# キャラクターの属性を取得する
character = Character.first
character.elements

# キャラクターの属性を作成する
character = Character.first
character.elements.create(name: 'fire')

炎属性のキャラクターリストを取得する場合などは、テーブルを結合して取得します。

Character.joins(:elements).where(elements: { name: 'fire' })

4. 注意点

ポリモーフィック関連では複数テーブルのレコードを保存するため、レコード数がその分増加します。それによってクエリのパフォーマンスが低下する可能性があるので、ポリモーフィックの採用は慎重に検討した方が良さそうです。

さいごに

この他にもhas_one :throughhas_and_belongs_to_manyといったアソシエーションが存在します。気になる方はRailsガイドで調べてみてください!

参考

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3