Active Record の関連付け

本ガイドでは、Active Recordの関連付け機能(アソシエーション)について解説します。

このガイドの内容:

  • さまざまな種別の関連付けを理解する
  • Active Recordのモデル同士の関連付けを宣言する方法
  • モデルに適した関連付けの種別を選ぶ方法
  • STI(Single Table Inheritance)の利用方法
  • Delegated Typesのセットアップ方法と利用方法

目次

  1. 関連付けの概要
  2. 関連付けの種類
  3. 関連付けの選び方
  4. 高度な関連付け
  5. 単一テーブル継承 (STI)
  6. Delegated Types
  7. ヒントと注意事項
  8. 関連付けの詳しい参考情報
  9. 参考資料(日本語)

1 関連付けの概要

Active Recordの「関連付け(アソシエーション: association)」を使うと、モデル間のリレーションシップを定義できます。関連付けは特殊なマクロスタイルの呼び出しとして実装されており、モデル同士をどのように関連させるかをRailsに手軽に指定できます。これにより、データの管理がより効率的になり、一般的なデータ操作がシンプルで読みやすくなります。

マクロスタイルの呼び出しは、実行時に他のメソッドを動的に生成・変更するメソッドであり、Railsでのモデルの関連付けの定義など、簡潔で表現力豊かな機能の宣言を可能にします。たとえばhas_many :commentsのように記述します。

関連付けを設定すると、Railsが2つのモデルのインスタンス同士の主キー(primary key)外部キー(foreign key)のリレーションシップや管理を支援し、データベースがデータの整合性を保つようにします。これにより、関連付けられているデータの取得・更新・削除を手軽に行えるようになります。

これにより、どのレコードがどのレコードと関係があるかを簡単に把握できるようになります。また、モデルにさまざまな便利メソッドが追加されるため、関連データをより手軽に操作可能になります。

Author(著者)モデルとBook(書籍)モデルを持つシンプルなアプリケーションを例に考えてみましょう。

1.1 関連付けを使わない場合

関連付けが設定されていない以下のような場合、その著者の本を作成・削除するために、以下のように面倒な手動の処理が必要になります。

class CreateAuthors < ActiveRecord::Migration[8.0]
  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をすべて取り出してから、個別のbookeachで回して削除し、それが終わってから著者を削除しなければならないでしょう。

@books = Book.where(author_id: @author.id)
@books.each do |book|
  book.destroy
end
@author.destroy

1.2 関連付けを使う場合

しかし関連付けを使えば、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の関連付けメソッドとオプションの完全な参考情報も記載されています。

2 関連付けの種類

Railsでは6種類の関連付けをサポートしています。それぞれの関連付けは、特定の用途に特化しています。

以下は、Railsでサポートされている全種類の関連付けのリストです。リストはAPIドキュメントにリンクされているので、詳しい情報や利用方法、メソッドパラメータなどはリンク先を参照してください。

本ガイドでは以後、それぞれの関連付けの宣言方法と利用方法について詳しく解説します。その前に、それぞれの関連付けが適切となる状況について簡単にご紹介します。

2.1 belongs_to関連付け

あるモデルでbelongs_to関連付けを行なうと、宣言を行った側のモデルの各インスタンスは、他方のモデルのインスタンスに文字どおり「従属(belongs to)」します。

たとえば、Railsアプリケーションに著者(Author)と書籍(Book)の情報が含まれており、書籍1冊につき正確に1人の著者を割り当てたい場合は、Bookモデルで以下のように宣言します。

class Book < ApplicationRecord
  belongs_to :author
end

belongs_to 関連付けの図

belongs_to関連付けで指定するモデル名は必ず「単数形」にしなければなりません。上記の例で、Bookモデルのauthor関連付けを複数形(authors)にしてからBook.create(authors: @author)でインスタンスを作成しようとすると、uninitialized constant Book::Authorsエラーが発生します。Railsは、関連付けの名前から自動的にモデルのクラス名を推測します。関連付け名を:authorsにすると、Railsは本来のAuthorクラスではなくAuthorsという誤ったクラス名を探索してしまいます。

上の関連付けに対応するマイグレーションは以下のような感じになります。

class CreateBooks < ActiveRecord::Migration[8.0]
  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はデフォルトで、参照整合性を保証するために、関連付けられたレコードの存在バリデーションを行います。

モデルでoptionaltrueに設定されている場合、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テーブル内のレコードが必ず有効でなければならないことが保証されます。

2.1.1 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_で始まるメソッドをお使いください。

2.1.1.1 関連付けを取り出す

associationメソッドは、関連付けられたオブジェクトを返します。関連付けられたオブジェクトがない場合はnilを返します。

@author = @book.author

このオブジェクトに関連付けられたオブジェクトがデータベースから既に取得されている場合は、キャッシュされたものを返します。この振る舞いを上書きして、キャッシュを読み出さずにデータベースから強制的に読み込みたい場合は、親オブジェクトが持つ#reload_associationメソッドを呼び出します。

@author = @book.reload_author

関連付けされたオブジェクトのキャッシュバージョンをアンロードして、次回のアクセスでデータベース呼び出しからクエリするには、親オブジェクトの#reset_associationを呼び出します。

@book.reset_author
2.1.1.2 関連付けの割り当て

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)
2.1.1.3 関連付けが変更されたかどうかをチェックする

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?は関連付けの「属性」が変更されたかどうかをチェックします。

2.1.1.4 既存の関連付けが存在するかどうかをチェックする

association.nil?メソッドを用いて、関連付けられたオブジェクトが存在するかどうかをチェックできます。

if @book.author.nil?
  @msg = "この本の著者が見つかりません"
end
2.1.1.5 オブジェクトが保存されるタイミング

オブジェクトをbelongs_to関連付けに割り当てても、現在のオブジェクトや関連付けられたオブジェクトが自動的に保存されるわけではありません。ただし、現在のオブジェクトを保存すれば、関連付けられたオブジェクトも保存されます。

2.2 has_one関連付け

has_one関連付けは、相手側のモデルがこのモデルへの参照を持っていることを示します。相手側のモデルは、この関連付けを経由してフェッチできます。

たとえば、アプリケーション内で供給元(supplier)ごとにアカウント(account)が1個だけ存在する場合は、次のようにSupplierモデルを宣言します。

class Supplier < ApplicationRecord
  has_one :account
end

belongs_to関連付けとの主な違いは、リンクカラム(ここではsupplier_id)が相手側のテーブルにあり、has_oneを宣言したテーブルには存在しないことです。

has_one関連付けの図

上の関連付けに対応するマイグレーションは以下のような感じになります。

class CreateSuppliers < ActiveRecord::Migration[8.0]
  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_idsuppliersテーブルの有効なsupplierを参照することが保証されます。これにより、関連付けがデータベースレベルで強制されます。

create_table :accounts do |t|
  t.belongs_to :supplier, index: { unique: true }, foreign_key: true
  # ...
end

このリレーションは、相手側のモデルでbelongs_to関連付けも設定することで双方向関連付けになります。

2.2.1 has_oneで追加されるメソッド

has_one関連付けを宣言したクラスでは、さまざまなメソッドが自動的に利用できるようになります。以下はその一部です。

  • association
  • association=(associate)
  • build_association(attributes = {})
  • create_association(attributes = {})
  • create_association!(attributes = {})
  • reload_association
  • reset_association

本ガイドでは、よく使われるメソッドの一部を取り上げていますが、完全なリストについてはActive Recordの関連付けAPIを参照してください。

belongs_to関連付けの場合と同様、上のメソッド名の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.buildhas_many関連付けやhas_and_belongs_to_many関連付けで使います)。関連付けを作成する場合は、create_で始まるメソッドをお使いください。

2.2.1.1 関連付けを取り出す

associationメソッドは、関連付けられたオブジェクトを返します。関連付けられたオブジェクトがない場合はnilを返します。

@account = @supplier.account

関連付けられたオブジェクトがデータベースから既に取得されている場合は、キャッシュされたものを返します。この振る舞いを上書きして、キャッシュを読み出さずにデータベースから強制的に読み込みたい場合は、親オブジェクトが持つ#reload_associationメソッドを呼び出します。

@account = @supplier.reload_account

関連付けされたオブジェクトのキャッシュバージョンをアンロードして、次回のアクセスでデータベース呼び出しからクエリするには、親オブジェクトの#reset_associationを呼び出します。

@supplier.reset_account
2.2.1.2 関連付けの割り当て

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)
2.2.1.3 既存の関連付けが存在するかどうかをチェックする

association.nil?メソッドを用いて、関連付けられたオブジェクトが存在するかどうかをチェックできます。

if @supplier.account.nil?
  @msg = "この本の著者が見つかりません"
end
2.2.1.4 オブジェクトが保存されるタイミング

オブジェクトをhas_one関連付けに割り当てると、そののオブジェクトや関連付けられたオブジェクトが自動的に保存されて外部キーが更新されます。また、置き換えられるオブジェクトも自動的に保存され、その外部キーも更新されます。

保存のいずれかがバリデーションエラーで失敗すると、割り当てステートメントはfalseを返し、割り当て自体がキャンセルされます。

親オブジェクト(has_one 関連付けを宣言している側のオブジェクト)が保存されていない場合(つまり、new_record?trueを返す場合)、子オブジェクトはすぐには保存されません。親オブジェクトが保存されると、子オブジェクトは自動的に保存されます。

オブジェクトを保存せずにhas_one関連付けにオブジェクトを割り当てるには、build_associationメソッドを使います。このメソッドは、関連付けられたオブジェクトの新しい未保存のインスタンスを作成して、保存するかどうかを決定する前に作業可能にします。

モデルに関連付けられたオブジェクトを保存するかどうかを制御するには、autosave: falseオプションを使います。この設定により、親オブジェクトが保存されたときに関連付けられたオブジェクトが自動的に保存されなくなります。逆に、保存されていない関連付けられたオブジェクトを操作し、準備ができるまでその永続化を遅延する必要がある場合は、build_associationメソッドを使います。

2.3 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関連付けを宣言する場合は、相手のモデル名を「複数形」で指定する必要があります。

has_many関連付けの図

上の関連付けに対応するマイグレーションは以下のような感じになります。

class CreateAuthors < ActiveRecord::Migration[8.0]
  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関連付けも設定することで双方向関連付けにできます。

2.3.1 has_many関連付けで追加されるメソッド

has_many関連付けを宣言したクラスでは、さまざまなメソッドが自動的に利用できるようになります。以下はその一部です。

本ガイドでは、よく使われるメソッドの一部を取り上げていますが、完全なリストについては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
2.3.1.1 コレクションを管理する

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
2.3.1.2 コレクションの割り当て

collection=(objects)メソッドは、削除や追加を適宜実行することで、渡したオブジェクトだけがそのコレクションに含まれるようにします。変更の結果はデータベースで永続化されます。

collection_singular_ids=(ids)メソッドは、削除や追加を適宜実行することで、指定した主キーのidを持つオブジェクトだけがコレクションに含まれるようにします。変更の結果はデータベースで永続化されます。

2.3.1.3 コレクションにクエリを実行する

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?メソッドは、指定された条件に合うオブジェクトがコレクションの中に存在するかどうかをチェックします。

2.3.1.4 関連付けられるオブジェクトのビルドと作成

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される点が異なります。

2.3.1.5 オブジェクトが保存されるタイミング

has_many関連付けにオブジェクトを割り当てると、外部キーを更新するためにそのオブジェクトは自動的に保存されます。1つの文で複数のオブジェクトを割り当てると、それらはすべて保存されます。

関連付けられているオブジェクトのどれかがバリデーションエラーで保存に失敗すると、falseを返し、割り当てはキャンセルされます。

親オブジェクト(has_many関連付けを宣言している側のオブジェクト)が保存されない場合(つまりnew_record?trueを返す場合)、子オブジェクトを追加したときに保存されません。親オブジェクトが保存されると、関連付けられていたオブジェクトのうち保存されていなかったメンバーはすべて保存されます。

has_many関連付けにオブジェクトを割り当てて、しかもそのオブジェクトを保存したくない場合は、collection.buildメソッドをお使いください。

2.4 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)の複数のインスタンスと関連付けられることを可能にします。

has_many :through関連付けの図

上の関連付けに対応するマイグレーションは以下のような感じになります。

class CreateAppointments < ActiveRecord::Migration[8.0]
  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カラムを持つ形で作成され、physicianspatientsの間に多対多の関係を確立します。

また、以下のようにhas_many :throughリレーションシップのjoinテーブルに複合主キーを利用することも検討できます。

class CreateAppointments < ActiveRecord::Migration[8.0]
  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

2.5 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

上のセットアップによって、supplieraccountを経由して直接account_historyにアクセス可能になります。

has_one :through関連付けの図

上の関連付けに対応するマイグレーションは以下のような感じになります。

class CreateAccountHistories < ActiveRecord::Migration[8.0]
  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

2.6 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関連付けの図

has_and_belongs_to_manyは介在モデルが不要ですが、関係する2つのモデル間の多対多リレーションシップを確立するためのテーブルは別途必要です。この介在テーブルは、2つのモデルのインスタンス間の関連付けをマッピングし、関連するデータを保存する役割があります。この介在テーブルは、関連するレコード間のリレーションシップを管理することだけが目的なので、テーブルには必ずしも主キーは必要ありません。

上の関連付けに対応するマイグレーションは以下のような感じになります。

class CreateAssembliesAndParts < ActiveRecord::Migration[8.0]
  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関連付けを使うべきです。

2.6.1 has_and_belongs_to_manyで追加されるメソッド

has_and_belongs_to_many関連付けを宣言したクラスでは、さまざまなメソッドが自動的に利用できるようになります。以下はその一部です。

本ガイドでは、よく使われるメソッドの一部を取り上げていますが、完全なリストについては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
2.6.1.1 コレクションを管理する

collectionメソッドは、関連付けられたすべてのオブジェクトのリレーションを返します。関連付けられたオブジェクトがない場合は、空のリレーションを1つ返します。

@assemblies = @part.assemblies

collection<<メソッドは、joinテーブル上でレコードを作成し、それによって1個以上のオブジェクトをコレクションに追加します。

@part.assemblies << @assembly1

このメソッドはcollection.concatcollection.pushのエイリアスです。

collection.deleteメソッドは、joinテーブル内のレコードを削除する形で、コレクションから1個以上のオブジェクトを取り除きます。オブジェクトはdestroyされません。

@part.assemblies.delete(@assembly1)

collection.destroyメソッドは、joinテーブル内のレコードを削除する形で、コレクションから1個以上のオブジェクトを取り除きます。オブジェクトはdestroyされません。

@part.assemblies.destroy(@assembly1)

collection.clearメソッドは、joinテーブル上のレコードを削除する形で、すべてのオブジェクトをコレクションから取り除きます。オブジェクトはdestroyされません。

2.6.1.2 コレクションの割り当て

collection=メソッドは、削除や追加を適宜実行することで、渡したオブジェクトだけがそのコレクションに含まれるようにします。変更の結果はデータベースで永続化されます。

collection_singular_idsメソッドは、削除や追加を適宜実行することで、指定した主キーの値を持つオブジェクトだけがコレクションに含まれるようにします。変更の結果はデータベースで永続化されます。

2.6.1.3 コレクションにクエリを実行する

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?メソッドは、指定された条件に合うオブジェクトがコレクションのテーブル内に存在するかどうかをチェックします。

2.6.1.4 関連付けられるオブジェクトのビルドと作成

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
2.6.1.5 オブジェクトが保存されるタイミング

has_and_belongs_to_many関連付けにオブジェクトを割り当てると、外部キーを更新するためにそのオブジェクトは自動的に保存されます。1つの文で複数のオブジェクトを割り当てると、それらはすべて保存されます。

関連付けられているオブジェクトのどれかがバリデーションエラーで保存に失敗すると、falseを返し、割り当てはキャンセルされます。

親オブジェクト(has_and_belongs_to_many関連付けを宣言している側のオブジェクト)が保存されない場合(つまりnew_record?trueを返す場合)、子オブジェクトを追加したときに保存されません。親オブジェクトが保存されると、関連付けられていたオブジェクトのうち保存されていなかったメンバーはすべて保存されます。

has_and_belongs_to_many関連付けにオブジェクトを割り当てて、しかもそのオブジェクトをsaveしたくない場合、collection.buildメソッドをお使いください。

3 関連付けの選び方

3.1 belongs_tohas_oneのどちらを選ぶか

2つのモデルの間に1対1のリレーションシップを設定したい場合は、一方のモデルにbelongs_to関連付けを追加し、他方のモデルにhas_one関連付けを追加できます。どちらの関連付けをどちらのモデルに置けばよいでしょうか。

区別の決め手となるのは、外部キー(foreign key)をどちらのモデルに置くかです(外部キーは、belongs_to関連付けを追加したモデルのテーブルに追加します)が、適切な関連付けを決めるためには、もう少しデータの実際の意味についても考えてみる必要があります。

  • belongs_to: この関連付けは、それを宣言する現在のモデルに外部キーが含まれていることと、現在のモデルがリレーションシップにおける「子」であることを意味します。この関連付けで他方のモデルを参照すると、このモデルの各インスタンスが、他方のモデルの「1個の」インスタンスに紐づけられることを示します。

  • has_one: この関連付けは、それを宣言する現在のモデルがリレーションシップにおける「親」であることと、他方のモデルのインスタンスを「1個」所有していることを意味します。

たとえば、供給元(suppliers)とそのアカウント(accounts)があるシナリオを考えてみましょう。アカウントが供給元を持っている/所有していると考えるよりも、供給元がアカウントを持っている/所有している (供給元が親になる) と考える方が自然です。したがって、正しい関連付けは次のようになります。

  • 1つの供給元が、1個のアカウントを所有する(has one)。
  • 1個のアカウントは、1つの供給元に属する(belongs to)。

Railsでは、これらの関連付けを以下のように定義できます。

class Supplier < ApplicationRecord
  has_one :account
end

class Account < ApplicationRecord
  belongs_to :supplier
end

これらの関連付けを実装するには、対応するデータベーステーブルを作成して、外部キーを設定する必要があります。 マイグレーションの例は以下のような感じになります。

class CreateSuppliers < ActiveRecord::Migration[8.0]
  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テーブルの方に外部キーを配置します。

3.2 has_many :throughhas_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関連付けを使います。

  • joinテーブルに追加の属性やメソッドを追加する必要がある場合。
  • joinモデルでバリデーションコールバックが必要な場合。
  • joinテーブルを、独自に振る舞う「独立したエンティティ」として扱う必要がある場合

もうひとつの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関連付けを使います。

  • 関連付けがシンプルで、joinテーブルに属性や振る舞いを追加する必要がない場合。
  • joinテーブルで「バリデーション」「コールバック」「追加メソッド」が必要ない場合。

4 高度な関連付け

4.1 ポリモーフィック関連付け

ポリモーフィック関連付け(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モデルと、EmployeeProductなどの他のモデルとの間のポリモーフィック関連付けを表すシンボル名です。 ここで重要な点は、ポリモーフィック関連付けを正しく確立するためには、関連付けられるすべてのモデルで必ず同じ名前(imageable)に統一することです。

Pictureモデルでbelongs_to :imageable, polymorphic: trueを宣言すると、「Pictureはこの関連付けを通じて任意のモデル(EmployeeProductなど)に属することが可能である」と宣言したことになります。

ポリモーフィックなbelongs_to宣言は、他の任意のモデルでも利用できるインターフェイスを設定するものとみなせます。これにより、たとえば@employee.picturesと書くだけで、Employeeモデルのインスタンスから写真のコレクションを取得できます。同様に、@product.picturesと書くだけで、Productモデルのインスタンスから写真のコレクションを取得できます。

さらに、Pictureモデルのインスタンスがある場合は、@picture.imageableを経由してその親モデル(EmployeeまたはProduct)を取得できます。

ポリモーフィック関連付けを手動でセットアップする場合は、以下のようにモデルで外部キーカラム(imageable_id)とtypeカラム(imageable_type)の両方を宣言する必要があります。

class CreatePictures < ActiveRecord::Migration[8.0]
  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_idEmployeeProductのIDであり、imageable_typeは関連付けられるモデルのクラス名(つまりEmployeeProduct)になります。

ポリモーフィック関連付けを手動で作成することも一応可能ですが、それよりも以下のようにt.references(またはそのエイリアスt.belong_to)を用いてpolymorphic: trueを指定する方がオススメです。これにより、関連付けがポリモーフィックであることがRailsに認識され、外部キーとtypeカラムが両方ともテーブルに自動的に追加されます。

class CreatePictures < ActiveRecord::Migration[8.0]
  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カラムの値を新しいクラス名で更新する必要があります。さらに、変更を反映するために、アプリケーションコード全体でもその他のクラス名への参照を更新する必要があります。

4.2 複合主キーを持つモデルの関連付け

Railsは多くの場合、関連付けられるモデル間の主キーと外部キーのリレーションシップを推測できますが、複合主キーを扱う場合、明示的に指示されない限り、複合キーの一部のみ(多くの場合idカラム)がデフォルトで利用されます。

Railsモデルで複合主キーを利用していて、関連付けを適切に処理する必要がある場合は、複合主キーガイドの複合主キーを持つモデルの関連付けセクションを参照してください。このセクションでは、必要に応じて複合外部キーを指定する方法など、Railsで複合主キーとの関連付けを設定・利用する方法について包括的なガイダンスを提供します。

4.3 self-join

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対多リレーションシップを設定します。このとき、関連するモデルにEmployeeclass_name: "Employee")を指定し、マネージャを特定するための外部キーにmanager_idを指定します。

belongs_to :manager関連付けは、1人の従業員が1人のマネージャを持つ1対1リレーションシップを設定します。こちらにもEmployeeモデルを指定します。

このリレーションシップをサポートするには、以下のようなマイグレーションでemployeesテーブルにmanager_idカラムを追加する必要があります。このカラムは、別の従業員(マネージャ)のidを参照します。

class CreateEmployees < ActiveRecord::Migration[8.0]
  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

5 単一テーブル継承 (STI)

単一テーブル継承 (STI: Single Table Inheritance) は、複数のモデルを単一のデータベーステーブルに収納できるRailsのパターンです。これは、さまざまなエンティティに共通の属性や振る舞いを持たせつつ、エンティティ固有の振る舞いも持たせたい場合に便利です。

たとえば、CarMotorcycleBicycleというモデルがあるとします。これらのモデルはcolorpriceなどのフィールドを共有しますが、モデルごとに固有の振る舞いもあるとします。さらに、モデルごとに独自のコントローラーもあるとします。

5.1 ベースとなるVehicleモデルを生成する

最初に、共有フィールドを持つVehicleモデルを生成します。

$ bin/rails generate model vehicle type:string color:string price:decimal{10.2}

STIで重要なのは、このtypeフィールドです(ここにCarMotorcycleBicycleなどのモデル名が保存されます)。STIでは、同じテーブルに保存されるさまざまなモデルを区別するために、このフィールドが不可欠です。

5.2 子モデルを生成する

次に、Vehicleを継承するCarMotorcycleBicycleモデルをそれぞれ生成します。これらのモデルは独自のテーブルを持たない代わりに、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モデルと同様の作業を繰り返します。

5.3 レコードを作成する

以下を実行してCarモデルのレコードを作成します。

Car.create(color: "Red", price: 10000)

実際に生成されるSQLは次のようになります。

INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000)

5.4 レコードのクエリを送信する

Carのレコードを取得するクエリを送信すると、vehiclesテーブル内のCarに該当するレコードだけが検索されます。

Car.all

実際のクエリは次のようになります。

SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car')

5.5 モデルに固有の振る舞いを追加する

STIでは、子モデルに特定の振る舞いやメソッドを追加できます。たとえば、Carモデルにメソッドを追加するには以下のように書きます。

class Car < Vehicle
  def honk
    "Beep Beep"
  end
end

これで、Carモデルのインスタンスでのみhonkメソッドを呼び出せるようになります。

car = Car.first
car.honk
# => 'Beep Beep'

5.6 コントローラーを追加する

STIの子モデルには、独自のコントローラも追加できます。たとえば以下のCarsControllerを追加できます。

# app/controllers/cars_controller.rb

class CarsController < ApplicationController
  def index
    @cars = Car.all
  end
end

5.7 継承カラムをオーバーライドする

レガシーデータベースで作業する場合などで、継承カラム名をオーバーライドする必要が生じることがあります。これは、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がカスタムカラム名で正しく機能できるようになります。

5.8 継承カラムを無効にする

レガシーデータベースで作業する場合などで、単一テーブル継承(STI)を完全に無効にする必要が生じることがあります(そうしないとActiveRecord::SubclassNotFoundが発生する)。

inheritance_columnnilに設定することで、STIを無効にできます。

# スキーマ: 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を既存のデータベースと統合する場合や、モデルに特定のカスタマイズが必要な場合に、柔軟な対応が可能になります。

5.9 STIで考慮すべき点

単一テーブル継承(STI)は、サブクラス同士(およびその属性)にほとんど違いがない場合に最適ですが、すべてのサブクラスのすべての属性が1個のテーブルに収納されることになります。

この方法の欠点は、サブクラス固有の属性を(他のサブクラスで使われていない属性であっても)1個のテーブルに含めるため、テーブルが肥大化する可能性があることです。これについては、後述のDelegated Typesで解決できることがあります。

さらに、ポリモーフィック関連付けを使っている場合は、1個のモデルがtypeとIDを介して他の複数のモデルに属している可能性があるため、関連付けロジックがさまざまなモデルtypeを正しく処理するための参照整合性を維持する処理が複雑になる可能性があります。

最後に、データ整合性チェックやバリデーションがサブクラスごとに異なる場合は、特に外部キー制約を設定するときに、Railsやデータベースでデータ整合性チェックやバリデーションが正しく処理されるようにしておく必要があります。

6 Delegated Types

Delegated types(委譲型)は、単一テーブル継承(STI)によるテーブル肥大化の問題を、delegated_typeで解決します。このアプローチにより、共有属性をスーパークラスのテーブルに保存し、サブクラス固有の属性を別のテーブルに保存できるようになります。

6.1 Delegated Typesをセットアップする

Delegated typesを使うためには、データを以下のようにモデリングする必要があります。

  • すべてのサブクラス間で共有する属性をそのテーブルに格納するためのスーパークラスが1個存在すること。
  • 個別のサブクラスは必ずそのスーパークラスを継承し、サブクラス固有の追加属性については別途テーブルを用意すること。

これにより、単一のテーブルで、すべてのサブクラス間で不必要に共有される属性を定義する必要がなくなります。

6.2 モデルを生成する

上記の例に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

6.3 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が保存されます。

6.4 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を返す

6.5 オブジェクトを作成する

新しいEntryオブジェクトを作成する際に、entryableサブクラスを同時に指定できます。

Entry.create! entryable: Message.new(subject: "hello!")

6.6 さらに委譲を追加する

Entryデリゲータを拡張してdelegatesを定義し、サブクラスに対してポリモーフィズムを使用することで、さらに拡張できます。

たとえば、Entrytitleメソッドをそのサブクラスに委譲するには以下のようにします。

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

このセットアップによって、Entrytitleメソッドをサブクラスに委譲できるようになります。Messageモデルはsubjectメソッドを利用し、Commentモデルはcontentメソッドの結果をtruncateしたものを利用できるようになります。

7 ヒントと注意事項

RailsアプリケーションでActive Recordの関連付けを効果的に使うためには、以下について知っておく必要があります。

  • 関連付けのキャッシュを制御する
  • 名前衝突の回避
  • スキーマの更新
  • 関連付けのスコープ制御
  • 双方向関連付け

7.1 関連付けのキャッシュを制御する

すべての関連付けメソッドは、キャッシュを中心に構築されています。最後に実行したクエリの結果はキャッシュに保持され、次回以降の操作で利用されます。このキャッシュは、以下のようにメソッド間でも共有される点にご注意ください。

# データベースからbooksを取得する
author.books.load

# booksのキャッシュコピーが使われる
author.books.size

# booksのキャッシュコピーが使われる
author.books.empty?

このauthor.booksを実行しても、それだけではデータベースからすぐにデータを読み込みません。代わりに、実際にデータを使おうとしたとき(例: eachsizeempty?などの「データを必要とするメソッド」を呼び出したとき)に実行されるクエリを単にセットアップします。

データを利用する他のメソッドを呼び出す前にauthor.books.loadメソッドを呼び出すと、データベースからデータを読み込むクエリがその場で明示的にトリガーされます。このテクニックは、データが必要であることが事前にわかっていて、関連付けを操作するときにクエリが何度もトリガーされることによるパフォーマンス上の潜在的なオーバーヘッドを回避したい場合に便利です。

しかし、アプリケーションの他の部分によってデータが変更されている可能性があるため、キャッシュを再読み込みしたい場合は、その関連付けでreloadメソッドを呼び出すだけで再読み込みできます。

# データベースからbooksを取得する
author.books.load

# booksのキャッシュコピーが使われる
author.books.size

# booksのキャッシュコピーが破棄され、その後データベースから再度読み込まれる
author.books.reload.empty?

7.2 名前衝突の回避

Ruby on Railsのモデルで関連付けを作成する場合、ActiveRecord::Baseのインスタンスメソッドで既に使われている名前を関連付け名で使わないようにすることが重要です。既存のメソッドと競合する名前で関連付けを作成すると、ベースメソッドがオーバーライドされて機能に問題が発生するなど、意図しない結果につながる可能性があります。たとえば、関連付け名にattributesconnectionなどを使うと問題が発生します。

7.3 スキーマの更新

関連付けは非常に便利です。関連付けは、モデル間のリレーションシップを定義する役割を担いますが、データベーススキーマの更新は行いません。つまり、関連付けとデータベーススキーマを常に一致させる責任はアプリケーション開発者にあります。そのために、主に以下の2つのタスクについては手作業が必要です。

  1. belongs_to関連付けを使う場合は、外部キーを作成する必要があります。
  2. has_and_belongs_to_many関連付けを使う場合は、適切なjoinテーブルを作成する必要があります。

has_many :throughhas_and_belongs_to_manyの使い分けについて詳しくは、has_many :throughhas_and_belongs_to_manyのどちらを選ぶかを参照してください。

7.3.1 belongs_to関連付けに対応する外部キーを作成する

belongs_to関連付け関連付けを宣言するときは、対応する外部キーも作成する必要があります。以下のモデルを例にとります。

class Book < ApplicationRecord
  belongs_to :author
end

上の宣言は、以下のbooksテーブルの対応する外部キーカラムと整合していなければなりません。テーブルを作成した直後のマイグレーションは、以下のような感じになります。

class CreateBooks < ActiveRecord::Migration[8.0]
  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[8.0]
  def change
    add_reference :books, :author
  end
end
7.3.2 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[8.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

このjoinテーブルはモデルを表すためのものではないので、create_tableid: falseオプションを指定します。 モデルのIDが破損する、IDの競合で例外が発生するなど、has_and_belongs_to_many関連付けの動作が怪しい場合は、マイグレーション作成時にid: falseオプションの設定を忘れていないかどうか再度確認してみてください。

なお、以下のようにcreate_join_tableメソッドを使えば、同じことをもっとシンプルに書けます。

class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[8.0]
  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マイグレーションガイドを参照してください。

7.3.3 has_many :throughに対応するjoinテーブルを作成する

has_many :through関連付けとhas_and_belongs_to_many関連付けでjoinテーブルを作成する場合の、スキーマ実装方法の主な違いは、has_many :throughのjoinテーブルにはidが必須であることです。

class CreateAppointments < ActiveRecord::Migration[8.0]
  def change
    create_table :appointments do |t|
      t.belongs_to :physician
      t.belongs_to :patient
      t.datetime :appointment_date
      t.timestamps
    end
  end
end

7.4 関連付けのスコープを制御する

関連付けは、デフォルトでは現在のモジュールのスコープ内にあるオブジェクトだけを探索します。この機能は、特に以下のようにモジュール内で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にしなければなりません。

ただし、以下のようにSupplierAccountモデルが異なるスコープで定義されている場合、この関連付けはデフォルトでは機能しません。

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オプションを明示的に宣言することで、名前空間が異なるモデル間で関連付けを作成し、モジュールのスコープに関係なく正しいモデルがリンクされるようになります。

7.5 双方向関連付け

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
    
  • 関連付けのpresenceabsenceがバリデーションされるケースが増える

    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_inversingtrueに設定していない場合、関連付け自体にカスタムスコープを設定したときと同様に、逆方向の関連付けにカスタムスコープを設定したときの自動認識も効かなくなります。

たとえば、カスタム外部キーを含む次のモデル宣言を考えてみましょう。

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
    
  • presenceabsenceバリデーションが失敗する

    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が双方向関連付けを認識して、上述の最初の例と同じように動作するようになります。

8 関連付けの詳しい参考情報

8.1 オプション

Railsで使われているインテリジェントなデフォルト設定は、ほとんどの状況で適切に機能しますが、関連付け参照の動作をカスタマイズしたい場合もあります。 このようなカスタマイズは、関連付けを作成するときにオプションブロックを渡すことで実現できます。たとえば、以下の関連付けでは、そうしたオプションが2つ使われています。

class Book < ApplicationRecord
  belongs_to :author, touch: :books_updated_at,
    counter_cache: true
end

どの関連付けも多数のオプションをサポートしています。詳しくは、ActiveRecord Associations APIで個別の関連付けのOptionsセクションを参照してください。ここからは、よくあるユースケースをいくつか解説します。

8.1.1 :class_name

関連付けの相手となるオブジェクト名を関連付け名から生成できない事情がある場合、:class_nameオプションを用いてモデル名を直接指定できます。 たとえば、書籍(book)が著者(author)に従属しているが、実際の著者のモデル名がPatronである場合には、以下のように指定します。

class Book < ApplicationRecord
  belongs_to :author, class_name: "Patron"
end
8.1.1.1 :dependent

:dependentオプションは、オーナーが破棄されたときに、関連付けられているオブジェクトがどう振る舞うかを制御します。

  • :destroy: オブジェクトが破棄されると、関連付けられたオブジェクトに対してdestroyを呼び出します。 このメソッドは、関連付けられたレコードをデータベースから削除するだけでなく、定義されているコールバック(before_destroyafter_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関連付けに切り替えましょう。

8.1.1.2 :foreign_key

Railsの規約では、相手のモデルを指す外部キーを保持しているjoinテーブル上のカラム名には、そのモデル名にサフィックス_idを追加した関連付け名が使われることを前提とします。

:foreign_keyオプションを使えば、外部キー名を直接指定できます。

class Supplier < ApplicationRecord
  has_one :account, foreign_key: "supp_id"
end

Railsは外部キーカラムを自動的に作成しません。外部キーを使うには、マイグレーションを作成して明示的に定義する必要があります。

8.1.1.3 :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値が@userguid値に設定されます。

has_and_belongs_to_many関連付けは、:primary_keyオプションをサポートしていません。代わりにhas_many :through関連付けとjoinテーブルを使えば、同様の機能を実現できます。これにより柔軟性が高まり、:primary_keyオプションがサポートされます。詳しくは、has_many :throughセクションを参照してください。

8.1.1.4 :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セクションを参照してください。

8.1.1.5 :validate

:validateオプションをtrueに設定すると、新たに関連付けられたオブジェクトを保存したときにバリデーションが実行されます。デフォルトはfalseであり、この場合新たに関連付けられたオブジェクトは保存時にバリデーションされません。

has_and_belongs_to_many関連付けは、:validateオプションをサポートしていません。代わりにhas_many :through関連付けとjoinテーブルを使えば、同様の機能を実現できます。詳しくは、has_many :throughセクションを参照してください。

8.1.1.6 :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
8.1.1.7 :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
8.1.1.8 :strict_loading

trueを指定すると、関連付けられるレコードが、この関連付けを経由して読み込まれるたびにstrict loadingを強制するようになります。

8.1.1.9 :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を行いたいときに便利です。

8.1.1.10 :join_table

:join_tableオプションは、has_and_belongs_to_many関連付けで利用可能です。辞書順に基いて生成されたjoinテーブルのデフォルト名では不都合がある場合、:join_tableオプションを用いてデフォルトのテーブル名を上書きできます。

8.2 スコープ

スコープ(scope)を使うと、関連付けオブジェクトのメソッド呼び出しとして参照可能な共通クエリを指定できます。スコープは、以下のようにアプリケーション内の複数の場所で再利用されるカスタムクエリを定義する場合に便利です。

class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies, -> { where active: true }
end
8.2.1 汎用のスコープ

スコープブロック内では標準のクエリメソッドをすべて利用できます。ここでは以下について説明します。

  • where
  • includes
  • readonly
  • select
8.2.1.1 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"であるアセンブリが作成されます。

8.2.1.2 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されます。

8.2.1.3 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する
8.2.1.4 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カラムだけが取得されます。

selectbelongs_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
8.2.2 コレクションのスコープ

has_manyhas_and_belongs_to_manyはレコードのコレクションを扱う関連付けなので、grouplimitorderselectdistinctなどの追加メソッドを用いて、関連付けで使われるクエリをカスタマイズできます。

8.2.2.1 group

groupメソッドは、結果をグループ化する属性名を1つ指定します。内部的にはSQLのGROUP BY句が使われます。

class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies, -> { group "factory" }
end
8.2.2.2 limit

limitメソッドは、関連付けを用いて取得できるオブジェクトの総数の上限を指定するのに使います。

class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies,
    -> { order("created_at DESC").limit(50) }
end
8.2.2.3 order

orderメソッドは、関連付けられたオブジェクトを受け取るときの並び順を指定します。内部的にはSQLのORDER BY句が使われます。

class Author < ApplicationRecord
  has_many :books, -> { order "date_confirmed DESC" }
end
8.2.2.4 select

selectメソッドを使うと、関連付けられたオブジェクトのデータ取り出しに使われるSQLのSELECT句をオーバーライドできます。Railsはデフォルトではすべてのカラムを取り出します。

selectメソッドをカスタマイズする場合、関連付けられているモデルの主キーカラムと外部キーカラムを除外してはいけません。さもないと、Railsでエラーが発生します。

8.2.2.5 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)
8.2.3 関連付けのオーナーで関連付けのスコープを制御する

関連付けのスコープをさらに制御する必要がある状況では、関連付けのオーナーをスコープブロックに引数として渡す方法が使えます。ただし、これを行うと関連付けのプリロードが不可能になる点にご注意ください。

class Supplier < ApplicationRecord
  has_one :account, ->(supplier) { where active: supplier.active? }
end

上の例では、Supplierモデルのaccount関連付けは、供給元(supplier)のactiveステータスに基づいてスコープが設定されます。

関連付けを拡張して、関連付けのオーナーでスコープを設定することで、Railsアプリケーションでより動的でコンテキストに対応した関連付けを作成できるようになります。

8.3 カウンタキャッシュ

: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オプションを利用する前の段階で行っておく必要があります(さもないと、カウンタキャッシュを内部で利用するsizeany?などのメソッドで誤った結果が生成される可能性があります)。

子レコードの作成や削除で発生するカウンタキャッシュのカラム更新を止めずに、値を安全にバックフィルするには、counter_cache: { active: false }オプションを指定します。このオプションを指定している間は、上述のメソッドでカウンタキャッシュカラムの誤った値を使わなくなり、常にデータベースから結果を取得するようになります。

カスタムのカラム名も指定する必要がある場合は、counter_cache: { active: false, column: :my_custom_counter }を使います。

何らかの理由でオーナーモデルの主キーの値を変更し、カウントされたモデルの外部キーも更新しなかった場合、カウンタキャッシュのデータが古くなっている可能性があります(つまり、孤立したモデルも引き続きカウンタでカウントされます)。古くなったカウンタキャッシュを修正するには、reset_countersをお使いください。

8.4 関連付けのコールバック

通常のコールバックは、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モデルはbookshas_many関連付けがあります。before_addコールバックで指定したcheck_credit_limitは、書籍がコレクションに追加される前にトリガーされます。limit_reached?メソッドがtrueを返す場合、書籍はコレクションに追加されません。

これらの関連付けコールバックを活用することで、関連付けの振る舞いをカスタマイズして、コレクションのライフサイクルの重要なポイントで特定のアクションを実行できるようになります。

関連付けのコールバックについて詳しくは、Active Recordコールバックガイドを参照してください。

8.5 関連付けの拡張

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を介して関連付けの名前にアクセスします。

9 参考資料(日本語)

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加