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

## はじめに

[HappinessChain](https://happiness-chain.com/)で学習しているAisakaです｡
苦手なアソシエーションの理解を深めるため､それぞれの使い方を纏めてみました｡

## 注意点
この記事では以下の内容については解説していません｡
- N+1問題
- アソシエーションのオプション(`dependent`等)

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

https://qiita.com/massaaaaan/items/4eb770f20e636f7a1361

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

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

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

## 目次
1. [has_many](#has_many)
2. [belongs_to](#belongs_to)
3. [has_many :through](#has_many-through)
4. [has_one](#has_one)
5. [自己結合](#自己結合)
6. [ポリモーフィック](#ポリモーフィック)

## has_many

### 1. 使用シーン

`has_many`はあるモデルが他のモデルを複数持っている関係を表現するときに使います。

- 映画が複数のレビューを持っている
- ユーザーが複数の投稿を持っている
- 著者が複数の本を書いている

### 2. アソシエーションの設定
関連付けするモデルを複数形で指定します｡
```ruby
class Movie < ApplicationRecord
  has_many :reviews
end
```

### 3. 使い方
`has_many`関連付けにより映画が持っているレビューを全て取得することができます｡
```ruby
# 映画のレビューを全て取得
movie = Movie.first
movie.reviews
```
特定の映画に属するレビューを作成することができます｡
関連付けにより`movie_id`は自動で設定されます｡
```ruby
# 映画のレビューを作成
movie = Movie.first
movie.reviews.create(commnet: 'great!')
```


### 4. SQL文
`movie.reviews`によって発行されるSQL文です｡
`?`には`movie.id`の値が入ります｡
```SQL
SELECT "reviews".* FROM "reviews" WHERE "reviews"."movie_id" = ?
```

`has_many` を使わない場合は次のように書けます｡
```ruby
# 映画のレビューを全て取得
movie = Movie.first
Review.where(movie_id: movie.id)
```

`has_many`関連付けの`movie.reviews`の方がより直感的ですね｡

<a name="belongs_to"></a>
## belongs_to

### 1. 使用シーン

`belongs_to`はあるモデルが他のモデルに属している関係を表現するときに使います。

- レビューが映画に属している
- 投稿がユーザーに属している
- 本が著者に属している

### 2. アソシエーションの設定
関連付けするモデルを単数形で指定します｡
```ruby
class Review < ApplicationRecord
  belongs_to :movie
end
```

### 3. 使い方
`belongs_to`関連付けによりレビューの所有者（映画）を取得することができます｡
```ruby
# レビューの所有者（映画）を取得
review = Review.first
review.movie
```

### 4. SQL文
`review.movie`によって発行されるSQL文です｡
`?`には`review.movie_id`の値が入ります｡
```SQL
SELECT "movies".* FROM "movies" WHERE "movies"."id" = ? LIMIT 1
```

`belongs_to`関連付けを使わない場合は次のように書けます｡
```ruby
# レビューの所有者（映画）を取得
review = Review.first
Movie.find(review.movie_id)
```

## has_many :through

### 1. 使用シーン

`has_many :through`は2つのモデルが中間テーブルを介して多対多の関係にあるときに使います。

- 医者が複数の患者を持ち、患者も複数の医者を持つ（予約テーブルを介して）
- 学生が複数の授業を取り、授業も複数の学生を持つ（履修テーブルを介して）

### 2. アソシエーションの設定
今までの内容を踏まえると､医者･患者･予約の関係は次のようになるかと思います｡
```ruby
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
```

この関連付けを使って医者の患者リストを取得してみます｡
```ruby
doctor = Doctor.first
Patient.where(id: doctor.appointments.select(:patient_id))
```

取得できるにはできますが､もっと直感的に`doctor.patients`としたいですよね｡
それを叶えてくれるのが`has_many :through`です｡

先程のアソシエーションに`has_many :through`を追加しましょう｡

```ruby
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`関連付けにより中間テーブルを通してお互いのリストを取得できます｡
```ruby
# 医者の患者リストを取得
doctor = Doctor.first
doctor.patients

# 患者の医者リストを取得
patient = Patient.first
patient.doctors
```

### 4. SQL文
`doctor.patients`によって発行されるSQL文です｡

```SQL
SELECT "patients".* 
FROM "patients" 
INNER JOIN "appointments" ON "appointments"."patient_id" = "patients"."id" 
WHERE "appointments"."doctor_id" = ?
```

動作は次のようになります｡
1. patientsテーブルから全てのカラム`（*）`を選択します
1. 結合条件に従ってappointmentsテーブルと内部結合`（INNER JOIN）`します
1. WHERE句で特定の医者`（appointments.doctor_id = ?）`に関連する患者のみをフィルタリングします
1. `?`にはdoctor.idの値が入ります

## has_one

### 1. 使用シーン

`has_one`はあるモデルが他のモデルを1つだけ持っている関係(1対1)を表現するときに使います｡

- 患者は1つの保険証を持っている
- ユーザーは1つのプロフィールを持っている

### 2. アソシエーションの設定
患者と保険証の関係を例にアソシエーションを設定していきます｡

`belongs_to`: 外部キーを持つモデルに記述する (保険証モデル)
`has_one`: 外部キーを持たないモデルに記述する (患者モデル)

```ruby
class Patient < ApplicationRecord
  has_one :insurance_card
end

class InsuranceCard < ApplicationRecord
  belongs_to :patient
end
```

### 3. 使い方
`has_one`関連付けにより､所有しているモデルのデータを取得することができます｡
```ruby
# 患者の保険証を取得 (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の値が入ります｡
```SQL
SELECT "insurance_cards".*
FROM "insurance_cards"
WHERE "insurance_cards"."patient_id" = ?
LIMIT 1
```

`has_one`関連付けを使わない場合は次のように書けます｡
```ruby
patient = Patient.first
InsuranceCard.find_by(patient_id: patient.id)
```

<a name="self_reference"></a>
## 自己結合

### 1. 使用シーン

自己結合の多対多関連は、同じモデル内で複数の関連を持つ場合に使います。

- SNSのフォロー･フォロワーの関係
- ECサイトの商品と関連商品の関係

### 2. アソシエーションの設定

SNSのフォロー･フォロワーの関係は､ユーザーが複数のユーザーをフォローでき、同時に複数のユーザーからフォローされる関係にあります｡

これは多対多の関係なので [has_many :through](#has_many-through) の時のように中間テーブル(Relationshipテーブル)を用意します｡

少し大げさかもしれませんが､今までの内容を踏まえると次のようになります｡
```ruby
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
```

このアソシエーションは勿論動作しません｡
理由はフォローする人とフォローされる人を区別できていないからです｡

同じモデル内で複数の関連を持つ場合には､それぞれを区別できるように名前を付けます｡
```ruby
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. 関連に役割を表す明確な名前をつけ、同じテーブルの異なる参照を区別する
1. `foreign_key` オプションで､中間テーブルのどのカラムを起点にするか指定する
1. `class_name` オプションで、関連先のクラスを指定し、Railsに参照すべきモデルを教える
1. `source` オプションで、`has_many :through` 関連において中間テーブルを通じてどの関連を参照するかを指定する

### 3. 使い方
`has_many :through`によって簡単にユーザーリストを取得できます｡
```ruby
# フォローしているユーザーリストを取得
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文です｡
```SQL
SELECT "users".*
FROM "users"
INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id"
WHERE "relationships"."follower_id" = ?
```
動作は次のようになります｡

1. usersテーブルから全てのカラム`（*）`を選択します
1. 結合条件に従ってrelationshipsテーブルと内部結合`（INNER JOIN）`します
1. WHERE句で`relationships.follower_id`がuser1.idと一致するユーザーのみをフィルタリングします
1. `?`にはuser.idの値が入ります

## ポリモーフィック

### 1. 使用シーン

ポリモーフィック関連は、1つのモデルが複数の異なるモデルに属する場合に使用します｡

- 属性とキャラクター･武器･場所の関係
- 通知といいね･コメント､DMの関係

### 2. アソシエーションの設定
ゲーム内のキャラクターや武器が複数の属性を保有している関係をポリモーフィック関連で表してみます｡

属性のマイグレーションファイルは次のように想定しています｡
```ruby
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`になります｡
```ruby: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`カラムを使用して、関連するモデルを特定します
1. 特定したモデルから`elementable_id`とidが一致するレコードを特定します
<br>

アソシエーションは次のように設定します｡
```ruby
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`

<br>

これによってElementsテーブルには次のようなデータが保存されます｡
| id | name  | elementable_type | elementable_id |
|----|-------|------------------|----------------|
| 1  | fire  | Character        | 2              |
| 2  | water | Character        | 2              |
| 3  | wind  | Location         | 1              |

例えば`Character`テーブルのid:2(`elementable_id`)の属性は炎･水であることが分かりますね｡

<br>

今回は1つのゲームオブジェクトが複数の属性を保有するケースなので､`has_many`を使用していますが､1つの属性を持たせたい場合は`has_one`を使用します｡
```ruby
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`と同じ要領でデータを取得できます｡

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

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

炎属性のキャラクターリストを取得する場合などは､テーブルを結合して取得します｡
```ruby
Character.joins(:elements).where(elements: { name: 'fire' })
```

### 4. 注意点

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

## さいごに
この他にも`has_one :through`や`has_and_belongs_to_many`といったアソシエーションが存在します｡気になる方はRailsガイドで調べてみてください！


## 参考
https://railsguides.jp/association_basics.html