本ガイドでは、Active Recordの関連付け機能(アソシエーション)について解説します。
このガイドの内容:
Active Recordの「関連付け(アソシエーション: association)」を使うと、モデル間のリレーションシップを定義できます。関連付けは特殊なマクロスタイルの呼び出しとして実装されており、モデル同士をどのように関連させるかをRailsに手軽に指定できます。これにより、データの管理がより効率的になり、一般的なデータ操作がシンプルで読みやすくなります。
マクロスタイルの呼び出しは、実行時に他のメソッドを動的に生成・変更するメソッドであり、Railsでのモデルの関連付けの定義など、簡潔で表現力豊かな機能の宣言を可能にします。たとえばhas_many :comments
のように記述します。
関連付けを設定すると、Railsが2つのモデルのインスタンス同士の主キー(primary key)と外部キー(foreign key)のリレーションシップや管理を支援し、データベースがデータの整合性を保つようにします。これにより、関連付けられているデータの取得・更新・削除を手軽に行えるようになります。
これにより、どのレコードがどのレコードと関係があるかを簡単に把握できるようになります。また、モデルにさまざまな便利メソッドが追加されるため、関連データをより手軽に操作可能になります。
Author
(著者)モデルとBook
(書籍)モデルを持つシンプルなアプリケーションを例に考えてみましょう。
関連付けが設定されていない以下のような場合、その著者の本を作成・削除するために、以下のように面倒な手動の処理が必要になります。
class CreateAuthors < ActiveRecord::Migration[7.2] def change create_table :authors do |t| t.string :name t.timestamps end create_table :books do |t| t.references :author t.datetime :published_at t.timestamps end end end
class Author < ApplicationRecord end class Book < ApplicationRecord end
ここで、既存の著者に新しい書籍を1件追加するには、以下のようにauthor_id
の値を明示的に指定しなければならないでしょう。
@book = Book.create(author_id: @author.id, published_at: Time.now)
今度は著者を1人削除し、その著者の書籍もすべて削除する場合を考えてみましょう。以下のように、その著者のbooks
をすべて取り出してから、個別のbook
をeach
で回して削除し、それが終わってから著者を削除しなければならないでしょう。
@books = Book.where(author_id: @author.id) @books.each do |book| book.destroy end @author.destroy
しかし関連付けを使えば、2つのモデルのリレーションシップをRailsに明示的に指定することで、こうした操作を効率化できます。関連付けを使う形でAuthor
モデルとBook
モデルを設定する修正コードは次のとおりです。
class Author < ApplicationRecord has_many :books, dependent: :destroy end class Book < ApplicationRecord belongs_to :author end
上のように関連付けを追加したことで、特定の著者の新しい書籍を1冊追加する作業が以下のように1行でシンプルに書けるようになりました。
@book = @author.books.create(published_at: Time.now)
著者と、その著者の書籍をまとめて削除する作業も、以下のようにずっと簡単に書けます。
@author.destroy
Railsで関連付けを設定する場合は、データベースが関連付けを適切に処理するよう構成するために、マイグレーションを作成する必要があります。このマイグレーションでは、関連付けで必要となる外部キー列をデータベーステーブルに追加しておく必要があります。
たとえば、Book
モデルにbelongs_to :author
関連付けを設定する場合は、books
テーブルにauthor_id
カラムを追加するマイグレーションを以下のコマンドで作成します。
rails generate migration AddAuthorToBooks author:references
このマイグレーションを行うことで、author_id
カラムが追加され、データベースに外部キーのリレーションが設定され、それによってモデルとデータベースが同期した状態が維持されます。
その他の関連付け方法については、本ガイドの次のセクションをお読みください。その後に、関連付けに関するさまざまなヒントや活用方法も記載されています。ガイドの末尾では、Railsの関連付けメソッドとオプションの完全な参考情報も記載されています。
Railsでは6種類の関連付けをサポートしています。それぞれの関連付けは、特定の用途に特化しています。
以下は、Railsでサポートされている全種類の関連付けのリストです。リストはAPIドキュメントにリンクされているので、詳しい情報や利用方法、メソッドパラメータなどはリンク先を参照してください。
本ガイドでは以後、それぞれの関連付けの宣言方法と利用方法について詳しく解説します。その前に、それぞれの関連付けが適切となる状況について簡単にご紹介します。
belongs_to
関連付けあるモデルでbelongs_to
関連付けを行なうと、宣言を行った側のモデルの各インスタンスは、他方のモデルのインスタンスに文字どおり「従属(belongs to)」します。
たとえば、Railsアプリケーションに著者(Author
)と書籍(Book
)の情報が含まれており、書籍1冊につき正確に1人の著者を割り当てたい場合は、Book
モデルで以下のように宣言します。
class Book < ApplicationRecord belongs_to :author end
belongs_to
関連付けで指定するモデル名は必ず「単数形」にしなければなりません。上記の例で、Book
モデルのauthor
関連付けを複数形(authors
)にしてからBook.create(authors: @author)
でインスタンスを作成しようとすると、uninitialized constant Book::Authors
エラーが発生します。Railsは、関連付けの名前から自動的にモデルのクラス名を推測します。関連付け名を:authors
にすると、Railsは本来のAuthor
クラスではなくAuthors
という誤ったクラス名を探索してしまいます。
上の関連付けに対応するマイグレーションは以下のような感じになります。
class CreateBooks < ActiveRecord::Migration[7.2] def change create_table :authors do |t| t.string :name t.timestamps end create_table :books do |t| t.belongs_to :author t.datetime :published_at t.timestamps end end end
データベースの観点におけるbelongs_to
関連付けは、このモデルのテーブルに、他方のテーブルへの参照を表すカラムが存在することを意味します。これは、設定に応じて「1対1リレーション」や「1対多リレーション」を設定するのに使えます。他方のクラスのテーブルに1対1リレーションの参照が含まれている場合は、belongs_to
関連付けではなくhas_one
関連付けを使う必要があります。
belongs_to
を単独で利用すると、一方向の1対1リレーションが生成されます。したがって、上記の例における個別のbook
はそのauthor
を「認識」しますが、逆にauthor
は自分の著書であるbook
を認識しません。
双方向関連付けを設定するには、belongs_to
を他のモデル(この場合はAuthor
モデル)のhas_one
またはhas_many
と組み合わせる形で使います。
belongs_to
はデフォルトで、参照整合性を保証するために、関連付けられたレコードの存在バリデーションを行います。
モデルでoptional
がtrue
に設定されている場合、belongs_to
は参照整合性を保証しません。つまり、あるテーブルの外部キーが指している、参照先のテーブルの主キーが必ずしも有効ではない可能性があります。
class Book < ApplicationRecord belongs_to :author, optional: true end
つまり、ユースケースによっては、以下のようにforeign_key: true
オプションでデータベースレベルの外部キー制約を参照カラムに追加する必要が生じることもあります。
create_table :books do |t| t.belongs_to :author, foreign_key: true # ... end
上のように設定することで、author_id
カラムがoptional: true
でNULL許容に設定されているとしても、このカラムがNULLでない場合は、参照するauthors
テーブル内のレコードが必ず有効でなければならないことが保証されます。
belongs_to
関連付けで追加されるメソッドbelongs_to
関連付けを宣言したクラスでは、さまざまなメソッドが自動的に利用できるようになります。以下はその一部です。
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association
reset_association
association_changed?
association_previously_changed?
本ガイドでは、よく使われるメソッドの一部を取り上げていますが、完全なリストについてはActive Recordの関連付けAPIを参照してください。
上のメソッド名のassociation
の部分はプレースホルダなので、belongs_to
の第1引数として渡されるシンボルで読み替えてください。
たとえば以下のようなモデルが宣言されているとします。
# app/models/book.rb class Book < ApplicationRecord belongs_to :author end # app/models/author.rb class Author < ApplicationRecord has_many :books validates :name, presence: true end
このとき、Book
モデルのインスタンスで以下のメソッドが使えるようになります。
author
author=
build_author
create_author
create_author!
reload_author
reset_author
author_changed?
author_previously_changed?
新しく作成したhas_one
関連付けまたはbelongs_to
関連付けを初期化するには、association.build
メソッドではなく、必ずbuild_
で始まるメソッドを使わなければなりません(association.build
は、has_many
関連付けやhas_and_belongs_to_many
関連付けで使います)。関連付けを作成する場合は、create_
で始まるメソッドをお使いください。
association
メソッドは、関連付けられたオブジェクトを返します。関連付けられたオブジェクトがない場合はnil
を返します。
@author = @book.author
このオブジェクトに関連付けられたオブジェクトがデータベースから既に取得されている場合は、キャッシュされたものを返します。この振る舞いを上書きして、キャッシュを読み出さずにデータベースから強制的に読み込みたい場合は、親オブジェクトが持つ#reload_association
メソッドを呼び出します。
@author = @book.reload_author
関連付けされたオブジェクトのキャッシュバージョンをアンロードして、次回のアクセスでデータベース呼び出しからクエリするには、親オブジェクトの#reset_association
を呼び出します。
@book.reset_author
association=
メソッドは、関連付けられたオブジェクトをそのオブジェクトに割り当てます。これは、このオブジェクトから主キーを抽出して、関連付けられたオブジェクトの外部キーに同じ値を設定することを意味しています。
@book.author = @author
build_association
メソッドは、関連付けられた型の新しいオブジェクトを返します。返されるオブジェクトは、渡された属性に基いてインスタンス化され、外部キーを経由するリンクが設定されます。関連付けられたオブジェクトは、その時点ではまだ保存されないことにご注意ください。
@author = @book.build_author(author_number: 123, author_name: "John Doe")
create_association
メソッドは、上のbuild_association
に加えて、関連付けられたモデルで指定されているバリデーションがすべてパスしたときに、そのオブジェクトの保存も行います。
@author = @book.create_author(author_number: 123, author_name: "John Doe")
最後に、create_association!
は上のcreate_association
と同じですが、レコードが無効な場合にActiveRecord::RecordInvalid
がraiseされる点が異なります。
# nameが空なのでActiveRecord::RecordInvalidをraiseする begin @book.create_author!(author_number: 123, name: "") rescue ActiveRecord::RecordInvalid => e puts e.message end
irb> raise_validation_error: Validation failed: Name can't be blank (ActiveRecord::RecordInvalid)
association_changed?
メソッドは、新しい関連付けオブジェクトが割り当てられた場合にtrue
を返します。外部キーは次の保存で更新されます。
association_previously_changed?
メソッドは、関連付けが前回の保存で更新されて新しい関連付けオブジェクトを参照している場合にtrue
を返します。
@book.author # => #<Author author_number: 123, author_name: "John Doe"> @book.author_changed? # => false @book.author_previously_changed? # => false @book.author = Author.second # => #<Author author_number: 456, author_name: "Jane Smith"> @book.author_changed? # => true @book.save! @book.author_changed? # => false @book.author_previously_changed? # => true
model.association_changed?
とmodel.association.changed?
を取り違えないようご注意ください。前者のmodel.association_changed?
は、その関連付けが新しいレコードで置き換えられたかどうかをチェックしますが、後者のmodel.association.changed?
は関連付けの「属性」が変更されたかどうかをチェックします。
association.nil?
メソッドを用いて、関連付けられたオブジェクトが存在するかどうかをチェックできます。
if @book.author.nil? @msg = "この本の著者が見つかりません" end
オブジェクトをbelongs_to
関連付けに割り当てても、現在のオブジェクトや関連付けられたオブジェクトが自動的に保存されるわけではありません。ただし、現在のオブジェクトを保存すれば、関連付けられたオブジェクトも保存されます。
has_one
関連付けhas_one
関連付けは、相手側のモデルがこのモデルへの参照を持っていることを示します。相手側のモデルは、この関連付けを経由してフェッチできます。
たとえば、アプリケーション内で供給元(supplier)ごとにアカウント(account)が1個だけ存在する場合は、次のようにSupplier
モデルを宣言します。
class Supplier < ApplicationRecord has_one :account end
belongs_to
関連付けとの主な違いは、リンクカラム(ここではsupplier_id
)が相手側のテーブルにあり、has_one
を宣言したテーブルには存在しないことです。
上の関連付けに対応するマイグレーションは以下のような感じになります。
class CreateSuppliers < ActiveRecord::Migration[7.2] def change create_table :suppliers do |t| t.string :name t.timestamps end create_table :accounts do |t| t.belongs_to :supplier t.string :account_number t.timestamps end end end
has_one
関連付けは、他方のモデルとの1対1対応を作成します。データベースの観点では、このhas_one
関連付けは、外部キーが他方のクラスに存在することを意味します。外部キーがこのクラスに含まれている場合は、has_one
ではなく、代わりにbelongs_to
を使う必要があります。
ユースケースによっては、accounts
テーブルとの関連付けのために、supplier
カラムにuniqueインデックスか外部キー制約を追加する必要が生じることもあります。uniqueインデックスにより、個別の供給元が1個のアカウントだけに関連付けられ、効率よくクエリを実行できるようになります。
一方、外部キー制約により、accounts
テーブルのsupplier_id
がsuppliers
テーブルの有効なsupplier
を参照することが保証されます。これにより、関連付けがデータベースレベルで強制されます。
create_table :accounts do |t| t.belongs_to :supplier, index: { unique: true }, foreign_key: true # ... end
このリレーションは、相手側のモデルでbelongs_to
関連付けも設定することで双方向関連付けになります。
has_one
で追加されるメソッドhas_one
関連付けを宣言したクラスでは、さまざまなメソッドが自動的に利用できるようになります。以下はその一部です。
association
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association
reset_association
本ガイドでは、よく使われるメソッドの一部を取り上げていますが、完全なリストについてはActive Recordの関連付けAPIを参照してください。
上のメソッド名のassociation
の部分はプレースホルダなので、has_one
の第1引数として渡されるシンボルで読み替えてください。
たとえば以下のようなモデルが宣言されているとします。
# app/models/supplier.rb class Supplier < ApplicationRecord has_one :account end # app/models/account.rb class Account < ApplicationRecord validates :terms, presence: true belongs_to :supplier end
このとき、Supplier
モデルのインスタンスで以下のメソッドが使えるようになります。
account
account=
build_account
create_account
create_account!
reload_account
reset_account
新しく作成したhas_one
関連付けまたはbelongs_to
関連付けを初期化するには、association.build
メソッドではなく、必ずbuild_
で始まるメソッドを使わなければなりません(association.build
はhas_many
関連付けやhas_and_belongs_to_many
関連付けで使います)。関連付けを作成する場合は、create_
で始まるメソッドをお使いください。
association
メソッドは、関連付けられたオブジェクトを返します。関連付けられたオブジェクトがない場合はnil
を返します。
@account = @supplier.account
関連付けられたオブジェクトがデータベースから既に取得されている場合は、キャッシュされたものを返します。この振る舞いを上書きして、キャッシュを読み出さずにデータベースから強制的に読み込みたい場合は、親オブジェクトが持つ#reload_association
メソッドを呼び出します。
@account = @supplier.reload_account
関連付けされたオブジェクトのキャッシュバージョンをアンロードして、次回のアクセスでデータベース呼び出しからクエリするには、親オブジェクトの#reset_association
を呼び出します。
@supplier.reset_account
association=
メソッドは、関連付けられたオブジェクトをそのオブジェクトに割り当てます。これは、このオブジェクトから主キーを抽出して、関連付けられたオブジェクトの外部キーに同じ値を設定することを意味しています。
@supplier.account = @account
build_association
メソッドは、関連付けられた型の新しいオブジェクトを返します。返されるオブジェクトは、渡された属性に基いてインスタンス化され、外部キーを経由するリンクが設定されます。関連付けられたオブジェクトは、その時点ではまだ保存されないことにご注意ください。
@account = @supplier.build_account(terms: "Net 30")
create_association
メソッドは、上のbuild_association
に加えて、関連付けられたモデルで指定されているバリデーションがすべてパスしたときに、そのオブジェクトの保存も行います。
@account = @supplier.create_account(terms: "Net 30")
最後に、create_association!
は上のcreate_association
と同じですが、レコードが無効な場合にActiveRecord::RecordInvalid
がraiseされる点が異なります。
# termsが空なのでActiveRecord::RecordInvalidをraiseする begin @supplier.create_account!(terms: "") rescue ActiveRecord::RecordInvalid => e puts e.message end
irb> raise_validation_error: Validation failed: Terms can't be blank (ActiveRecord::RecordInvalid)
association.nil?
メソッドを用いて、関連付けられたオブジェクトが存在するかどうかをチェックできます。
if @supplier.account.nil? @msg = "この本の著者が見つかりません" end
オブジェクトをhas_one
関連付けに割り当てると、そののオブジェクトや関連付けられたオブジェクトが自動的に保存されて外部キーが更新されます。また、置き換えられるオブジェクトも自動的に保存され、その外部キーも更新されます。
保存のいずれかがバリデーションエラーで失敗すると、割り当てステートメントはfalse
を返し、割り当て自体がキャンセルされます。
親オブジェクト(has_one
関連付けを宣言している側のオブジェクト)が保存されていない場合(つまり、new_record?
がtrue
を返す場合)、子オブジェクトはすぐには保存されません。親オブジェクトが保存されると、子オブジェクトは自動的に保存されます。
オブジェクトを保存せずにhas_one
関連付けにオブジェクトを割り当てるには、build_association
メソッドを使います。このメソッドは、関連付けられたオブジェクトの新しい未保存のインスタンスを作成して、保存するかどうかを決定する前に作業可能にします。
モデルに関連付けられたオブジェクトを保存するかどうかを制御するには、autosave: false
オプションを使います。この設定により、親オブジェクトが保存されたときに関連付けられたオブジェクトが自動的に保存されなくなります。逆に、保存されていない関連付けられたオブジェクトを操作し、準備ができるまでその永続化を遅延する必要がある場合は、build_association
メソッドを使います。
has_many
関連付けhas_many
関連付けは、has_one
と似ていますが、相手のモデルとの「1対多」のつながりを表す点が異なります。has_many
関連付けは、多くの場合belongs_to
の反対側で使われます。
has_many
関連付けは、そのモデルの各インスタンスが、相手のモデルのインスタンスを0個以上持っていることを示します。たとえば、さまざまな著者(Author
)や書籍(Book
)を含むアプリケーションでは、Author
モデルを以下のように宣言できます。
class Author < ApplicationRecord has_many :books end
has_many
関連付けは、モデル間に1対多のリレーションシップを確立し、宣言したモデル(Author
)の各インスタンスが、関連付けられたモデル(Book
)のインスタンスを複数持てるようにします。
has_one
関連付けやbelongs_to
関連付けの場合と異なり、has_many
関連付けを宣言する場合は、相手のモデル名を「複数形」で指定する必要があります。
上の関連付けに対応するマイグレーションは以下のような感じになります。
class CreateAuthors < ActiveRecord::Migration[7.2] def change create_table :authors do |t| t.string :name t.timestamps end create_table :books do |t| t.belongs_to :author t.datetime :published_at t.timestamps end end end
has_many
関連付けは、他方のモデルと1対多のリレーションシップを作成します。データベースの観点におけるhas_many
関連付けは、他方のクラスがこのクラスのインスタンスを参照する外部キーを持つことを意味します。
このマイグレーションではauthors
テーブルが作成され、著者名を保存するname
カラムがテーブルに含まれます。books
テーブルも作成され、belongs_to :author
関連付けが含まれます。
この関連付けにより、books
テーブルとauthors
テーブルの間に外部キーリレーションシップが確立されます。具体的には、books
テーブルのauthor_id
カラムが、authors
テーブルのid
カラムを参照する外部キーとして機能します。このbelongs_to :author
関連付けをbooks
テーブルに含めると、Author
モデルからのhas_many
関連付けが有効になり、個別の書籍が1人の著者に関連付けられます。この設定により、1人の著者が複数の関連する書籍を持てるようになります。
ユースケースにもよりますが、通常はこのbooks
テーブルのauthor
カラムに「non-unique」インデックスを追加し、オプションで外部キー制約を作成することをオススメします。author_id
カラムにインデックスを追加すると、特定の著者に関連付けられた書籍を取得するときのクエリパフォーマンスが向上します。
データベースレベルで参照整合性を適用する場合は、上記のreference
カラム宣言にforeign_key: true
オプションを追加します。これにより、books
テーブルのauthor_id
が、authors
テーブルの有効なid
に対応づけられるようになります。
create_table :books do |t| t.belongs_to :author, index: true, foreign_key: true # ... end
このリレーションは、相手側のモデルでbelongs_to
関連付けも設定することで双方向関連付けにできます。
has_many
関連付けで追加されるメソッドhas_many
関連付けを宣言したクラスでは、さまざまなメソッドが自動的に利用できるようになります。以下はその一部です。
collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {})
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload
本ガイドでは、よく使われるメソッドの一部を取り上げていますが、完全なリストについてはActive Recordの関連付けAPIを参照してください。
上のメソッド名のcollection
の部分はプレースホルダなので、has_many
の第1引数として渡されるシンボルで読み替えてください。また、collection_singular
の部分はコレクション名を単数形にして読み替えてください。
たとえば以下の宣言があるとします。
class Author < ApplicationRecord has_many :books end
これにより、Author
モデルで以下のメソッドが使えるようになります。
books books<<(object, ...) books.delete(object, ...) books.destroy(object, ...) books=(objects) book_ids book_ids=(ids) books.clear books.empty? books.size books.find(...) books.where(...) books.exists?(...) books.build(attributes = {}, ...) books.create(attributes = {}) books.create!(attributes = {}) books.reload
collection
メソッドは、関連付けられたすべてのオブジェクトのリレーションを返します。関連付けられたオブジェクトがない場合は、空のリレーションを1つ返します。
@books = @author.books
collection.delete
メソッドは、外部キーをNULLに設定することで、コレクションから1個以上のオブジェクトを削除します。
@author.books.delete(@book1)
削除の方法はこれだけではありません。オブジェクト同士がdependent: :destroy
で関連付けられている場合はdestroy
で削除されますが、オブジェクト同士がdependent: :delete_all
で関連付けられている場合はdelete
で削除されるのでご注意ください。
collection.destroy
メソッドは、コレクションに関連付けられているオブジェクトに対してdestroy
を実行することで、コレクションから1つ以上のオブジェクトを削除します。
@author.books.destroy(@book1)
この場合オブジェクトは無条件にデータベースから削除されます。このとき:dependent
オプションはすべて無視されます。
collection.clear
メソッドは、dependent
オプションで指定された戦略に応じて、コレクションからすべてのオブジェクトを削除します。オプションが渡されなかった場合は、デフォルトの戦略に従います。デフォルトの戦略は、has_many :through
関連付けの場合はdelete_all
が指定され、has_many
関連付けの場合は外部キーがNULLに設定されます。
@author.books.clear
オブジェクトがdependent: :destroy
またはdependent: :destroy_async
を指定して関連付けされていた場合、それらのオブジェクトはdependent: :delete_all
の場合と同様に削除されます。
collection.reload
メソッドは、関連付けられたすべてのオブジェクトのリレーションを1つ返し、データベースを強制的に読み出します。関連付けられたオブジェクトがない場合は、空のリレーションを1つ返します。
@books = @author.books.reload
collection=(objects)
メソッドは、削除や追加を適宜実行することで、渡したオブジェクトだけがそのコレクションに含まれるようにします。変更の結果はデータベースで永続化されます。
collection_singular_ids=(ids)
メソッドは、削除や追加を適宜実行することで、指定した主キーのidを持つオブジェクトだけがコレクションに含まれるようにします。変更の結果はデータベースで永続化されます。
collection_singular_ids
メソッドは、そのコレクションに含まれるオブジェクトのidを配列にしたものを返します。
@book_ids = @author.book_ids
collection.empty?
メソッドは、関連付けられたオブジェクトがコレクションに存在しない場合にtrue
を返します。
<% if @author.books.empty? %> No Books Found <% end %>
collection.size
メソッドは、コレクションに含まれるオブジェクトの個数を返します。
@book_count = @author.books.size
collection.find
メソッドは、コレクションに含まれるオブジェクトを検索します。
@available_book = @author.books.find(1)
collection.where
メソッドは、コレクションに含まれているオブジェクトを指定された条件に基いて検索します。このメソッドではオブジェクトは遅延読み込み(lazy load)されるので、オブジェクトに実際にアクセスするときだけデータベースへのクエリが発生します。
@available_books = @author.books.where(available: true) # クエリはまだ発生しない @available_book = @available_books.first # ここでクエリが発生する
collection.exists?
メソッドは、指定された条件に合うオブジェクトがコレクションの中に存在するかどうかをチェックします。
collection.build
メソッドは、関連付けされた型のオブジェクトまたはオブジェクトの配列を返します。返されるオブジェクトは、渡された属性に基いてインスタンス化され、外部キーを経由するリンクが作成されます。関連付けられたオブジェクトはまだ保存されないことにご注意ください。
@book = @author.books.build(published_at: Time.now, book_number: "A12345") @books = @author.books.build([ { published_at: Time.now, book_number: "A12346" }, { published_at: Time.now, book_number: "A12347" } ])
collection.create
メソッドは、関連付けされた型の新しいオブジェクトまたはオブジェクトの配列を返します。このオブジェクトは、渡された属性を用いてインスタンス化され、そのオブジェクトの外部キーを介してリンクが作成されます。そして、関連付けられたモデルで指定されているバリデーションがすべてパスすると、この関連付けられたオブジェクトは保存されます。
@book = @author.books.create(published_at: Time.now, book_number: "A12345") @books = @author.books.create([ { published_at: Time.now, book_number: "A12346" }, { published_at: Time.now, book_number: "A12347" } ])
collection.create!
は上のcollection.create
と同じですが、レコードが無効な場合にActiveRecord::RecordInvalid
がraiseされる点が異なります。
has_many
関連付けにオブジェクトを割り当てると、外部キーを更新するためにそのオブジェクトは自動的に保存されます。1つの文で複数のオブジェクトを割り当てると、それらはすべて保存されます。
関連付けられているオブジェクトのどれかがバリデーションエラーで保存に失敗すると、false
を返し、割り当てはキャンセルされます。
親オブジェクト(has_many
関連付けを宣言している側のオブジェクト)が保存されない場合(つまりnew_record?
がtrue
を返す場合)、子オブジェクトを追加したときに保存されません。親オブジェクトが保存されると、関連付けられていたオブジェクトのうち保存されていなかったメンバーはすべて保存されます。
has_many
関連付けにオブジェクトを割り当てて、しかもそのオブジェクトを保存したくない場合は、collection.build
メソッドをお使いください。
has_many :through
関連付けhas_many :through
関連付けは、他方のモデルと「多対多」のリレーションシップを設定する場合によく使われます。この関連付けでは、2つのモデルの間に「第3のモデル」(joinモデル)が介在し、それを経由(through)して相手のモデルの「0個以上」のインスタンスとマッチします。
たとえば、患者(patients)が医師(physicians)との診察予約(appointments)を設定する医療業務を考えてみます。この場合、関連付けの宣言は次のような感じになるでしょう。
class Physician < ApplicationRecord has_many :appointments has_many :patients, through: :appointments end class Appointment < ApplicationRecord belongs_to :physician belongs_to :patient end class Patient < ApplicationRecord has_many :appointments has_many :physicians, through: :appointments end
has_many :through
関連付けは、モデル同士の間に多対多リレーションシップを確立し、一方のモデル(Physician
)のインスタンスが、第3の「join」モデル(Appointment
)を経由して、他方のモデル(Patient
)の複数のインスタンスと関連付けられることを可能にします。
上の関連付けに対応するマイグレーションは以下のような感じになります。
class CreateAppointments < ActiveRecord::Migration[7.2] def change create_table :physicians do |t| t.string :name t.timestamps end create_table :patients do |t| t.string :name t.timestamps end create_table :appointments do |t| t.belongs_to :physician t.belongs_to :patient t.datetime :appointment_date t.timestamps end end end
このマイグレーションでは、physicians
テーブルとpatients
テーブルが作成され、どちらのテーブルにもname
カラムがあります。joinテーブルとして機能するappointments
テーブルはphysician_id
カラムとpatient_id
カラムを持つ形で作成され、physicians
とpatients
の間に多対多の関係を確立します。
また、以下のようにhas_many :through
リレーションシップのjoinテーブルに複合主キーを利用することも検討できます。
class CreateAppointments < ActiveRecord::Migration[7.2] def change # ... create_table :appointments, primary_key: [:physician_id, :patient_id] do |t| t.belongs_to :physician t.belongs_to :patient t.datetime :appointment_date t.timestamps end end end
has_many :through
関連付けにあるjoinモデルのコレクションは、標準のhas_many
関連付けメソッド経由で管理できます。たとえば、患者のリスト(patients
)を以下のように医師(physician
)に割り当てたとします。
physician.patients = patients
Railsは自動的に、以前はその医師に関連付けられていなかった患者たちが新しいリスト内に含まれていれば、新しいjoinモデルを作成します。さらに、以前はその医師に関連付けられていた患者が新しいリストに含まれていなければ、そのjoinレコードは自動的に削除されます。 このようにしてjoinモデルの作成と削除が処理されるため、多対多リレーションシップの管理がシンプルになります。
joinモデルの自動削除は即座に行われ、destroy
コールバックは発生しないので注意が必要です。詳しくはActive Recordコールバックガイドを参照してください。
has_many :through
関連付けは、ネストしたhas_many
関連付けを介して「ショートカット」を設定する場合にも便利です。このショートカットは、関連するレコードのコレクションに、中間の関連付けを介してアクセスする必要がある場合に特に有用です。
たとえば、あるドキュメントに多くの節(section)があり、1つの節の下に多くの段落(paragraph)がある状態で、個別の節をたどらずに、ドキュメントにあるすべての段落のコレクションだけが欲しいとします。
これは、以下のようにhas_many :through
関連付けで設定できます。
class Document < ApplicationRecord has_many :sections has_many :paragraphs, through: :sections end class Section < ApplicationRecord belongs_to :document has_many :paragraphs end class Paragraph < ApplicationRecord belongs_to :section end
through: :sections
を指定することで、Railsは以下の文を理解できるようになります。
@document.paragraphs
has_many :through
関連付けを設定しないと、ドキュメント内の段落を取得するために以下のような煩雑な操作が必要になります。
paragraphs = [] @document.sections.each do |section| paragraphs.concat(section.paragraphs) end
has_one :through
関連付けhas_one :through
関連付けは、他方のモデルに対して「1対1」のリレーションシップを設定します。この関連付けは、2つのモデルの間に「第3のモデル」(joinモデル)が介在し、それを経由(through)して相手モデルの1個のインスタンスとマッチします。
たとえば、個別の供給元(supplier)が1個のアカウント(account)を持ち、さらに1個のアカウントが1個のアカウント履歴に関連付けられる場合、Supplier
モデルは以下のような感じになります。
class Supplier < ApplicationRecord has_one :account has_one :account_history, through: :account end class Account < ApplicationRecord belongs_to :supplier has_one :account_history end class AccountHistory < ApplicationRecord belongs_to :account end
上のセットアップによって、supplier
はaccount
を経由して直接account_history
にアクセス可能になります。
上の関連付けに対応するマイグレーションは以下のような感じになります。
class CreateAccountHistories < ActiveRecord::Migration[7.2] def change create_table :suppliers do |t| t.string :name t.timestamps end create_table :accounts do |t| t.belongs_to :supplier t.string :account_number t.timestamps end create_table :account_histories do |t| t.belongs_to :account t.integer :credit_rating t.timestamps end end end
has_and_belongs_to_many
関連付けhas_and_belongs_to_many
関連付けは、他方のモデルと「多対多」のリレーションシップを作成しますが、through:
を指定した場合と異なり、第3のモデル(joinモデル)が介在しません。この関連付けは、それを宣言しているモデルの各インスタンスが、他方のモデルのインスタンスを0個以上参照することを示します。
たとえば、Assembly
(完成品)モデルとPart
(部品)モデルを持つアプリケーションを考えてみましょう。個別の完成品には多数の部品が含まれ、個別の部品は多くの完成品で利用できます。このモデルは次のようにセットアップできます。
class Assembly < ApplicationRecord has_and_belongs_to_many :parts end class Part < ApplicationRecord has_and_belongs_to_many :assemblies end
has_and_belongs_to_many
は介在モデルが不要ですが、関係する2つのモデル間の多対多リレーションシップを確立するためのテーブルは別途必要です。この介在テーブルは、2つのモデルのインスタンス間の関連付けをマッピングし、関連するデータを保存する役割があります。この介在テーブルは、関連するレコード間のリレーションシップを管理することだけが目的なので、テーブルには必ずしも主キーは必要ありません。
上の関連付けに対応するマイグレーションは以下のような感じになります。
class CreateAssembliesAndParts < ActiveRecord::Migration[7.2] def change create_table :assemblies do |t| t.string :name t.timestamps end create_table :parts do |t| t.string :part_number t.timestamps end # `assemblies`テーブルと`parts`テーブル間の多対多リレーションシップを # 確立するためのjoinテーブルを作成する # `id: false`は、このテーブルには主キーが不要であることを指定する create_table :assemblies_parts, id: false do |t| # joinテーブルを`assemblies`テーブルと`parts`テーブルに # リンクする外部キーを追加する t.belongs_to :assembly t.belongs_to :part end end end
has_and_belongs_to_many
関連付けは、他方のモデルとの多対多の関係を作成します。データベースの観点では、これは各クラスを参照する外部キーを含む中間のjoinテーブルを介して、2つのクラスを関連付けることを指します。
has_and_belongs_to_many
関連付けのjoinテーブルに2つの外部キー以外のカラムが存在すると、これらのカラムはその関連付けを介して取得されるレコードに「属性」として追加されます。追加の属性とともに返されるレコードは、常に読み取り専用になります(Railsはこのような属性への変更を保存できません)。
has_and_belongs_to_many
関連付けのjoinテーブルにこのような属性を追加して利用することは非推奨です。多対多リレーションシップで2つのモデルを結合するテーブルでこのような複雑な振る舞いを必要とする場合は、has_and_belongs_to_many
関連付けではなくhas_many :through
関連付けを使うべきです。
has_and_belongs_to_many
で追加されるメソッドhas_and_belongs_to_many
関連付けを宣言したクラスでは、さまざまなメソッドが自動的に利用できるようになります。以下はその一部です。
collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {})
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload
本ガイドでは、よく使われるメソッドの一部を取り上げていますが、完全なリストについてはActive Recordの関連付けAPIを参照してください。
上のメソッド名のcollection
の部分はプレースホルダなので、has_and_belongs_to_many
の第1引数として渡されるシンボルで読み替えてください。
また、collection_singular
の部分はコレクション名を単数形にして読み替えてください。
たとえば以下の宣言があるとします。
class Part < ApplicationRecord has_and_belongs_to_many :assemblies end
これにより、Part
モデルで以下のメソッドが使えるようになります。
assemblies assemblies<<(object, ...) assemblies.delete(object, ...) assemblies.destroy(object, ...) assemblies=(objects) assembly_ids assembly_ids=(ids) assemblies.clear assemblies.empty? assemblies.size assemblies.find(...) assemblies.where(...) assemblies.exists?(...) assemblies.build(attributes = {}, ...) assemblies.create(attributes = {}) assemblies.create!(attributes = {}) assemblies.reload
collection
メソッドは、関連付けられたすべてのオブジェクトのリレーションを返します。関連付けられたオブジェクトがない場合は、空のリレーションを1つ返します。
@assemblies = @part.assemblies
collection<<
メソッドは、joinテーブル上でレコードを作成し、それによって1個以上のオブジェクトをコレクションに追加します。
@part.assemblies << @assembly1
このメソッドはcollection.concat
とcollection.push
のエイリアスです。
collection.delete
メソッドは、joinテーブル内のレコードを削除する形で、コレクションから1個以上のオブジェクトを取り除きます。オブジェクトはdestroyされません。
@part.assemblies.delete(@assembly1)
collection.destroy
メソッドは、joinテーブル内のレコードを削除する形で、コレクションから1個以上のオブジェクトを取り除きます。オブジェクトはdestroyされません。
@part.assemblies.destroy(@assembly1)
collection.clear
メソッドは、joinテーブル上のレコードを削除する形で、すべてのオブジェクトをコレクションから取り除きます。オブジェクトはdestroyされません。
collection=
メソッドは、削除や追加を適宜実行することで、渡したオブジェクトだけがそのコレクションに含まれるようにします。変更の結果はデータベースで永続化されます。
collection_singular_ids
メソッドは、削除や追加を適宜実行することで、指定した主キーの値を持つオブジェクトだけがコレクションに含まれるようにします。変更の結果はデータベースで永続化されます。
collection_singular_ids
メソッドは、そのコレクションに含まれるオブジェクトのidを配列にしたものを返します。
@assembly_ids = @part.assembly_ids
collection.empty?
メソッドは、関連付けられたオブジェクトがコレクションに存在しない場合にtrue
を返します。
<% if @part.assemblies.empty? %> この部品はどの完成品にも使われていません <% end %>
collection.size
メソッドは、コレクションに含まれるオブジェクトの個数を返します。
@assembly_count = @part.assemblies.size
collection.find
メソッドは、コレクションに含まれるオブジェクトを検索します。
@assembly = @part.assemblies.find(1)
collection.where
メソッドは、コレクションに含まれているオブジェクトを指定された条件に基いて検索します。このメソッドではオブジェクトは遅延読み込み(lazy load)されるので、オブジェクトに実際にアクセスするときだけデータベースへのクエリが発生します。
@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)
collection.exists?
メソッドは、指定された条件に合うオブジェクトがコレクションのテーブル内に存在するかどうかをチェックします。
collection.build
メソッドは、関連付けされた型のオブジェクトまたはオブジェクトの配列を返します。返されるオブジェクトは、渡された属性に基いてインスタンス化され、joinテーブルを経由するリンクが作成されます。関連付けられたオブジェクトはまだ保存されないことにご注意ください。
@assembly = @part.assemblies.build({ assembly_name: "Transmission housing" })
collection.create
メソッドは、関連付けされた型の新しいオブジェクトを返します。このオブジェクトは、渡された属性を用いてインスタンス化され、そのオブジェクトのjoinテーブルを介してリンクが作成されます。そして、関連付けられたモデルで指定されているバリデーションがすべてパスすると、この関連付けられたオブジェクトは保存されます。
@assembly = @part.assemblies.create({ assembly_name: "Transmission housing" })
collection.create!
は上のcollection.create
と同じですが、レコードが無効な場合にActiveRecord::RecordInvalid
がraiseされる点が異なります。
collection.reload
メソッドは、関連付けられたすべてのオブジェクトのリレーションを1つ返し、データベースを強制的に読み出します。関連付けられたオブジェクトがない場合は、空のリレーションを1つ返します。
@assemblies = @part.assemblies.reload
has_and_belongs_to_many
関連付けにオブジェクトを割り当てると、外部キーを更新するためにそのオブジェクトは自動的に保存されます。1つの文で複数のオブジェクトを割り当てると、それらはすべて保存されます。
関連付けられているオブジェクトのどれかがバリデーションエラーで保存に失敗すると、false
を返し、割り当てはキャンセルされます。
親オブジェクト(has_and_belongs_to_many
関連付けを宣言している側のオブジェクト)が保存されない場合(つまりnew_record?
がtrue
を返す場合)、子オブジェクトを追加したときに保存されません。親オブジェクトが保存されると、関連付けられていたオブジェクトのうち保存されていなかったメンバーはすべて保存されます。
has_and_belongs_to_many
関連付けにオブジェクトを割り当てて、しかもそのオブジェクトをsave
したくない場合、collection.build
メソッドをお使いください。
belongs_to
とhas_one
のどちらを選ぶか2つのモデルの間に1対1のリレーションシップを設定したい場合は、一方のモデルにbelongs_to
関連付けを追加し、他方のモデルにhas_one
関連付けを追加できます。どちらの関連付けをどちらのモデルに置けばよいでしょうか。
区別の決め手となるのは外部キー(foreign key)をどちらのモデルに置くかです(外部キーは、belongs_to
関連付けを追加したモデルのテーブルに追加します)が、適切な関連付けを決めるためには、もう少しデータの実際の意味についても考えてみる必要があります。
belongs_to
: この関連付けは、それを宣言する現在のモデルに外部キーが含まれていることと、現在のモデルがリレーションシップにおける「子」であることを意味します。この関連付けで他方のモデルを参照すると、このモデルの各インスタンスが、他方のモデルの「1個の」インスタンスに紐づけられることを示します。
has_one
: この関連付けは、それを宣言する現在のモデルがリレーションシップにおける「親」であることと、他方のモデルのインスタンスを「1個」所有していることを意味します。
たとえば、供給元(suppliers)とそのアカウント(accounts)があるシナリオを考えてみましょう。アカウントが供給元を持っている/所有していると考えるよりも、供給元がアカウントを持っている/所有している (供給元が親になる) と考える方が自然です。したがって、正しい関連付けは次のようになります。
Railsでは、これらの関連付けを以下のように定義できます。
class Supplier < ApplicationRecord has_one :account end class Account < ApplicationRecord belongs_to :supplier end
これらの関連付けを実装するには、対応するデータベーステーブルを作成して、外部キーを設定する必要があります。 マイグレーションの例は以下のような感じになります。
class CreateSuppliers < ActiveRecord::Migration[7.2] def change create_table :suppliers do |t| t.string :name t.timestamps end create_table :accounts do |t| t.belongs_to :supplier_id t.string :account_number t.timestamps end add_index :accounts, :supplier_id end end
「外部キーは、belongs_to
関連付けを宣言しているクラスのテーブルに配置する」と覚えておきましょう。この場合は、account
テーブルの方に外部キーを配置します。
has_many :through
とhas_and_belongs_to_many
のどちらを選ぶかRailsでは、モデル間の多対多リレーションシップを宣言するのにhas_many :through
関連付けとhas_and_belongs_to_many
関連付けという2とおりの方法が利用できます。2つの方法の違いとユースケースを理解することで、アプリケーションのニーズに最適な方法を決められるようになります。
has_many :through
関連付けは、中間モデル(joinモデル)を介して多対多リレーションシップを設定します。
このアプローチは柔軟性が高く、joinモデルに「バリデーション」「コールバック」「追加の属性」も追加できます。joinテーブルにはprimary_key
(複合主キー)が必ず必要です。
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
以下に該当する場合は、has_many :through
関連付けを使います。
もうひとつのhas_and_belongs_to_many
関連付けは、中間モデルを必要とせずに、2つのモデル間に多対多リレーションシップを直接作成できます。
この方法は手軽で、joinテーブルに属性や振る舞いを追加する必要がないシンプルな関連付けに適しています。その代わり、has_and_belongs_to_many
関連付けで作成するjoinテーブルには、主キーを含めないようにする必要があります。
class Assembly < ApplicationRecord has_and_belongs_to_many :parts end class Part < ApplicationRecord has_and_belongs_to_many :assemblies end
以下に該当する場合は、has_and_belongs_to_many
関連付けを使います。
ポリモーフィック関連付け(polymorphic association)は、関連付けのやや高度な応用です。Railsのポリモーフィック関連付けを使うと、ある1つのモデルが他の複数のモデルに属していることを、1つの関連付けだけで表現できます。ポリモーフィック関連付けは、あるモデルを種類の異なる複数のモデルに紐づける必要がある場合に特に便利です。
たとえば、Picture
(写真)モデルがあり、このモデルをEmployee
(従業員)モデルとProduct
(製品)モデルの両方に従属させたいとします。この場合は以下のように宣言します。
class Picture < ApplicationRecord belongs_to :imageable, polymorphic: true end class Employee < ApplicationRecord has_many :pictures, as: :imageable end class Product < ApplicationRecord has_many :pictures, as: :imageable end
上のimageable
は、関連付けを表すために選んだ名前です。これは、Picture
モデルと、Employee
やProduct
などの他のモデルとの間のポリモーフィック関連付けを表すシンボル名です。
ここで重要な点は、ポリモーフィック関連付けを正しく確立するためには、関連付けられるすべてのモデルで必ず同じ名前(imageable
)に統一することです。
Picture
モデルでbelongs_to :imageable, polymorphic: true
を宣言すると、「Picture
はこの関連付けを通じて任意のモデル(Employee
やProduct
など)に属することが可能である」と宣言したことになります。
ポリモーフィックなbelongs_to
宣言は、他の任意のモデルでも利用できるインターフェイスを設定するものとみなせます。これにより、たとえば@employee.pictures
と書くだけで、Employee
モデルのインスタンスから写真のコレクションを取得できます。同様に、@product.pictures
と書くだけで、Product
モデルのインスタンスから写真のコレクションを取得できます。
さらに、Picture
モデルのインスタンスがある場合は、@picture.imageable
を経由してその親モデル(Employee
またはProduct
)を取得できます。
ポリモーフィック関連付けを手動でセットアップする場合は、以下のようにモデルで外部キーカラム(imageable_id
)とtypeカラム(imageable_type
)の両方を宣言する必要があります。
class CreatePictures < ActiveRecord::Migration[7.2] def change create_table :pictures do |t| t.string :name t.bigint :imageable_id t.string :imageable_type t.timestamps end add_index :pictures, [:imageable_type, :imageable_id] end end
上の例では、imageable_id
はEmployee
やProduct
のIDであり、imageable_type
は関連付けられるモデルのクラス名(つまりEmployee
やProduct
)になります。
ポリモーフィック関連付けを手動で作成することも一応可能ですが、それよりも以下のようにt.references
(またはそのエイリアスt.belong_to
)を用いてpolymorphic: true
を指定する方がオススメです。これにより、関連付けがポリモーフィックであることがRailsに認識され、外部キーとtypeカラムが両方ともテーブルに自動的に追加されます。
class CreatePictures < ActiveRecord::Migration[7.2] def change create_table :pictures do |t| t.string :name t.belongs_to :imageable, polymorphic: true t.timestamps end end end
ポリモーフィック関連付けは、クラス名がデータベースに保存されることに依存しているため、保存されているクラス名がRubyコードで使われるクラス名とずれないよう、常に同期させる必要があります。クラス名を変更する場合は、ポリモーフィックのtypeカラムのデータも必ず更新してください。
たとえば、クラス名をProduct
からItem
に変更する場合は、マイグレーションスクリプトを実行してpictures
テーブル(または影響を受けるテーブル)のimageable_type
カラムの値を新しいクラス名で更新する必要があります。さらに、変更を反映するために、アプリケーションコード全体でもその他のクラス名への参照を更新する必要があります。
Railsは多くの場合、関連付けられるモデル間の主キーと外部キーのリレーションシップを推測できますが、複合主キーを扱う場合、明示的に指示されない限り、複合キーの一部のみ(多くの場合idカラム)がデフォルトで利用されます。
Railsモデルで複合主キーを利用していて、関連付けを適切に処理する必要がある場合は、複合主キーガイドの複合主キーを持つモデルの関連付けセクションを参照してください。このセクションでは、必要に応じて複合外部キーを指定する方法など、Railsで複合主キーとの関連付けを設定・利用する方法について包括的なガイダンスを提供します。
self-joining(自己結合)は通常のjoinですが、テーブルがそれ自身とjoinされます。これは、1個のテーブル内に階層関係がある場合に便利です。一般的な例としては、従業員管理システムがあります。従業員(employee)にはマネージャ(manager)がいて、そのマネージャも従業員の1人です。
ある従業員が他の従業員のマネージャーになる可能性がある組織を考えてみましょう。単一のemployees
テーブルで、このリレーションを追跡できるようにしたいします。
Railsモデルで、このリレーションシップを反映するEmployee
クラスを定義します。
class Employee < ApplicationRecord # 1人の従業員が複数の部下を持つ可能性がある has_many :subordinates, class_name: "Employee", foreign_key: "manager_id" # 1人の従業員のマネージャは1人だけ belongs_to :manager, class_name: "Employee", optional: true end
has_many :subordinates
関連付けは、1人の従業員が複数の部下を持つ1対多リレーションシップを設定します。このとき、関連するモデルにEmployee
(class_name: "Employee"
)を指定し、マネージャを特定するための外部キーにmanager_id
を指定します。
belongs_to :manager
関連付けは、1人の従業員が1人のマネージャを持つ1対1リレーションシップを設定します。こちらにもEmployee
モデルを指定します。
このリレーションシップをサポートするには、以下のようなマイグレーションでemployees
テーブルにmanager_id
カラムを追加する必要があります。このカラムは、別の従業員(マネージャ)のid
を参照します。
class CreateEmployees < ActiveRecord::Migration[7.2] def change create_table :employees do |t| # belongs_to参照をマネージャに追加する(従業員でもある) t.belongs_to :manager, foreign_key: { to_table: :employees } t.timestamps end end end
t.belongs_to :manager
は、employees
テーブルにmanager_id
カラムを追加します。foreign_key: { to_table: :employees }
は、manager_id
カラムが、employees
テーブルのid
カラムを参照するようにします。foreign_key
に渡しているto_table
オプションなどについては、APIドキュメントSchemaStatements#add_reference
に解説があります。
このセットアップにより、Railsアプリケーションで従業員の部下やマネージャーに手軽にアクセスできます。
ある従業員の部下を取得するには、以下のようにします。
employee = Employee.find(1) subordinates = employee.subordinates
ある従業員のマネージャを取得するには、以下のようにします。
manager = employee.manager
単一テーブル継承 (STI: Single Table Inheritance) は、複数のモデルを単一のデータベーステーブルに収納できるRailsのパターンです。これは、さまざまなエンティティに共通の属性や振る舞いを持たせつつ、エンティティ固有の振る舞いも持たせたい場合に便利です。
たとえば、Car
、Motorcycle
、Bicycle
というモデルがあるとします。これらのモデルはcolor
やprice
などのフィールドを共有しますが、モデルごとに固有の振る舞いもあるとします。さらに、モデルごとに独自のコントローラーもあるとします。
最初に、共有フィールドを持つVehicle
モデルを生成します。
$ bin/rails generate model vehicle type:string color:string price:decimal{10.2}
STIで重要なのは、このtype
フィールドです(ここにCar
、Motorcycle
、Bicycle
などのモデル名が保存されます)。STIでは、同じテーブルに保存されるさまざまなモデルを区別するために、このフィールドが不可欠です。
次に、Vehicle
を継承するCar
、Motorcycle
、Bicycle
モデルをそれぞれ生成します。これらのモデルは独自のテーブルを持たない代わりに、vehicles
テーブルを利用します。
たとえばCar
モデルは以下のように生成します。
$ bin/rails generate model car --parent=Vehicle
ここで--parent=親モデル
オプションを使うことで、指定した親モデルを継承できます。また、テーブルは既に存在しているので、マイグレーションファイルは生成されません。
Vehicle
を継承したCar
モデルは次のようになります。
class Car < Vehicle end
これで、Vehicle
モデルに追加されたすべての振る舞いが、Car
モデルにも追加されるようになります。関連付けやpublicメソッドなども同様に追加されます。
この状態で新しく作成したCar
を保存すると、type
フィールドに"Car"を割り当てたデータがvehicles
テーブルに追加されます。
Motorcycle
モデルとBicycle
モデルについても、Car
モデルと同様の作業を繰り返します。
以下を実行してCar
モデルのレコードを作成します。
Car.create(color: 'Red', price: 10000)
実際に生成されるSQLは次のようになります。
INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000)
Car
のレコードを取得するクエリを送信すると、vehicles
テーブル内のCar
に該当するレコードだけが検索されます。
Car.all
実際のクエリは次のようになります。
SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car')
STIでは、子モデルに特定の振る舞いやメソッドを追加できます。たとえば、Car
モデルにメソッドを追加するには以下のように書きます。
class Car < Vehicle def honk 'Beep Beep' end end
これで、Car
モデルのインスタンスでのみhonk
メソッドを呼び出せるようになります。
car = Car.first car.honk # => 'Beep Beep'
STIの子モデルには、独自のコントローラも追加できます。たとえば以下のCarsController
を追加できます。
# app/controllers/cars_controller.rb class CarsController < ApplicationController def index @cars = Car.all end end
レガシーデータベースで作業する場合などで、継承カラム名をオーバーライドする必要が生じることがあります。これは、inheritance_column
メソッドで実現できます。
# スキーマ: vehicles[ id, kind, created_at, updated_at ] class Vehicle < ApplicationRecord self.inheritance_column = "kind" end class Car < Vehicle end Car.create # => #<Car kind: "Car", color: "Red", price: 10000>
このセットアップにすると、モデルのtype(モデル名)をkind
カラムに保存するように変更され、STIがカスタムカラム名で正しく機能できるようになります。
レガシーデータベースで作業する場合などで、単一テーブル継承を完全に無効にする必要が生じることがあります(そうしないとActiveRecord::SubclassNotFound
が発生する)。
これは、inheritance_column
をnil
に設定することで実現できます。
# スキーマ: vehicles[ id, type, created_at, updated_at ] class Vehicle < ApplicationRecord self.inheritance_column = nil end Vehicle.create!(type: "Car") # => #<Vehicle type: "Car", color: "Red", price: 10000>
このセットアップにすると、type
カラムが通常の属性として扱われるように変更され、STIで使われないようになります。これは、STIパターンに従っていないレガシースキーマで作業しなければならない場合に便利です。
これらの調整機能によって、Railsを既存のデータベースと統合する場合や、モデルに特定のカスタマイズが必要な場合に、柔軟な対応が可能になります。
単一テーブル継承(STI)は、サブクラス同士(およびその属性)にほとんど違いがない場合に最適ですが、すべてのサブクラスのすべての属性が1個のテーブルに収納されることになります。
この方法の欠点は、サブクラス固有の属性を(他のサブクラスで使われていない属性であっても)1個のテーブルに含めるため、テーブルが肥大化する可能性があることです。これについては、後述のDelegated Typesで解決できることがあります。
さらに、ポリモーフィック関連付けを使っている場合は、1個のモデルがtypeとIDを介して他の複数のモデルに属している可能性があるため、関連付けロジックがさまざまなモデルtypeを正しく処理するための参照整合性を維持する処理が複雑になる可能性があります。
最後に、データ整合性チェックやバリデーションがサブクラスごとに異なる場合は、特に外部キー制約を設定するときに、Railsやデータベースでデータ整合性チェックやバリデーションが正しく処理されるようにしておく必要があります。
Delegated types(委譲型)は、単一テーブル継承(STI)によるテーブル肥大化の問題を、delegated_type
で解決します。このアプローチにより、共有属性をスーパークラスのテーブルに保存し、サブクラス固有の属性を別のテーブルに保存できるようになります。
Delegated typesを使うためには、データを以下のようにモデリングする必要があります。
これにより、単一のテーブルで、すべてのサブクラス間で不必要に共有される属性を定義する必要がなくなります。
上記の例にDelegated typesを適用するには、モデルを再生成する必要があります。
まず、スーパークラスとして機能するベースEntry
モデルを生成しましょう。
$ bin/rails generate model entry entryable_type:string entryable_id:integer
次に、委譲で使うMessage
モデルとComment
モデルを新しく生成します。
$ bin/rails generate model message subject:string body:string $ bin/rails generate model comment content:string
ジェネレータ実行後のモデルは以下のようになります。
# スキーマ: entries[ id, entryable_type, entryable_id, created_at, updated_at ] class Entry < ApplicationRecord end # スキーマ: messages[ id, subject, body, created_at, updated_at ] class Message < ApplicationRecord end # スキーマ: comments[ id, content, created_at, updated_at ] class Comment < ApplicationRecord end
delegated_type
を宣言するまず、Entry
スーパークラスでdelegated_type
を宣言します。
class Entry < ApplicationRecord delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy end
このentryable
パラメータは、委譲で使うフィールドを指定し、委譲クラスとしてMessage
型とComment
型を含みます。entryable_type
には委譲サブクラス名が保存され、entryable_id
には委譲サブクラスのレコードIDが保存されます。
Entryable
モジュールを定義する次に、それらのdelegated typesを実装するモジュールを定義する必要があります。このモジュールは、as: :entryable
パラメータをhas_one
関連付けに宣言することで定義します。
module Entryable extend ActiveSupport::Concern included do has_one :entry, as: :entryable, touch: true end end
続いて、作成したモジュールをサブクラスにinclude
します。
class Message < ApplicationRecord include Entryable end class Comment < ApplicationRecord include Entryable end
定義が完了すると、Entry
デリゲーターは以下のメソッドを提供するようになります。
メソッド | 戻り値 |
---|---|
Entry.entryable_types |
["Message", "Comment"] |
Entry#entryable_class |
Message または Comment |
Entry#entryable_name |
"message" または "comment" |
Entry.messages |
Entry.where(entryable_type: "Message") |
Entry#message? |
entryable_type == "Message" の場合trueを返す |
Entry#message |
entryable_type == "Message" の場合はメッセージレコードを返し、それ以外の場合は nil を返す |
Entry#message_id |
entryable_type == "Message" の場合はentryable_id を返し、それ以外の場合はnil を返す |
Entry.comments |
Entry.where(entryable_type: "Comment") |
Entry#comment? |
entryable_type == "Comment" の場合はtrueを返す |
Entry#comment |
entryable_type == "Comment" の場合はコメント・メッセージを返し、それ以外の場合はnil を返す |
Entry#comment_id |
entryable_type == "Comment" の場合はentryable_id を返し、それ以外の場合はnil を返す |
新しいEntry
オブジェクトを作成する際に、entryable
サブクラスを同時に指定できます。
Entry.create! entryable: Message.new(subject: "hello!")
Entry
デリゲータを拡張してdelegates
を定義し、サブクラスに対してポリモーフィズムを使用することで、さらに拡張できます。
たとえば、Entry
のtitle
メソッドをそのサブクラスに委譲するには以下のようにします。
class Entry < ApplicationRecord delegated_type :entryable, types: %w[ Message Comment ] delegate :title, to: :entryable end class Message < ApplicationRecord include Entryable def title subject end end class Comment < ApplicationRecord include Entryable def title content.truncate(20) end end
このセットアップによって、Entry
はtitle
メソッドをサブクラスに委譲できるようになります。Message
モデルはsubject
メソッドを利用し、Comment
モデルはcontent
メソッドの結果をtruncate
したものを利用できるようになります。
RailsアプリケーションでActive Recordの関連付けを効果的に使うためには、以下について知っておく必要があります。
すべての関連付けメソッドは、キャッシュを中心に構築されています。最後に実行したクエリの結果はキャッシュに保持され、次回以降の操作で利用されます。このキャッシュは、以下のようにメソッド間でも共有される点にご注意ください。
# データベースからbooksを取得する author.books.load # booksのキャッシュコピーが使われる author.books.size # booksのキャッシュコピーが使われる author.books.empty?
このauthor.books
を実行しても、それだけではデータベースからすぐにデータを読み込みません。代わりに、実際にデータを使おうとしたとき(例: each
、size
、empty?
などの「データを必要とするメソッド」を呼び出したとき)に実行されるクエリを単にセットアップします。
データを利用する他のメソッドを呼び出す前にauthor.books.load
メソッドを呼び出すと、データベースからデータを読み込むクエリがその場で明示的にトリガーされます。このテクニックは、データが必要であることが事前にわかっていて、関連付けを操作するときにクエリが何度もトリガーされることによるパフォーマンス上の潜在的なオーバーヘッドを回避したい場合に便利です。
しかし、アプリケーションの他の部分によってデータが変更されている可能性があるため、キャッシュを再読み込みしたい場合は、その関連付けでreload
メソッドを呼び出すだけで再読み込みできます。
# データベースからbooksを取得する author.books.load # booksのキャッシュコピーが使われる author.books.size # booksのキャッシュコピーが破棄され、その後データベースから再度読み込まれる author.books.reload.empty?
Ruby on Railsのモデルで関連付けを作成する場合、ActiveRecord::Base
のインスタンスメソッドで既に使われている名前を関連付け名で使わないようにすることが重要です。既存のメソッドと競合する名前で関連付けを作成すると、ベースメソッドがオーバーライドされて機能に問題が発生するなど、意図しない結果につながる可能性があります。たとえば、関連付け名にattributes
やconnection
などを使うと問題が発生します。
関連付けは非常に便利です。関連付けは、モデル間のリレーションシップを定義する役割を担いますが、データベーススキーマの更新は行いません。つまり、関連付けとデータベーススキーマを常に一致させる責任はアプリケーション開発者にあります。そのために、主に以下の2つのタスクについては手作業が必要です。
belongs_to
関連付けを使う場合は、外部キーを作成する必要があります。has_and_belongs_to_many
関連付けを使う場合は、適切なjoinテーブルを作成する必要があります。has_many :through
とhas_and_belongs_to_many
の使い分けについて詳しくは、has_many :through
とhas_and_belongs_to_many
のどちらを選ぶかを参照してください。
belongs_to
関連付けに対応する外部キーを作成するbelongs_to
関連付け関連付けを宣言するときは、対応する外部キーも作成する必要があります。以下のモデルを例にとります。
class Book < ApplicationRecord belongs_to :author end
上の宣言は、以下のbooks
テーブルの対応する外部キーカラムと整合していなければなりません。テーブルを作成した直後のマイグレーションは、以下のような感じになります。
class CreateBooks < ActiveRecord::Migration[7.2] def change create_table :books do |t| t.datetime :published_at t.string :book_number t.references :author end end end
一方、既存のテーブルに外部キーを設定するときのマイグレーションは、以下のような感じになります。
class AddAuthorToBooks < ActiveRecord::Migration[7.2] def change add_reference :books, :author end end
has_and_belongs_to_many
関連付けに対応するjoinテーブルを作成するhas_and_belongs_to_many
関連付けを作成した場合は、それに対応するjoinテーブルも明示的に作成する必要があります。joinテーブルの名前が:join_table
オプションで明示的に指定されていない場合、Active Recordは2つのクラス名をABC順に結合して、joinテーブル名を作成します。
たとえばAuthor
モデルとBook
モデルを結合する場合、'a'は辞書の並び順で'b'より先に出現するので、"authors_books"というデフォルトのjoinテーブル名が使われます。
どのような名前であっても、適切なマイグレーションを実行してjoinテーブルを手動で生成する必要があります。以下の関連付けを例にとって考えてみましょう。
class Assembly < ApplicationRecord has_and_belongs_to_many :parts end class Part < ApplicationRecord has_and_belongs_to_many :assemblies end
この関連付けに正しく対応するassemblies_parts
テーブルは、以下のようなマイグレーションで作成する必要があります。
$ bin/rails generate migration CreateAssembliesPartsJoinTable assemblies parts
生成されたマイグレーションファイルを以下のように編集します。このとき、テーブルに主キーを設定してはいけません。
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[7.2] 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
このjoinテーブルはモデルを表すためのものではないので、create_table
にid: false
オプションを指定します。
モデルのIDが破損する、IDの競合で例外が発生するなど、has_and_belongs_to_many
関連付けの動作が怪しい場合は、マイグレーション作成時にid: false
オプションの設定を忘れていないかどうか再度確認してみてください。
なお、以下のようにcreate_join_table
メソッドを使えば、同じことをもっとシンプルに書けます。
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[7.2] def change create_join_table :assemblies, :parts do |t| t.index :assembly_id t.index :part_id end end end
create_join_table
メソッドについて詳しくは、Active Recordマイグレーションガイドを参照してください。
has_many :through
に対応するjoinテーブルを作成するhas_many :through
関連付けとhas_and_belongs_to_many
関連付けでjoinテーブルを作成する場合の、スキーマ実装方法の主な違いは、has_many :through
のjoinテーブルにはid
が必須であることです。
class CreateAppointments < ActiveRecord::Migration[7.2] def change create_table :appointments do |t| t.belongs_to :physician t.belongs_to :patient t.datetime :appointment_date t.timestamps end end end
関連付けは、デフォルトでは現在のモジュールのスコープ内にあるオブジェクトだけを探索します。この機能は、特に以下のようにモジュール内でActive Recordモデルを宣言するときに、関連付けのスコープが適切に維持される点が有用です。
module MyApplication module Business class Supplier < ApplicationRecord has_one :account end class Account < ApplicationRecord belongs_to :supplier end end end
上の例では、Supplier
クラスとAccount
クラスがどちらも同じモジュール(MyApplication::Business
)内で定義されています。
このようにコードを編成すれば、すべての関連付けにスコープを明示的に追加しなくても、以下のようにモデルをスコープに基づいてフォルダで構造化できます。
# app/models/my_application/business/supplier.rb module MyApplication module Business class Supplier < ApplicationRecord has_one :account end end end
# app/models/my_application/business/account.rb module MyApplication module Business class Account < ApplicationRecord belongs_to :supplier end end end
モデルのスコープ設定はコードを整理するときに便利ですが、モデルのスコープ設定はデータベースのテーブル名の命名規則を変更しないことに注意が必要です。
たとえば、MyApplication::Business::Supplier
というモデルがある場合は、データベーステーブル名も命名規則に沿ったmy_application_business_suppliers
にしなければなりません。
ただし、以下のようにSupplier
とAccount
モデルが異なるスコープで定義されている場合、この関連付けはデフォルトでは機能しません。
module MyApplication module Business class Supplier < ApplicationRecord has_one :account end end module Billing class Account < ApplicationRecord belongs_to :supplier end end end
あるモデルを別の名前空間にあるモデルと関連付けるには、関連付けの宣言で以下のように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
class_name
オプションを明示的に宣言することで、名前空間が異なるモデル間で関連付けを作成し、モジュールのスコープに関係なく正しいモデルがリンクされるようになります。
Railsでは、モデル間の関連付けを双方向(bi-directional)に設定するのが一般的です。つまり、関連付けは2つのモデルの両方で宣言する必要があります。以下の例を考えてみましょう。
class Author < ApplicationRecord has_many :books end class Book < ApplicationRecord belongs_to :author end
Active Recordは、関連付け名に基づいて、2つのモデルが双方向の関連を共有していることを自動的に認識しようとします。この情報によって、Active Recordは以下を行えるようになります。
既に読み込み済みのデータに対する不要なクエリを防ぐ
irb> author = Author.first irb> author.books.all? do |book| irb> book.author.equal?(author) # 追加クエリはここで実行されない irb> end => true
データの不整合を防ぐ
読み込むAuthor
オブジェクトのコピーは1個だけなので、不整合が発生しにくくなります。
irb> author = Author.first irb> book = author.books.first irb> author.name == book.writer.name => true irb> author.name = "Changed Name" irb> author.name == book.writer.name => true
関連付けが自動保存されるケースが増える
irb> author = Author.new irb> book = author.books.new irb> book.save! irb> book.persisted? => true irb> author.persisted? => true
関連付けのpresence
やabsence
がバリデーションされるケースが増える
irb> book = Book.new irb> book.valid? => false irb> book.errors.full_messages => ["Author must exist"] irb> author = Author.new irb> book = author.books.new irb> book.valid? => true
場合によっては、:foreign_key
や:class_name
などのオプションを用いて関連付けをカスタマイズする必要が生じることがあります。これらのオプションを設定すると、Railsが:through
や:foreign_key
オプションを含む双方向の関連付けを自動的に認識しなくなる可能性があります。
config.active_record.automatic_scope_inversing
をtrue
に設定していない場合、関連付け自体にカスタムスコープを設定したときと同様に、逆方向の関連付けにカスタムスコープを設定したときの自動認識も効かなくなります。
たとえば、カスタム外部キーを含む次のモデル宣言を考えてみましょう。
class Author < ApplicationRecord has_many :books end class Book < ApplicationRecord belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' end
この場合、:foreign_key
オプションが指定されているため、Active Recordは双方向関連付けを自動的に認識しなくなります。これによってアプリケーションで以下が発生する可能性があります。
同じデータに対して不要なクエリを実行する(この例ではN+1クエリが発生する)
irb> author = Author.first irb> author.books.any? do |book| irb> book.writer.equal?(author) # authorクエリがbook 1件ごとに発生する irb> end => false
同じモデルの複数のコピーが参照しているデータが不整合になる
irb> author = Author.first irb> book = author.books.first irb> author.name == book.author.name => true irb> author.name = "Changed Name" irb> author.name == book.author.name => false
関連付けの自動保存が失敗する
irb> author = Author.new irb> book = author.books.new irb> book.save! irb> book.persisted? => true irb> author.persisted? => false
presence
やabsence
バリデーションが失敗する
irb> author = Author.new irb> book = author.books.new irb> book.valid? => false irb> book.errors.full_messages => ["Author must exist"]
このような問題を解決するには、以下のように: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
has_many
関連付けの宣言で:inverse_of
オプションを指定すると、Active Recordが双方向関連付けを認識して、上述の最初の例と同じように動作するようになります。
Railsで使われているインテリジェントなデフォルト設定は、ほとんどの状況で適切に機能しますが、関連付け参照の動作をカスタマイズしたい場合もあります。 このようなカスタマイズは、関連付けを作成するときにオプションブロックを渡すことで実現できます。たとえば、以下の関連付けでは、そうしたオプションが2つ使われています。
class Book < ApplicationRecord belongs_to :author, touch: :books_updated_at, counter_cache: true end
どの関連付けも多数のオプションをサポートしています。詳しくは、ActiveRecord Associations APIで個別の関連付けのOptions
セクションを参照してください。ここからは、よくあるユースケースをいくつか解説します。
:class_name
関連付けの相手となるオブジェクト名を関連付け名から生成できない事情がある場合、:class_name
オプションを用いてモデル名を直接指定できます。
たとえば、書籍(book)が著者(author)に従属しているが、実際の著者のモデル名がPatron
である場合には、以下のように指定します。
class Book < ApplicationRecord belongs_to :author, class_name: "Patron" end
:dependent
:dependent
オプションは、オーナーが破棄されたときに、関連付けられているオブジェクトがどう振る舞うかを制御します。
:destroy
: オブジェクトが破棄されると、関連付けられたオブジェクトに対してdestroy
を呼び出します。
このメソッドは、関連付けられたレコードをデータベースから削除するだけでなく、定義されているコールバック(before_destroy
やafter_destroy
など)も実行します。このオプションは、ログ出力や関連データのクリーンアップなど、削除プロセス中にカスタムロジックを実行する場合に便利です。
:delete
: オブジェクトが破棄されると、destroy
メソッドを呼び出さずに、そのオブジェクトに関連付けられたすべてのオブジェクトをデータベースから直接削除します。
このメソッドは直接削除を実行し、関連付けられたモデル内のコールバックやバリデーションをバイパスするため、より効率的ですが、重要なクリーンアップタスクがスキップされると、データの整合性の問題が発生する可能性があります。delete
メソッドは、レコードを迅速に削除する必要があり、関連付けられたレコードに対して追加のアクションが必要ないことが確実な場合に使うこと。
:destroy_async
: オブジェクトが破棄されると、関連付けられたオブジェクトのdestroy
を呼び出すためのActiveRecord::DestroyAssociationAsyncJob
ジョブをキューに登録します。
このオプションが機能するためには、Active Jobをセットアップしておく必要があります。関連付けの背後にあるデータベースで外部キー制約が設定されている場合は、このオプションを使ってはいけません。外部キー制約の操作は、オーナーを削除するのと同じトランザクション内で発生します。
:nullify
: 外部キーをNULL
に設定します。
ポリモーフィック関連付けでは、ポリモーフィックtype
カラムもNULL
に設定されます。コールバックは実行されません。:restrict_with_exception
: 関連付けられたレコードが存在している場合はActiveRecord::DeleteRestrictionError
例外が発生します:restrict_with_error
: 関連付けられたオブジェクトが存在している場合は、オーナーにエラーが追加されます。このオプションは、他のクラスのhas_many
関連付けに接続されているbelongs_to
関連付けで指定してはいけません。これを行うと、親オブジェクトを破棄したときにその子オブジェクトも破棄され、その子オブジェクトが再び親オブジェクトを破棄しようとして不整合が発生し、データベースに孤立レコードが発生する可能性があります。
データベースのNOT NULL
制約を使っている関連付けでは、:nullify
オプションを指定しないでください。そのような関連付けでは、dependent
を:destroy
に設定することが必須です。さもないと、関連付けられたオブジェクトの外部キーがNULL
に設定されて変更できなくなる可能性があります。
:dependent
オプションは、:through
オプションでは無視されます。:through
オプションを使う場合は、joinモデルにはbelongs_to
関連付けが必要です。削除はjoinレコードのみに影響し、関連付けられたレコードには影響しません。
スコープ付き関連付けでdependent: :destroy
オプションを指定すると、スコープ付きオブジェクトのみが破棄されます。
たとえば、Post
モデルでhas_many :comments, -> { where published: true }, dependent: :destroy
と定義されている場合、postでdestroy
を呼び出すと、published: true
のコメントだけが削除され、削除されたpostを指す外部キーを持つ未公開のコメントは削除されずに残ります。
has_and_belongs_to_many
関連付けでは、:dependent
オプションを直接指定できません。joinテーブルのレコードの削除を管理したい場合は、手動で処理するか、:dependent
オプションをサポートしている柔軟なhas_many :through
関連付けに切り替えましょう。
:foreign_key
Railsの規約では、相手のモデルを指す外部キーを保持しているjoinテーブル上のカラム名には、そのモデル名にサフィックス_id
を追加した関連付け名が使われることを前提とします。
:foreign_key
オプションを使えば、外部キー名を直接指定できます。
class Supplier < ApplicationRecord has_one :account, foreign_key: "supp_id" end
Railsは外部キーカラムを自動的に作成しません。外部キーを使うには、マイグレーションを作成して明示的に定義する必要があります。
:primary_key
Railsの規約では、id
カラムをテーブルの主キーとして使います。
:primary_key
オプションを指定すると、別のカラムを主キーに設定できます。
たとえば、users
テーブルにguid
という主キーがあるとします。そのguid
カラムに、別のtodos
テーブルの外部キーであるuser_id
カラムを使いたい場合は、次のようにprimary_key
を設定します。
class User < ApplicationRecord self.primary_key = 'guid' # 主キーをidからguidに変更する end class Todo < ApplicationRecord belongs_to :user, primary_key: 'guid' # usersテーブル内のguidを参照する end
@user.todos.create
を実行すると、@todo
レコードのuser_id
値が@user
のguid
値に設定されます。
has_and_belongs_to_many
関連付けは、:primary_key
オプションをサポートしていません。代わりにhas_many :through
関連付けとjoinテーブルを使えば、同様の機能を実現できます。これにより柔軟性が高まり、:primary_key
オプションがサポートされます。詳しくは、has_many :through
セクションを参照してください。
:touch
:touch
オプションをtrue
に設定すると、そのオブジェクトがsave
またはdestroy
されたときに、関連付けられたオブジェクトのupdated_at
タイムスタンプやupdated_on
タイムスタンプが常に現在時刻に設定されます。
class Book < ApplicationRecord belongs_to :author, touch: true end class Author < ApplicationRecord has_many :books end
上のBook
は、save
またはdestroy
したときに、関連付けられているAuthor
のタイムスタンプが更新されます。
以下のように、更新時に特定のタイムスタンプ属性を指定することも可能です。
class Book < ApplicationRecord belongs_to :author, touch: :books_updated_at end
has_and_belongs_to_many
関連付けは、:touch
オプションをサポートしていません。代わりにhas_many :through
関連付けとjoinテーブルを使えば、同様の機能を実現できます。詳しくは、has_many :through
セクションを参照してください。
:validate
:validate
オプションをtrue
に設定すると、新たに関連付けられたオブジェクトを保存したときにバリデーションが実行されます。デフォルトはfalse
であり、この場合新たに関連付けられたオブジェクトは保存時にバリデーションされません。
has_and_belongs_to_many
関連付けは、:validate
オプションをサポートしていません。代わりにhas_many :through
関連付けとjoinテーブルを使えば、同様の機能を実現できます。詳しくは、has_many :through
セクションを参照してください。
:inverse_of
:inverse_of
オプションは、その関連付けの逆関連付けとなるhas_many
関連付けまたはhas_one
関連付けの名前を指定します。
詳しくは双方向関連付けを参照してください。
class Supplier < ApplicationRecord has_one :account, inverse_of: :supplier end class Account < ApplicationRecord belongs_to :supplier, inverse_of: :account end
:source_type
:source_type
オプションは、ポリモーフィック関連付けを介するhas_one :through
関連付けで、関連付け元の型を指定します。
class Author < ApplicationRecord has_many :books has_many :paperbacks, through: :books, source: :format, source_type: "Paperback" end class Book < ApplicationRecord belongs_to :format, polymorphic: true end class Hardback < ApplicationRecord; end class Paperback < ApplicationRecord; end
:strict_loading
true
を指定すると、関連付けられるレコードが、この関連付けを経由して読み込まれるたびにstrict loadingを強制するようになります。
:association_foreign_key
:association_foreign_key
オプションは、has_and_belongs_to_many
関連付けで利用可能です。Railsの規約では、相手のモデルを指す外部キーを保持しているjoinテーブル上のカラム名については、そのモデル名にサフィックス_id
を追加した名前が使われることが想定されます。:association_foreign_key
オプションを使うと、外部キー名を以下のように直接指定できます。
class User < ApplicationRecord has_and_belongs_to_many :friends, class_name: "User", foreign_key: "this_user_id", association_foreign_key: "other_user_id" end
:foreign_key
オプションおよび:association_foreign_key
オプションは、多対多のself-joinを行いたいときに便利です。
:join_table
:join_table
オプションは、has_and_belongs_to_many
関連付けで利用可能です。辞書順に基いて生成されたjoinテーブルのデフォルト名では不都合がある場合、:join_table
オプションを用いてデフォルトのテーブル名を上書きできます。
スコープ(scope)を使うと、関連付けオブジェクトのメソッド呼び出しとして参照可能な共通クエリを指定できます。スコープは、以下のようにアプリケーション内の複数の場所で再利用されるカスタムクエリを定義する場合に便利です。
class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { where active: true } end
スコープブロック内では標準のクエリメソッドをすべて利用できます。ここでは以下について説明します。
where
includes
readonly
select
where
where
メソッドは、関連付けられるオブジェクトが満たすべき条件を指定します。
class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { where "factory = 'Seattle'" } end
以下のようにwhere
をハッシュ形式で使うことも可能です。
class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { where factory: 'Seattle' } end
where
をハッシュ形式で利用すると、この関連付けによるレコード作成はハッシュによって自動的にスコープ設定されます。この場合、@parts.assemblies.create
や@parts.assemblies.build
を使うことで、factory
カラムの値が"Seattle"であるアセンブリが作成されます。
includes
includes
メソッドを使うと、その関連付けが使われるときにeager loadingすべき第2関連付けを指定できます。以下のモデルを例に考えてみましょう。
class Supplier < ApplicationRecord has_one :account end class Account < ApplicationRecord belongs_to :supplier belongs_to :representative end class Representative < ApplicationRecord has_many :accounts end
供給元(supplier)からアカウントの代表(representative)を@supplier.account.representative
のように直接取り出す機会が多い場合は、Supplier
からAccount
への関連付けにRepresentative
をあらかじめincludes
しておくと、クエリ数が削減されて効率が高まります。
class Supplier < ApplicationRecord has_one :account, -> { includes :representative } end class Account < ApplicationRecord belongs_to :supplier belongs_to :representative end class Representative < ApplicationRecord has_many :accounts end
直接の関連付けではincludes
を使う必要はありません。Book belongs_to :author
のような直接の関連付けでは、必要に応じて自動的にeager loadingされます。
readonly
readonly
を指定すると、関連付けられたオブジェクトを読み出し専用で取り出します。
class Book < ApplicationRecord belongs_to :author, -> { readonly } end
このオプションは、関連付けられたオブジェクトが関連付けで変更されないようにしたい場合に便利です。たとえば、belongs_to :author
関連付けを指定したBook
モデルがある場合、readonly
オプションを指定することで、著者(author)が書籍(book)を介して変更される事故を防止できます。
@book.author = Author.first @book.author.save! # ActiveRecord::ReadOnlyRecordエラーをraiseする
select
select
メソッドを使うと、関連付けられたオブジェクトのデータ取り出しに使われるSQLのSELECT
句をオーバーライドできます。Railsはデフォルトですべてのカラムを取り出します。
たとえば、Author
モデルが多数のBook
を持つ場合に、書籍のtitle
だけを取得したい場合は、次のようになります。
class Author < ApplicationRecord has_many :books, -> { select(:id, :title) } # idカラムとtitleカラムだけをSELECTする end class Book < ApplicationRecord belongs_to :author end
これで、著者の本にアクセスすると、books
テーブルからid
カラムとtitle
カラムだけが取得されます。
select
をbelongs_to
関連付けで使う場合は、正しい結果を得るために:foreign_key
オプションも設定する必要があります。
class Book < ApplicationRecord belongs_to :author, -> { select(:id, :name) }, foreign_key: 'author_id' # idカラムとnameカラムだけをselectする end class Author < ApplicationRecord has_many :books end
has_many
とhas_and_belongs_to_many
はレコードのコレクションを扱う関連付けなので、group
、limit
、order
、select
、distinct
などの追加メソッドを用いて、関連付けで使われるクエリをカスタマイズできます。
group
group
メソッドは、結果をグループ化する属性名を1つ指定します。内部的にはSQLのGROUP BY
句が使われます。
class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { group "factory" } end
limit
limit
メソッドは、関連付けを用いて取得できるオブジェクトの総数の上限を指定するのに使います。
class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { order("created_at DESC").limit(50) } end
order
order
メソッドは、関連付けられたオブジェクトを受け取るときの並び順を指定します。内部的にはSQLのORDER BY
句が使われます。
class Author < ApplicationRecord has_many :books, -> { order "date_confirmed DESC" } end
select
select
メソッドを使うと、関連付けられたオブジェクトのデータ取り出しに使われるSQLのSELECT
句をオーバーライドできます。Railsはデフォルトではすべてのカラムを取り出します。
select
メソッドをカスタマイズする場合、関連付けられているモデルの主キーカラムと外部キーカラムを除外してはいけません。さもないと、Railsでエラーが発生します。
distinct
distinct
メソッドは、コレクション内で重複が発生しないようにします。
このメソッドは、特に:through
オプションと併用するときに便利です。
class Person < ApplicationRecord has_many :readings has_many :articles, through: :readings end
irb> person = Person.create(name: 'John') irb> article = Article.create(name: 'a1') irb> person.articles << article irb> person.articles << article irb> person.articles.to_a => [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">] irb> Reading.all.to_a => [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>]
上の例にはreading
が2件ありますが、これらのレコードは同じarticle
を指しているにもかかわらず、person.articles
では2件重複して出力されています。
今度はdistinct
を設定してみましょう。
class Person has_many :readings has_many :articles, -> { distinct }, through: :readings end
irb> person = Person.create(name: 'Honda') irb> article = Article.create(name: 'a1') irb> person.articles << article irb> person.articles << article irb> person.articles.to_a => [#<Article id: 7, name: "a1">] irb> Reading.all.to_a => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
上の例にもreading
が2件ありますが、person.articles
を実行すると1件のarticle
だけを表示します。これはコレクションが一意のレコードだけを読み出しているからです。
挿入時にも同様に、永続化済みのレコードをすべて一意にする(関連付けを検査したときに重複レコードが決して発生しないようにする)には、テーブル自体にuniqueインデックスを追加する必要があります。たとえばreadings
というテーブルがあるとすると、1人のperson
に記事を1回しか追加できないようにするには、マイグレーションに以下を追加します。
add_index :readings, [:person_id, :article_id], unique: true
uniqueインデックスを設定すると、同じ記事をperson
に2回追加しようとしたときに
ActiveRecord::RecordNotUnique
エラーが発生するようになります
irb> person = Person.create(name: 'Honda') irb> article = Article.create(name: 'a1') irb> person.articles << article irb> person.articles << article ActiveRecord::RecordNotUnique
なお、一意性チェックにinclude?
などのRubyメソッドを使うと競合が発生しやすいので注意が必要です。関連付けで強制的に一意にする目的でRubyのinclude?
を使ってはいけません。たとえば上のarticle
の例では、以下のコードを複数のユーザーが同時に実行したときに競合が発生しやすくなります。
person.articles << article unless person.articles.include?(article)
関連付けのスコープをさらに制御する必要がある状況では、関連付けのオーナーをスコープブロックに引数として渡す方法が使えます。ただし、これを行うと関連付けのプリロードが不可能になる点にご注意ください。
class Supplier < ApplicationRecord has_one :account, ->(supplier) { where active: supplier.active? } end
上の例では、Supplier
モデルのaccount
関連付けは、供給元(supplier)のactive
ステータスに基づいてスコープが設定されます。
関連付けを拡張して、関連付けのオーナーでスコープを設定することで、Railsアプリケーションでより動的でコンテキストに対応した関連付けを作成できるようになります。
:counter_cache
オプションは、従属しているオブジェクトの個数の検索効率を向上させます。
以下のモデルで考えてみましょう。
class Book < ApplicationRecord belongs_to :author end class Author < ApplicationRecord has_many :books end
上の宣言のままでは、@author.books.size
の値を知るためにデータベースでCOUNT(*)
クエリをデフォルトで実行します。
これを最適化するには、以下のように、そのモデルに「従属する側のモデル(belongs_to
を宣言している側のモデル)」にカウンタキャッシュを追加します。これにより、Railsはデータベースにクエリを送信せずにキャッシュから直接カウントを返せるようになります。
class Book < ApplicationRecord belongs_to :author, counter_cache: true end class Author < ApplicationRecord has_many :books end
上のように宣言すると、Railsはキャッシュ値を最新の状態に保ち、次回size
メソッドが呼び出されたときにその値を返します。これにより、不要なデータベースクエリを回避できます。
ここで1つ注意が必要です。:counter_cache
オプションはbelongs_to
宣言で指定しますが、実際に個数を数えたいカラムは関連付け先のモデル(ここではhas_many
を宣言しているモデル)の側に追加する必要があります。上の場合は、相手側のAuthor
モデルにbooks_count
カラムを追加する必要があります。
class AddBooksCountToAuthors < ActiveRecord::Migration[6.0] def change add_column :authors, :books_count, :integer, default: 0, null: false end end
counter_cache
オプションでtrue
の代わりに任意のカラム名を設定すると、デフォルトのカラム名をオーバーライドできます。以下は、books_count
の代わりにcount_of_books
を設定した場合の例です。
class Book < ApplicationRecord belongs_to :author, counter_cache: :count_of_books end class Author < ApplicationRecord has_many :books end
:counter_cache
オプションは、関連付けのbelongs_to
側にだけ指定する必要があります。
既存の巨大テーブルで準備なしにカウンタキャッシュを使い始めると、トラブルが生じる可能性があります。テーブルを長時間ロックされるのを避けるためには、カラムの追加作業と、値をバックフィルする作業を分けて行う必要があります(つまりカラムの追加と値のバックフィルを一度に行わないようにします)。このバックフィル作業は、:counter_cache
オプションを利用する前の段階で行っておく必要があります(さもないと、カウンタキャッシュを内部で利用するsize
やany?
などのメソッドで誤った結果が生成される可能性があります)。
子レコードの作成や削除で発生するカウンタキャッシュのカラム更新を止めずに、値を安全にバックフィルするには、counter_cache: { active: false }
オプションを指定します。このオプションを指定している間は、上述のメソッドでカウンタキャッシュカラムの誤った値を使わなくなり、常にデータベースから結果を取得するようになります。
カスタムのカラム名も指定する必要がある場合は、counter_cache: { active: false, column: :my_custom_counter }
を使います。
何らかの理由でオーナーモデルの主キーの値を変更し、カウントされたモデルの外部キーも更新しなかった場合、カウンタキャッシュのデータが古くなっている可能性があります(つまり、孤立したモデルも引き続きカウンタでカウントされます)。古くなったカウンタキャッシュを修正するには、reset_counters
をお使いください。
通常のコールバックは、Active Recordオブジェクトのライフサイクルの中でフックされます。これにより、さまざまなタイミングでオブジェクトのコールバックを実行できます。たとえば、:before_save
コールバックを使うと、オブジェクトが保存される直前に何かを実行できます。
関連付けのコールバックも、上のような通常のコールバックと似ていますが、(Active Recordオブジェクトではなく)コレクションのライフサイクルによってイベントがトリガされる点が異なります。関連付けでは、以下の4つのコールバックを利用できます。
before_add
after_add
before_remove
after_remove
これらのオプションを関連付けの宣言に追加することで、関連付けコールバックを定義できます。以下に例を示します。
class Author < ApplicationRecord has_many :books, before_add: :check_credit_limit def check_credit_limit(book) throw(:abort) if limit_reached? end end
この例では、Author
モデルはbooks
とhas_many
関連付けがあります。before_add
コールバックで指定したcheck_credit_limit
は、書籍がコレクションに追加される前にトリガーされます。limit_reached?
メソッドがtrue
を返す場合、書籍はコレクションに追加されません。
これらの関連付けコールバックを活用することで、関連付けの振る舞いをカスタマイズして、コレクションのライフサイクルの重要なポイントで特定のアクションを実行できるようになります。
関連付けのコールバックについて詳しくは、Active Recordコールバックガイドを参照してください。
Railsは、関連付けのプロキシオブジェクト(関連付けを管理する)を拡張するための機能を提供しており、新しいファインダーやクリエーターなどのメソッドを無名モジュール(anonymous module)を介して追加します。この機能により、アプリケーションの特定のニーズに合わせて関連付けをカスタマイズできます。
以下のように、モデル定義内で直接カスタムメソッドを利用することでhas_many
関連付けを拡張できます。
class Author < ApplicationRecord has_many :books do def find_by_book_prefix(book_number) find_by(category_id: book_number[0..2]) end end end
上の例では、find_by_book_prefix
メソッドがAuthor
モデルのbooks
関連付けに追加されています。このカスタムメソッドを使えば、book_number
の特定のプレフィックスに基づいてbooks
を検索できるようになります。
拡張をさまざまな関連付けで共有したい場合は、名前付きの拡張モジュールを使うことも可能です。以下に例を示します。
module FindRecentExtension def find_recent where("created_at > ?", 5.days.ago) end end class Author < ApplicationRecord has_many :books, -> { extending FindRecentExtension } end class Supplier < ApplicationRecord has_many :deliveries, -> { extending FindRecentExtension } end
ここでは、Author
モデルのbooks
関連付けと、Supplier
モデルのdeliveries
関連付けの両方に対して、FindRecentExtension
モジュールを使ってfind_recent
メソッドを追加しています。このメソッドは、過去5日以内に作成されたレコードを取得します。
拡張は、proxy_association
アクセサを用いて関連付けプロキシの内部にアクセスできます。proxy_association
には、以下の重要な3つの属性があります。
proxy_association.owner
: 関連付けを所有するオブジェクトを返します。proxy_association.reflection
: 関連付けを記述するリフレクションオブジェクトを返します。proxy_association.target
: belongs_to
またはhas_one
関連付けのオブジェクトを返すか、has_many
またはhas_and_belongs_to_many
関連付けオブジェクトのコレクションを返します。拡張はこれらの属性を用いることで、関連付けプロキシの内部状態や振る舞いにアクセスして操作できるようになります。
拡張におけるこれらの属性の利用方法を示す高度な例を次に示します。
module AdvancedExtension def find_and_log(query) results = where(query) proxy_association.owner.logger.info("Querying #{proxy_association.reflection.name} with #{query}") results end end class Author < ApplicationRecord has_many :books, -> { extending AdvancedExtension } end
上の例では、find_and_log
メソッドは関連付けに対してクエリを実行し、オーナーのロガーを使ってクエリの詳細を記録しています。このメソッドは、proxy_association.owner
を介してオーナーのロガーにアクセスし、proxy_association.reflection.name
を介して関連付けの名前にアクセスします。
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。