【Ruby on Rails】モデルを関連づける方法（Active Recordの関連付け）。:referencesやbelongs_toなど

個人メモです。

Railsにおけるモデルである[Active Record](https://qiita.com/yuta-38/items/d3c3a41ff9b46ebd1436)には関連付け（association）という機能がある。

この関連付けとは何なのかについて。

＞[Rails公式 Active Recordの関連付けとは](https://railsguides.jp/association_basics.html)


#目次

1. [モデルの関連付けとは？](#モデルの関連付けとは)
2. [関連付けの種類](#関連付けの種類)
3. [関連付けの注意点](#関連付けの注意点)
4. [belongs_toとhas_oneどちらを選ぶべきか？](#belongs_toとhas_oneどちらを選ぶべきか)
5. [has_many :throughとhas_and_belongs_to_manyのどちらを選ぶかべきか？](#has_many_throughとhas_and_belongs_to_manyのどちらを選ぶかべきか)
6. [効率的な関連付けのために知っておくべきこと](#効率的な関連付けのために知っておくべきこと)
    1. [キャッシュ制御](#1-キャッシュ制御)
    2. [名前衝突の回避](#2-使ってはいけないモデル名名前衝突の回避)
    3. [スキーマの更新](#3-スキーマの更新)
    4. [関連付けのスコープ制御](#4-関連付けのスコープ制御)
    5. [双方向関連付け](#5-双方向関連付け)




##モデルの関連付けとは？
モデルはデフォルトでは個々に独立しており関連性がない。関連付けとは複数のモデル同士に関連性を持たせつこと。（そのまま、、）

関連付けとは**同期のようなもので、あるモデルの内容を変更すると、それと連動して、関連するモデルの内容も変更できるようになる**。

いちいち同じ処理を繰り返さなくていい便利機能。


<br>
##関連付けの種類
6種類の関連付けがサポートされている。１つのモデルに複数使うことができる。

| 関連付け | 意味 | 内容 |
|:--|:--|:--|
| belongs_to  | 従属。1対1 | 一つのインスタンス同士が繋がる |
| has_one  | まるごと含む（所有） | すべてのインスタンスが関連する |
| has_many  | 1対多 |  複数のインスタンスが繋がる。（関連付けされている側にはbelong_toが使われることが多い）  |
| has_many: through  | 多対多。モデルが３つ以上  | 中継するインスタンスを持つモデルが存在する。（モデル1 ⇄ モデル2 ⇄ モデル3）  |
| has_one: through  | 1対1。モデル３つ以上  | あるモデルを経由してインスタンスを取得できる。（モデル1 → モデル2 → モデル3）  |
| has_and_belongs_to_many  | 多対多。モデルは２つ | belongs_toでモデルを関連づけるが、インスタンスの間に複数のデータが存在する場合。（モデルは２つだが、接続用のテーブルが必要）  |




<br>
##関連付けの注意点

**・モデル名は単数形（命名ルールに従う）**
Railsがクラス名を推測するときに命名ルールに従うため。複数形の名前がついていると推測できなくなる。

**・has_manyでつなげるテーブル名は複数形になる**
対象のモデルの複数のインスタンスが繋がるため。（対象がBookモデルなら、`has_many :books`）

**・関連付けは通常双方向で設定する**
通常、2つのモデル両方に関連を定義する必要がある。双方向の関連付けが認識されると、一方のデータの取得のみでもう一方を操作できるようになる（通信回数が減り効率化に繋がる）



<br>
##belongs_toとhas_oneどちらを選ぶべきか？
結論的にはど**ちらも必要。主体となるモデルにhas_oneを記述する**。

２つのモデルの間に１対１の関係を作りたい場合は、一方のモデルにbelongs_toを追加し、もう一方にhas_oneを追加する。

メインとなるモデルに所有を意味するhas_oneを、従属するモデルにbelongs_toを設置する。

SupplierとAccountという２つのモデルがあった場合、SupplierがAccountを持っているのが普通なので、Supplierにhas_one: account, Accountにbelongs_to: supplierを記述する。


```rb:
#モデル１
class Supplier < ApplicationRecord
  has_one :account
end

#モデル２
class Account < ApplicationRecord
  belongs_to :supplier
end
```




<br>
##`has_many :through`と`has_and_belongs_to_many`のどちらを選ぶかべきか？

どちらも多対多の宣言をする時に使う。違いは、介在するモデルを、独立したエンティティとして扱いたいかどうか。

簡単なのは、`has_and_belongs_to_many`。介在するモデルを作らず、接続用のテーブルだけ作成して関連づける。


中継するモデルをエンティティとして独立して扱いたい場合は、`has_many :through`を使う。

また、中継のモデルで検証(validation)、コールバック、追加の属性が必要な場合は、has_many :throughを使う。


```rb:has_and_belongs_to_many
#モデル１
class Assembly < ApplicationRecord
  has_and_belongs_to_many :parts
end

#モデル2
class Part < ApplicationRecord
  has_and_belongs_to_many :assemblies
end
```

⇅

```rb:has_many_through
#モデル１
class Assembly < ApplicationRecord
  has_many :manifests
  has_many :parts, through: :manifests
end

#モデル２
class Manifest < ApplicationRecord
  belongs_to :assembly
  belongs_to :part
end

#モデル３
class Part < ApplicationRecord
  has_many :manifests
  has_many :assemblies, through: :manifests
end
```


<br>
##効率的な関連付けのために知っておくべきこと

1. キャッシュ制御
2. 名前衝突の回避
3. スキーマの更新
4. 関連付けのスコープ制御
5. 双方向関連付け


###1. キャッシュ制御
関連付けのメソッドはすべて、効率化のためキャッシュを中心に構築されている。（毎回通信してデータを取得することはしない）

- 最後に実行したクエリの結果はキャッシュに保持され、次回以降の操作で利用できる。
- キャッシュはメソッド間でも共有される。

```rb:キャッシュを使った処理の例
author.books       # データベースからbooksを取得
author.books.size  # booksのキャッシュコピーが使われる
author.books.empty? # booksのキャッシュコピーが使われる


#キャッシュ破棄（.reload）
author.books.reload.empty?  # booksのキャッシュコピーが破棄される
                            # 次の処理ではDBから再度読み込み
```


<br>
###2. 使ってはいけないモデル名（名前衝突の回避）
attributesやconnectionは関連付けに使ってはいけない。

生成したモデルが継承しているApplicationRecordが継承しているActiveRecord::Baseのインスタンスには既に使われているメソッドがある。

モデルの関連付けをすると、命名ルールに従ってメソッドが追加されるが、これによりベースのメソッドを上書きしてしまうリスクがある。



<br>
##3. スキーマの更新
定義した関連付けに合わせて、スキーマを更新する必要がある。

`belongs_to`を使う場合は、**外部キーを作成する必要がある**。

`has_and_belongs_to_many`を使う場合は、**適切なjoinテーブルを作成する必要がある**。joinテーブルの名前はアルファベット順になる（詳細は後述）。


###3-1. 外部キーの作成（belongs_to）
例えば、Bookモデルをbelongs_toでAuthorモデルと関連づける場合は以下のようになる。

```rb:Bookモデル
class Book < ApplicationRecord
  belongs_to :author
end
```

**▼新しいテーブルの場合**
`t.references :テーブル名（単数形）`

```rb:マイグレーションファイル
class CreateBooks < ActiveRecord::Migration[5.0]
  def change
    create_table :books do |t|
      t.datetime :published_at
      t.string   :book_number
      t.references :author
    end
  end
end
```


<br>
**▼既存のテーブルの場合**
`add_reference :関連づけるモデル1, :関連づけるモデル2`

```rb:マイグレーションファイル
class AddAuthorToBooks < ActiveRecord::Migration[5.0]
  def change
    add_reference :books, :author
  end
end
```


<br>
###3-2. 関連付けに対応するjoinテーブルの作成（has_and_belongs_to_many）

AuthorモデルとBookモデルは命名ルールより、authorsとbooksというテーブルと対応している。

この２つのモデルを`has_and_belongs_to_many`で関連付けた場合は、中間のjoinテーブルとして、

・`モデル1のテーブル名_モデル2のテーブル名`というテーブルを生成する必要がある。

モデルの**テーブル名の順番はアルファベット順**になる。（AuthorとBookなら、authors_booksというテーブル名になる）


```rb:モデル
#モデル1
class Assembly < ApplicationRecord
  has_and_belongs_to_many :parts
end

#モデル2
class Part < ApplicationRecord
  has_and_belongs_to_many :assemblies
end
```

⇅

関連付けに対応するassemblies_partsテーブルをマイグレーションで作成ておく。（このテーブルには主キーを設定しないこと）

**▼create_tableを使う場合**

```rb:マイグレーション
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0]
  def change
    create_table :assemblies_parts, id: false do |t|
      t.bigint :assembly_id
      t.bigint :part_id
    end

    add_index :assemblies_parts, :assembly_id
    add_index :assemblies_parts, :part_id
  end
end
```

・`id: false`
joinテーブルはモデルを表さないので、create_tableにid: falseを渡す。これがないと関連付けは正常に動作しない。



<br>
**create_join_tableを使う場合**
create_tableではなく、create_join_tablを使うと簡単に記述できる。

```rb:マイグレーション
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0]
  def change
    create_join_table :assemblies, :parts do |t|
      t.index :assembly_id
      t.index :part_id
    end
  end
end
```


<br>
##4. 関連付けのスコープ制御
関連付けによって探索されるオブジェクトは、現在のモジュールのスコープ内のものだけ。

モジュール外のモデルを同士を紐づけるためには、完全な名前空間を指定する必要がある。


###4-1. 同じモジュール内の場合

```rb:モジュール
module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account
    end

    class Account < ApplicationRecord
      belongs_to :supplier
    end
  end
end
```


###4-2. 異なるモジュール内の場合
`class_name:`で完全な名前空間を指定する。

```rb:モジュール
module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account,
      class_name: "MyApplication::Billing::Account"
    end
  end

  module Billing
    class Account < ApplicationRecord
      belongs_to :supplier,
      class_name: "MyApplication::Business::Supplier"
    end
  end
end
```

<br>
###4-3. NG事例

```rb:モジュール
module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account
    end
  end

  module Billing
    class Account < ApplicationRecord
      belongs_to :supplier
    end
  end
end
```


<br>
##5. 双方向関連付け
通常、2つのモデル両方に関連を定義する必要がある。

これを行うと、モデルが双方向の関連を共有していることを自動的に認識し、オブジェクトのコピーを１つだけ読み出し、アプリケーションをより効率的かつ一貫性を持って操作できるようになる。

##5-1. has_manyやbelongs_toの場合

```rb:モデルの関連付け
class Author < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :author
end
```

↓

```rb:双方向関連性の確認
a = Author.first
b = a.books.first
a.first_name == b.author.first_name # => true
a.first_name = 'David'
a.first_name == b.author.first_name # => true
```

Authorモデルのインスタンス、`a.first_name`を変更すると、Bookモデルのインスタンス`b.author.first_name`も自動で変更されている。


<br>
##5-2. throughやforeign_keyの場合
:throughや:foreign_keyオプションを使う場合は、`inverse_of`オプションをつける必要がある。


```rb:モデルの関連付け
class Author < ApplicationRecord
  has_many :books, inverse_of: 'writer'
end

class Book < ApplicationRecord
  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
```


```rb:双方向関連性の確認
a = Author.first
b = a.books.first
a.first_name == b.writer.first_name # => true
a.first_name = 'David'
a.first_name == b.writer.first_name # => true
```

Authorモデルのインスタンス、`a.first_name`を変更すると、Bookモデルのインスタンス`b.author.first_name`も自動で変更されている。