個人メモです。
RailsにおけるモデルであるActive Recordには関連付け(association)という機能がある。
この関連付けとは何なのかについて。
目次
- モデルの関連付けとは?
- 関連付けの種類
- 関連付けの注意点
- belongs_toとhas_oneどちらを選ぶべきか?
- has_many :throughとhas_and_belongs_to_manyのどちらを選ぶかべきか?
- 効率的な関連付けのために知っておくべきこと
モデルの関連付けとは?
モデルはデフォルトでは個々に独立しており関連性がない。関連付けとは複数のモデル同士に関連性を持たせつこと。(そのまま、、)
関連付けとは同期のようなもので、あるモデルの内容を変更すると、それと連動して、関連するモデルの内容も変更できるようになる。
いちいち同じ処理を繰り返さなくていい便利機能。
## 関連付けの種類 6種類の関連付けがサポートされている。1つのモデルに複数使うことができる。
関連付け | 意味 | 内容 |
---|---|---|
belongs_to | 従属。1対1 | 一つのインスタンス同士が繋がる |
has_one | まるごと含む(所有) | すべてのインスタンスが関連する |
has_many | 1対多 | 複数のインスタンスが繋がる。(関連付けされている側にはbelong_toが使われることが多い) |
has_many: through | 多対多。モデルが3つ以上 | 中継するインスタンスを持つモデルが存在する。(モデル1 ⇄ モデル2 ⇄ モデル3) |
has_one: through | 1対1。モデル3つ以上 | あるモデルを経由してインスタンスを取得できる。(モデル1 → モデル2 → モデル3) |
has_and_belongs_to_many | 多対多。モデルは2つ | belongs_toでモデルを関連づけるが、インスタンスの間に複数のデータが存在する場合。(モデルは2つだが、接続用のテーブルが必要) |
## 関連付けの注意点
・モデル名は単数形(命名ルールに従う)
Railsがクラス名を推測するときに命名ルールに従うため。複数形の名前がついていると推測できなくなる。
・has_manyでつなげるテーブル名は複数形になる
対象のモデルの複数のインスタンスが繋がるため。(対象がBookモデルなら、has_many :books
)
・関連付けは通常双方向で設定する
通常、2つのモデル両方に関連を定義する必要がある。双方向の関連付けが認識されると、一方のデータの取得のみでもう一方を操作できるようになる(通信回数が減り効率化に繋がる)
## belongs_toとhas_oneどちらを選ぶべきか? 結論的にはど**ちらも必要。主体となるモデルにhas_oneを記述する**。
2つのモデルの間に1対1の関係を作りたい場合は、一方のモデルにbelongs_toを追加し、もう一方にhas_oneを追加する。
メインとなるモデルに所有を意味するhas_oneを、従属するモデルにbelongs_toを設置する。
SupplierとAccountという2つのモデルがあった場合、SupplierがAccountを持っているのが普通なので、Supplierにhas_one: account, Accountにbelongs_to: supplierを記述する。
# モデル1
class Supplier < ApplicationRecord
has_one :account
end
# モデル2
class Account < ApplicationRecord
belongs_to :supplier
end
## `has_many :through`と`has_and_belongs_to_many`のどちらを選ぶかべきか?
どちらも多対多の宣言をする時に使う。違いは、介在するモデルを、独立したエンティティとして扱いたいかどうか。
簡単なのは、has_and_belongs_to_many
。介在するモデルを作らず、接続用のテーブルだけ作成して関連づける。
中継するモデルをエンティティとして独立して扱いたい場合は、has_many :through
を使う。
また、中継のモデルで検証(validation)、コールバック、追加の属性が必要な場合は、has_many :throughを使う。
# モデル1
class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
# モデル2
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
⇅
# モデル1
class Assembly < ApplicationRecord
has_many :manifests
has_many :parts, through: :manifests
end
# モデル2
class Manifest < ApplicationRecord
belongs_to :assembly
belongs_to :part
end
# モデル3
class Part < ApplicationRecord
has_many :manifests
has_many :assemblies, through: :manifests
end
## 効率的な関連付けのために知っておくべきこと
- キャッシュ制御
- 名前衝突の回避
- スキーマの更新
- 関連付けのスコープ制御
- 双方向関連付け
1. キャッシュ制御
関連付けのメソッドはすべて、効率化のためキャッシュを中心に構築されている。(毎回通信してデータを取得することはしない)
- 最後に実行したクエリの結果はキャッシュに保持され、次回以降の操作で利用できる。
- キャッシュはメソッド間でも共有される。
author.books # データベースからbooksを取得
author.books.size # booksのキャッシュコピーが使われる
author.books.empty? # booksのキャッシュコピーが使われる
# キャッシュ破棄(.reload)
author.books.reload.empty? # booksのキャッシュコピーが破棄される
# 次の処理ではDBから再度読み込み
### 2. 使ってはいけないモデル名(名前衝突の回避) attributesやconnectionは関連付けに使ってはいけない。
生成したモデルが継承しているApplicationRecordが継承しているActiveRecord::Baseのインスタンスには既に使われているメソッドがある。
モデルの関連付けをすると、命名ルールに従ってメソッドが追加されるが、これによりベースのメソッドを上書きしてしまうリスクがある。
## 3. スキーマの更新 定義した関連付けに合わせて、スキーマを更新する必要がある。
belongs_to
を使う場合は、外部キーを作成する必要がある。
has_and_belongs_to_many
を使う場合は、適切なjoinテーブルを作成する必要がある。joinテーブルの名前はアルファベット順になる(詳細は後述)。
3-1. 外部キーの作成(belongs_to)
例えば、Bookモデルをbelongs_toでAuthorモデルと関連づける場合は以下のようになる。
class Book < ApplicationRecord
belongs_to :author
end
▼新しいテーブルの場合
t.references :テーブル名(単数形)
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
**▼既存のテーブルの場合** `add_reference :関連づけるモデル1, :関連づけるモデル2`
class AddAuthorToBooks < ActiveRecord::Migration[5.0]
def change
add_reference :books, :author
end
end
### 3-2. 関連付けに対応するjoinテーブルの作成(has_and_belongs_to_many)
AuthorモデルとBookモデルは命名ルールより、authorsとbooksというテーブルと対応している。
この2つのモデルをhas_and_belongs_to_many
で関連付けた場合は、中間のjoinテーブルとして、
・モデル1のテーブル名_モデル2のテーブル名
というテーブルを生成する必要がある。
モデルのテーブル名の順番はアルファベット順になる。(AuthorとBookなら、authors_booksというテーブル名になる)
# モデル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を使う場合
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を渡す。これがないと関連付けは正常に動作しない。
**create_join_tableを使う場合** create_tableではなく、create_join_tablを使うと簡単に記述できる。
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
## 4. 関連付けのスコープ制御 関連付けによって探索されるオブジェクトは、現在のモジュールのスコープ内のものだけ。
モジュール外のモデルを同士を紐づけるためには、完全な名前空間を指定する必要がある。
4-1. 同じモジュール内の場合
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account
end
class Account < ApplicationRecord
belongs_to :supplier
end
end
end
4-2. 異なるモジュール内の場合
class_name:
で完全な名前空間を指定する。
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
### 4-3. NG事例
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account
end
end
module Billing
class Account < ApplicationRecord
belongs_to :supplier
end
end
end
## 5. 双方向関連付け 通常、2つのモデル両方に関連を定義する必要がある。
これを行うと、モデルが双方向の関連を共有していることを自動的に認識し、オブジェクトのコピーを1つだけ読み出し、アプリケーションをより効率的かつ一貫性を持って操作できるようになる。
5-1. has_manyやbelongs_toの場合
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
end
↓
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
も自動で変更されている。
## 5-2. throughやforeign_keyの場合 :throughや:foreign_keyオプションを使う場合は、`inverse_of`オプションをつける必要がある。
class Author < ApplicationRecord
has_many :books, inverse_of: 'writer'
end
class Book < ApplicationRecord
belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
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
も自動で変更されている。