更多内容请访问 rubyonrails.org:

1 关联概述

Active Record 关联允许您定义模型之间的关系。关联 是以特殊宏风格调用实现的,使您能够轻松地告诉 Rails 您的模型如何相互关联,这将帮助您更有效地管理数据,并使常见操作更简单易读。

宏风格调用是一种在运行时生成或修改其他方法的方法,允许简洁且表达性地声明功能,例如在 Rails 中定义模型关联。例如,has_many :comments

当您设置关联时,Rails 将帮助定义和管理 主键外键 关系两个模型的实例之间,而数据库确保您的数据保持一致并正确链接。

这使得跟踪哪些记录相关变得容易。它还为您的模型添加了有用的方法,因此您可以更轻松地使用相关数据。

考虑一个简单的 Rails 应用程序,其中包含作者和书籍的模型。

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

要为现有作者添加新书籍,您需要在创建书籍时提供 author_id 值。

@book = Book.create(author_id: @author.id, published_at: Time.now)

要删除作者并确保所有书籍也被删除,您需要检索所有作者的 books,循环遍历每个 book 来销毁它,然后销毁作者。

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

1.2 使用关联

但是,使用关联,我们可以通过明确告知 Rails 两个模型之间的关系来简化这些操作以及其他操作。以下是使用关联设置作者和书籍的修改后的代码

class Author < ApplicationRecord
  has_many :books, dependent: :destroy
end

class Book < ApplicationRecord
  belongs_to :author
end

有了这个更改,为特定作者创建新书籍就变得更简单了

@book = @author.books.create(published_at: Time.now)

删除作者及其所有书籍要容易得多

@author.destroy

当您在 Rails 中设置关联时,您仍然需要创建一个 迁移 以确保数据库正确配置以处理关联。此迁移需要将必要的外国键列添加到您的数据库表中。

例如,如果您在 Book 模型中设置 belongs_to :author 关联,您将创建一个迁移来将 author_id 列添加到 books 表中

rails generate migration AddAuthorToBooks author:references

此迁移将添加 author_id 列并在数据库中设置外键关系,确保您的模型和数据库保持同步。

要详细了解不同类型的关联,您可以阅读本指南的下一节。接下来,您将找到一些使用关联的技巧和窍门。最后,有一个关于 Rails 中关联方法和选项的完整参考。

2 关联类型

Rails 支持六种类型的关联,每种关联都有特定的用例。

以下是所有支持类型的列表,以及指向其 API 文档的链接,以获取有关如何使用它们、其方法参数等的更详细的信息。

在本指南的剩余部分,您将学习如何声明和使用各种形式的关联。首先,让我们快速了解每种关联类型适合的场景。

2.1 belongs_to

belongs_to 关联会与另一个模型建立关系,使得声明模型的每个实例都“属于”另一个模型的一个实例。例如,如果您的应用程序包含作者和书籍,并且每本书只能分配给一位作者,那么您将这样声明书籍模型

class Book < ApplicationRecord
  belongs_to :author
end

belongs_to Association Diagram

belongs_to 关联必须使用单数形式。如果您使用复数形式,例如在 Book 模型中使用 belongs_to :authors,并尝试使用 Book.create(authors: @author) 创建书籍,Rails 会给您一个“未初始化常量 Book::Authors”错误。这是因为 Rails 会根据关联名称自动推断类名。如果关联名称是 :authors,Rails 将查找名为 Authors 的类,而不是 Author

相应的迁移可能如下所示

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 关联表示该模型的表包含一个代表对另一个表的引用的列。这可用于根据设置建立一对一或一对多关系。如果另一类的表在一个对一关系中包含引用,那么您应该使用 has_one 而不是。

单独使用时,belongs_to 会产生一个单向一对一关系。因此,在上面的示例中,每本书都“知道”它的作者,但作者不知道他们的书。要设置双向关联 - 在另一个模型(在本例中为 Author 模型)中使用 belongs_tohas_onehas_many 结合使用。

默认情况下,belongs_to 会验证关联记录的存在以确保引用一致性。

如果在模型中将 optional 设置为 true,则 belongs_to 不会保证引用一致性。这意味着一个表中的外键可能无法可靠地指向被引用表中的有效主键。

class Book < ApplicationRecord
  belongs_to :author, optional: true
end

因此,根据使用情况,您可能还需要在引用列上添加数据库级别的外键约束,如下所示

create_table :books do |t|
  t.belongs_to :author, foreign_key: true
  # ...
end

这可以确保即使 optional: true 允许 author_id 为 NULL,当它不为 NULL 时,它仍然必须引用作者表中的有效记录。

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?

我们将讨论一些常用的方法,您可以在 ActiveRecord 关联 API 中找到详尽的列表。

在所有上述方法中,association 都被替换为作为第一个参数传递给 belongs_to 的符号。例如,给出以下声明

# 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_onebelongs_to 关联时,您必须使用 build_ 前缀来构建关联,而不是用于 has_manyhas_and_belongs_to_many 关联的 association.build 方法。要创建一个,请使用 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 方法更进一步,它还在关联对象通过关联模型上指定的所有验证后保存它。

@author = @book.create_author(author_number: 123,
                              author_name: "John Doe")

最后,create_association! 执行相同的操作,但如果记录无效,则会引发 ActiveRecord::RecordInvalid

# This will raise ActiveRecord::RecordInvalid because the name is blank
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? 混淆。前者检查关联是否已被替换为新记录,而后者跟踪关联属性的更改。

2.1.1.4 检查现有关联

您可以使用 association.nil? 方法查看是否存在任何关联对象

if @book.author.nil?
  @msg = "No author found for this book"
end
2.1.1.5 关联对象的保存行为

将对象分配给 belongs_to 关联不会自动保存当前对象或关联对象。但是,当您保存当前对象时,关联也会被保存。

2.2 has_one

has_one 关联表示另一个模型对该模型有一个引用。可以通过此关联获取该模型。

例如,如果您的应用程序中的每个供应商只有一个帐户,您将像这样声明供应商模型

class Supplier < ApplicationRecord
  has_one :account
end

belongs_to 的主要区别在于链接列(在本例中为 supplier_id)位于另一个表中,而不是声明 has_one 的表中。

has_one Association Diagram

相应的迁移可能如下所示

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 关联与另一个模型创建一对一匹配。在数据库术语中,此关联表示另一个类包含外键。如果此类包含外键,则应使用 belongs_to 而不是它。

根据使用情况,您可能还需要在帐户表中创建对供应商列的唯一索引和/或外键约束。唯一索引确保每个供应商只与一个帐户相关联,并允许您以高效的方式进行查询,而外键约束则确保帐户表中的 supplier_id 引用供应商表中的有效 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

我们将讨论一些常用的方法,您可以在 ActiveRecord 关联 API 中找到详尽的列表。

belongs_to 引用 一样,在所有这些方法中,association 都被替换为作为第一个参数传递给 has_one 的符号。例如,给出以下声明

# 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_onebelongs_to 关联时,您必须使用 build_ 前缀来构建关联,而不是用于 has_manyhas_and_belongs_to_many 关联的 association.build 方法。要创建一个,请使用 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 方法更进一步,它还在关联对象通过关联模型上指定的所有验证后保存它。

@account = @supplier.create_account(terms: "Net 30")

最后,create_association! 与上面的 create_association 执行相同的操作,但如果记录无效,则会引发 ActiveRecord::RecordInvalid

# This will raise ActiveRecord::RecordInvalid because the terms is blank
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 = "No account found for this supplier"
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,但表示与另一个模型的一对多关系。您经常会在 belongs_to 关联的“另一边”找到此关联。此关联表示模型的每个实例都具有另一个模型的零个或多个实例。例如,在包含作者和书籍的应用程序中,作者模型可以像这样声明

class Author < ApplicationRecord
  has_many :books
end

has_many 在模型之间建立一对多关系,允许声明模型(Author)的每个实例都具有关联模型(Book)的多个实例。

has_onebelongs_to 关联不同,在声明 has_many 关联时,另一个模型的名称会变为复数形式。

has_many Association Diagram

相应的迁移可能如下所示

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 关联与另一个模型创建一对多关系。在数据库术语中,此关联表示另一个类将具有一个外键,该外键引用此类的实例。

在此迁移中,authors 表是用一个 name 列创建的,用于存储作者的姓名。books 表也被创建,它包含一个 belongs_to :author 关联。此关联在 booksauthors 表之间建立外键关系。具体来说,books 表中的 author_id 列充当外键,引用 authors 表中的 id 列。通过在 books 表中包含此 belongs_to :author 关联,我们确保每本书都与单个作者相关联,从而实现来自 Author 模型的 has_many 关联。此设置允许每个作者拥有多个关联的书籍。

根据使用情况,通常最好在书籍表中创建对作者列的非唯一索引,并选择性地创建外键约束。在 author_id 列上添加索引可以提高检索与特定作者关联的书籍时的查询性能。

如果您希望在数据库级别强制执行 引用完整性,请将 foreign_key: true 选项添加到上面的 reference 列声明中。这将确保书籍表中的 author_id 必须对应于作者表中的有效 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 关联时,声明类会获得与该关联相关的许多方法。其中一些是

我们将讨论一些常见的方法,但您可以在 ActiveRecord 关联 API 中找到完整的列表。

在所有这些方法中,collection 被替换为传递给 has_many 的第一个参数的符号,而 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 方法返回所有关联对象的 Relation。如果没有关联对象,则返回一个空的 Relation。

@books = @author.books

collection.delete 方法通过将它们的外部键设置为 NULL 从集合中删除一个或多个对象。

@author.books.delete(@book1)

此外,如果对象与 dependent: :destroy 相关联,则将被销毁;如果对象与 dependent: :delete_all 相关联,则将被删除。

collection.destroy 方法通过对每个对象运行 destroy 从集合中删除一个或多个对象。

@author.books.destroy(@book1)

对象始终将从数据库中删除,忽略 :dependent 选项。

collection.clear 方法根据 dependent 选项指定的策略从集合中删除所有对象。如果没有给出选项,则遵循默认策略。has_many :through 关联的默认策略是 delete_all,而 has_many 关联的默认策略是将外部键设置为 NULL

@author.books.clear

如果对象与 dependent: :destroydependent: :destroy_async 相关联,则对象将被删除,就像 dependent: :delete_all 一样。

collection.reload 方法返回所有关联对象的 Relation,强制进行数据库读取。如果没有关联对象,则返回一个空的 Relation。

@books = @author.books.reload
2.3.1.2 赋值集合

collection=(objects) 方法通过添加和删除适当的对象,使集合仅包含提供的对象。这些更改将持久保存到数据库中。

collection_singular_ids=(ids) 方法通过添加和删除适当的对象,使集合仅包含由提供的主键值标识的对象。这些更改将持久保存到数据库中。

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 方法根据提供的条件在集合中查找对象,但这些对象是延迟加载的,这意味着只有在访问对象时才会查询数据库。

@available_books = @author.books.where(available: true) # No query yet
@available_book = @available_books.first # Now the database will be queried

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

2.3.1.5 何时保存对象?

当您将一个对象分配给 has_many 关联时,该对象将自动保存(为了更新其外部键)。如果您在一个语句中分配多个对象,那么它们都会被保存。

如果由于验证错误而导致其中任何保存失败,则赋值语句将返回 false,并且赋值本身将被取消。

如果父对象(声明 has_many 关联的对象)未保存(即,new_record? 返回 true),则在添加子对象时不会保存子对象。关联的所有未保存成员将在父对象保存时自动保存。

如果您想将对象分配给 has_many 关联而无需保存对象,请使用 collection.build 方法。

2.4 has_many :through

通常使用 has_many :through 关联来建立与另一个模型的多对多关系。此关联表明,声明模型可以通过通过第三个模型与另一个模型的零个或多个实例匹配。

例如,考虑一家医疗机构,患者在那里预约看医生。相关的关联声明可能如下所示

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)的实例通过第三个“连接”模型(Appointment)与另一个模型(Patient)的多个实例相关联。

has_many :through Association
Diagram

相应的迁移可能如下所示

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

在此迁移中,physicianspatients 表使用 name 列创建。appointments 表充当连接表,使用 physician_idpatient_id 列创建,在 physicianspatients 之间建立多对多关系。

您也可以考虑在 has_many :through 关系中使用 组合主键,如下所示

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 关联中的连接模型集合可以使用标准的 has_many 关联方法 进行管理。例如,如果您将患者列表分配给医生,如下所示

physician.patients = patients

Rails 会自动为新列表中之前未与医生关联的任何患者创建新的连接模型。此外,如果之前与医生关联的任何患者未包含在新列表中,则他们的连接记录将自动删除。这通过为您处理连接模型的创建和删除,简化了多对多关系的管理。

连接模型的自动删除是直接的,不会触发任何 destroy 回调。您可以在 Active Record 回调指南 中了解更多有关回调的信息。

has_many :through 关联对于通过嵌套的 has_many 关联设置“快捷方式”也很有用。当您需要通过中间关联访问相关记录的集合时,这尤其有用。

例如,如果文档有许多部分,每个部分有许多段落,您有时可能想要获取文档中所有段落的简单集合,而无需手动遍历每个部分。

您可以使用 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 关联通过中间模型与另一个模型建立一对一关系。此关联表明,声明模型可以通过通过第三个模型与另一个模型的一个实例匹配。

例如,如果每个供应商都有一个帐户,每个帐户都与一个帐户历史记录相关联,则供应商模型可能如下所示

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

has_one :through Association
Diagram

用于设置这些关联的相应迁移可能如下所示

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 关联创建与另一个模型的直接多对多关系,没有中间模型。此关联表明,声明模型的每个实例都引用另一个模型的零个或多个实例。

例如,考虑一个具有 AssemblyPart 模型的应用程序,其中每个组件可以包含多个零件,每个零件可以在多个组件中使用。您可以按如下方式设置模型

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 Association
Diagram

即使 has_and_belongs_to_many 不需要中间模型,它也需要一个单独的表来建立两个模型之间多对多关系。这个中间表用于存储相关数据,映射两个模型实例之间的关联。该表不需要主键,因为其目的仅仅是管理关联记录之间的关系。相应的迁移可能如下所示

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

    # Create a join table to establish the many-to-many relationship between assemblies and parts.
    # `id: false` indicates that the table does not need a primary key of its own
    create_table :assemblies_parts, id: false do |t|
      # creates foreign keys linking the join table to the `assemblies` and `parts` tables
      t.belongs_to :assembly
      t.belongs_to :part
    end
  end
end

has_and_belongs_to_many 关联创建与另一个模型的多对多关系。在数据库术语中,这通过一个中间连接表将两个类关联起来,该表包含引用每个类的外部键。

如果 has_and_belongs_to_many 关联的连接表除了两个外部键之外还有其他列,则这些列将作为属性添加到通过该关联检索的记录中。使用额外属性返回的记录将始终是只读的,因为 Rails 无法保存对这些属性的更改。

has_and_belongs_to_many 关联中使用连接表上的额外属性已被弃用。如果您需要在多对多关系中连接两个模型的表上进行这种复杂的行为,则应使用 has_many :through 关联而不是 has_and_belongs_to_many

2.6.1 has_and_belongs_to_many 添加的方法

当您声明一个 has_and_belongs_to_many 关联时,声明类会获得与该关联相关的许多方法。其中一些是

我们将讨论一些常见的关联方法,但您可以从ActiveRecord 关联 API中找到详尽的列表。

在所有这些方法中,collection将被替换为传递给has_and_belongs_to_many作为第一个参数的符号,而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 方法返回所有关联对象的 Relation。如果没有关联对象,则返回一个空的 Relation。

@assemblies = @part.assemblies

collection<<方法通过在连接表中创建记录来将一个或多个对象添加到集合中。

@part.assemblies << @assembly1

此方法的别名为collection.concatcollection.push

collection.delete方法通过删除连接表中的记录来从集合中删除一个或多个对象。这不会销毁这些对象。

@part.assemblies.delete(@assembly1)

collection.destroy方法通过删除连接表中的记录来从集合中删除一个或多个对象。这不会销毁这些对象。

@part.assemblies.destroy(@assembly1)

collection.clear方法通过删除连接表中的行来从集合中删除所有对象。这不会销毁关联的对象。

2.6.1.2 赋值集合

collection=方法通过添加和删除必要的记录来使集合仅包含提供的对象。这些更改将持久化到数据库。

collection_singular_ids=方法通过添加和删除必要的记录来使集合仅包含由提供的 primary key 值标识的对象。这些更改将持久化到数据库。

2.6.1.3 查询集合

collection_singular_ids 方法返回集合中对象的 id 数组。

@assembly_ids = @part.assembly_ids

collection.empty? 方法如果集合不包含任何关联对象,则返回 true

<% if @part.assemblies.empty? %>
  This part is not used in any assemblies
<% end %>

collection.size 方法返回集合中对象的数量。

@assembly_count = @part.assemblies.size

collection.find 方法在集合的表中查找对象。

@assembly = @part.assemblies.find(1)

collection.where 方法根据提供的条件在集合中查找对象,但这些对象是延迟加载的,这意味着只有在访问对象时才会查询数据库。

@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)

collection.exists? 方法检查集合的表中是否存在满足提供条件的对象。

2.6.1.4 构建和创建关联对象

collection.build方法返回关联类型的一个新对象。此对象将从传递的属性实例化,并且将通过连接表创建链接,但关联对象不会被保存。

@assembly = @part.assemblies.build({ assembly_name: "Transmission housing" })

collection.create方法返回关联类型的一个新对象。此对象将从传递的属性实例化,并且将通过连接表创建链接,并且,一旦它通过了关联模型上指定的所有验证,关联对象被保存。

@assembly = @part.assemblies.create({ assembly_name: "Transmission housing" })

collection.create相同,但如果记录无效则会引发ActiveRecord::RecordInvalid

collection.reload 方法返回所有关联对象的 Relation,强制进行数据库读取。如果没有关联对象,则返回一个空的 Relation。

@assemblies = @part.assemblies.reload
2.6.1.5 对象何时被保存?

当您将一个对象赋值给has_and_belongs_to_many关联时,该对象会自动保存(为了更新连接表)。如果您在一个语句中赋值多个对象,那么它们都会被保存。

如果由于验证错误而导致其中任何保存失败,则赋值语句将返回 false,并且赋值本身将被取消。

如果父对象(声明has_and_belongs_to_many关联的对象)未保存(即,new_record?返回true),那么子对象在被添加时不会被保存。关联的所有未保存的成员会在父对象保存时自动被保存。

如果您想将一个对象赋值给has_and_belongs_to_many关联而不保存该对象,请使用collection.build方法。

3 选择关联

3.1 belongs_to vs has_one

如果您想在两个模型之间建立一对一的关系,可以选择belongs_tohas_one关联。如何知道应该选择哪一个?

区别在于外键的放置位置,外键位于声明belongs_to关联的类的表中。然而,了解语义以确定正确的关联至关重要。

  • belongs_to:此关联表明当前模型包含外键,并且是关系中的子模型。它引用另一个模型,暗示此模型的每个实例都链接到另一个模型的一个实例。
  • has_one:此关联表明当前模型是关系中的父模型,并且它拥有另一个模型的一个实例。

例如,考虑一个包含供应商及其帐户的场景。说一个供应商拥有一个帐户(其中供应商是父模型)比说一个帐户拥有一个供应商更有意义。因此,正确的关联应该是:

  • 一个供应商拥有一个帐户。
  • 一个帐户属于一个供应商。

以下是在 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 :through vs has_and_belongs_to_many

Rails 提供了两种不同的方式来声明模型之间的多对多关系:has_many :throughhas_and_belongs_to_many。了解每种方法的差异和用例可以帮助您为应用程序的需要选择最佳方法。

has_many :through关联通过一个中间模型(也称为连接模型)建立多对多关系。这种方法更灵活,允许您向连接模型添加验证、回调和额外属性。连接表需要一个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关联允许您直接在两个模型之间创建多对多关系,而无需使用中间模型。这种方法很简单,适用于连接表不需要任何额外属性或行为的简单关联。对于has_and_belongs_to_many关联,您需要创建一个没有主键的连接表。

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

  • 关联很简单,并且连接表不需要额外的属性或行为。
  • 您不需要在连接表上进行验证、回调或额外方法。

4 高级关联

4.1 多态关联

关联的一个稍微高级的变体是多态关联。Rails 中的多态关联允许一个模型通过单个关联属于多个其他模型。当您有一个需要链接到不同类型模型的模型时,这将特别有用。

例如,假设您有一个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

Polymorphic Association Diagram

在上面的上下文中,imageable是为关联选择的名称。它是一个符号名称,代表Picture模型与其他模型(如EmployeeProduct)之间的多态关联。重要的是在所有关联模型中一致地使用相同的名称(imageable),以正确建立多态关联。

当您在Picture模型中声明belongs_to :imageable, polymorphic: true时,您是在说一个Picture可以通过此关联属于任何模型(如EmployeeProduct)。

您可以将多态belongs_to声明视为设置一个接口,任何其他模型都可以使用它。这允许您使用@employee.picturesEmployee模型的实例中检索图片集合。同样,您可以使用@product.picturesProduct模型的实例中检索图片集合。

此外,如果您有一个Picture模型的实例,您可以通过@picture.imageable获取其父对象,该对象可以是EmployeeProduct

要手动设置多态关联,您需要在模型中声明一个外键列(imageable_id)和一个类型列(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_id可以是EmployeeProduct的 ID,而imageable_type是关联模型类的名称,因此可以是EmployeeProduct

虽然手动创建多态关联是可以接受的,但建议使用t.references或其别名t.belong_to并指定polymorphic: true,以便 Rails 知道关联是多态的,并且它会自动向表中添加外键列和类型列。

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 代码使用的类名保持同步。重命名类时,请确保更新多态类型列中的数据。

例如,如果您将类名从Product更改为Item,那么您需要运行一个迁移脚本以将pictures表(或任何受影响的表)中的imageable_type列更新为新的类名。此外,您还需要更新整个应用程序代码中对类名的所有其他引用,以反映更改。

4.2 具有复合主键的模型

Rails 通常可以推断关联模型之间的主键-外键关系,但当处理复合主键时,Rails 通常默认仅使用复合主键的一部分,通常是 id 列,除非明确指示其他操作。

如果您在 Rails 模型中使用复合主键,并且需要确保正确处理关联,请参阅复合主键指南的关联部分。本节提供了在 Rails 中设置和使用复合主键关联的全面指南,包括如何在必要时指定复合外键。

4.3 自关联

自连接是一种常规连接,但表本身与自身连接。这在单表中存在层次关系的情况下非常有用。一个常见的例子是员工管理系统,其中员工可以有经理,而该经理也是员工。

考虑一个组织,员工可以担任其他员工的经理。我们希望使用单个 employees 表来跟踪这种关系。

在你的 Rails 模型中,你定义了 Employee 类来反映这些关系

class Employee < ApplicationRecord
  # an employee can have many subordinates.
  has_many :subordinates, class_name: "Employee", foreign_key: "manager_id"

  # an employee can have one manager.
  belongs_to :manager, class_name: "Employee", optional: true
end

has_many :subordinates 建立了一对多关系,其中一个员工可以有多个下属。在这里,我们指定相关模型也是 Employee (class_name: "Employee"),用于识别经理的外键是 manager_id

belongs_to :manager 建立了一对一关系,其中一个员工可以属于一个经理。同样,我们将相关模型指定为 Employee

为了支持这种关系,我们需要在 employees 表中添加一个 manager_id 列。此列引用了另一个员工(经理)的 id

class CreateEmployees < ActiveRecord::Migration[8.0]
  def change
    create_table :employees do |t|
      # Add a belongs_to reference to the manager, which is an employee.
      t.belongs_to :manager, foreign_key: { to_table: :employees }
      t.timestamps
    end
  end
end
  • t.belongs_to :manageremployees 表中添加一个 manager_id 列。
  • foreign_key: { to_table: :employees } 确保 manager_id 列引用了 employees 表的 id 列。

传递给 foreign_keyto_table 选项以及更多内容在 SchemaStatements#add_reference 中有解释。

通过这种设置,你可以在 Rails 应用程序中轻松访问员工的下属和经理。

要获取员工的下属

employee = Employee.find(1)
subordinates = employee.subordinates

要获取员工的经理

manager = employee.manager

5 单表继承 (STI)

单表继承 (STI) 是 Rails 中的一种模式,它允许将多个模型存储在单个数据库表中。当你有不同类型的实体共享公共属性和行为,但也有特定行为时,这很有用。

例如,假设我们有 CarMotorcycleBicycle 模型。这些模型将共享诸如 colorprice 之类的字段,但每个模型都将具有独特的行为。它们还将分别拥有自己的控制器。

5.1 生成基础车辆模型

首先,我们使用共享字段生成基础 Vehicle 模型

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

在这里,type 字段对于 STI 至关重要,因为它存储模型名称 (CarMotorcycleBicycle)。STI 需要此字段来区分存储在同一表中的不同模型。

5.2 生成子模型

接下来,我们生成从 Vehicle 继承的 CarMotorcycleBicycle 模型。这些模型将不会拥有自己的表;相反,它们将使用 vehicles 表。

要生成 Car 模型

$ bin/rails generate model car --parent=Vehicle

为此,我们可以使用 --parent=PARENT 选项,它将生成一个从指定父类继承的模型,并且没有等效的迁移(因为表已经存在)。

这将生成一个从 Vehicle 继承的 Car 模型

class Car < Vehicle
end

这意味着添加到 Vehicle 的所有行为也适用于 Car,例如关联、公共方法等。创建一辆汽车将在 vehicles 表中以 "Car" 作为 type 字段保存它。

MotorcycleBicycle 重复相同的过程。

5.3 创建记录

Car 创建记录

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

这将生成以下 SQL

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

5.4 查询记录

查询汽车记录将只搜索是汽车的车辆

Car.all

将运行类似于以下内容的查询

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

5.5 添加特定行为

你可以在子模型中添加特定行为或方法。例如,在 Car 模型中添加一个方法

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

现在,你可以在 Car 实例上调用 honk 方法

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

5.6 控制器

每个模型都可以拥有自己的控制器。例如,CarsController

# app/controllers/cars_controller.rb

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

5.7 覆盖继承列

在某些情况下(例如在使用遗留数据库时),你可能需要覆盖继承列的名称。这可以通过 inheritance_column 方法来实现。

# Schema: 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>

在此设置中,Rails 将使用 kind 列来存储模型类型,允许 STI 使用自定义列名称正常运行。

5.8 禁用继承列

在某些情况下(例如在使用遗留数据库时),你可能需要完全禁用单表继承。如果你没有正确地禁用 STI,你可能会遇到 ActiveRecord::SubclassNotFound 错误。

要禁用 STI,你可以将 inheritance_column 设置为 nil

# Schema: 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>

在此配置中,Rails 将把类型列视为普通属性,并且不会将其用于 STI 目的。如果你需要使用不遵循 STI 模式的遗留模式,这将很有用。

这些调整在将 Rails 与现有数据库集成或当模型需要特定自定义时提供了灵活性。

5.9 注意事项

单表继承 (STI) 在子类与其属性之间的差异很小的情况下效果最好,但它将所有子类的所有属性包含在一个表中。

这种方法的一个缺点是它会导致表膨胀,因为表将包含特定于每个子类的属性,即使它们没有被其他子类使用。这可以通过使用 委托类型 来解决。

此外,如果你使用的是 多态关联,其中一个模型可以通过类型和 ID 属于多个其他模型,那么保持引用完整性可能会变得很复杂,因为关联逻辑必须正确地处理不同类型。

最后,如果你有特定于子类的特定数据完整性检查或验证,则需要确保 Rails 或数据库正确地处理这些检查或验证,尤其是在设置外键约束时。

6 委托类型

委托类型通过 delegated_type 解决 单表继承 (STI) 的表膨胀问题。这种方法允许我们将共享属性存储在超类表中,并为特定于子类的属性使用单独的表。

6.1 设置委托类型

要使用委托类型,我们需要对数据进行如下建模

  • 有一个超类,它在其表中存储所有子类中的共享属性。
  • 每个子类都必须从超类继承,并且将拥有一个单独的表来存储特定于它的任何其他属性。

这消除了在单表中定义无意中在所有子类之间共享的属性的需要。

6.2 生成模型

为了将此应用于我们上面的示例,我们需要重新生成我们的模型。

首先,让我们生成作为超类的基础 Entry 模型

$ bin/rails generate model entry entryable_type:string entryable_id:integer

然后,我们将生成用于委托的新 MessageComment 模型

$ bin/rails generate model message subject:string body:string
$ bin/rails generate model comment content:string

运行完生成器后,我们的模型应该如下所示

# Schema: entries[ id, entryable_type, entryable_id, created_at, updated_at ]
class Entry < ApplicationRecord
end

# Schema: messages[ id, subject, body, created_at, updated_at ]
class Message < ApplicationRecord
end

# Schema: 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 参数指定用于委托的字段,并将 MessageComment 类型包含为委托类。entryable_typeentryable_id 字段分别存储子类名称和委托子类的记录 ID。

6.4 定义 Entryable 模块

接下来,定义一个模块来实现这些委托类型,方法是在 has_one 关联中声明 as: :entryable 参数。

module Entryable
  extend ActiveSupport::Concern

  included do
    has_one :entry, as: :entryable, touch: true
  end
end

在你的子类中包含创建的模块

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 添加更多委托

我们可以通过定义 delegate 并对子类使用多态性来增强我们的 Entry 委托器。例如,要将 title 方法从 Entry 委托给它的子类

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 的截断版本。

7 提示、技巧和警告

以下是一些在 Rails 应用程序中有效使用 Active Record 关联应该了解的事项

  • 控制缓存
  • 避免名称冲突
  • 更新模式
  • 控制关联范围
  • 双向关联

7.1 控制关联缓存

所有关联方法都构建在缓存的基础上,缓存会保留已加载关联的结果以供进一步操作。缓存甚至在方法之间共享。例如

# retrieves books from the database
author.books.load

# uses the cached copy of books
author.books.size

# uses the cached copy of books
author.books.empty?

当我们使用 author.books 时,数据不会立即从数据库中加载。相反,它会设置一个查询,该查询将在你实际尝试使用数据时执行,例如,通过调用需要数据的 methods,例如 each、size、empty? 等。通过在调用其他使用数据的 methods 之前调用 author.books.load,你可以显式地触发查询以立即从数据库中加载数据。如果你知道你需要这些数据,并且想要避免由于触发多个查询而可能造成的性能开销,这将很有用。

但是,如果你想重新加载缓存,因为数据可能已由应用程序的其他部分更改?只需在关联上调用 reload

# retrieves books from the database
author.books.load

# uses the cached copy of books
author.books.size

# discards the cached copy of books and goes back to the database
author.books.reload.empty?

7.2 避免名称冲突

在 Ruby on Rails 模型中创建关联时,重要的是避免使用已用于 ActiveRecord::Base 的实例方法的名称。这是因为使用与现有方法冲突的名称创建关联会导致意外的后果,例如覆盖基本方法并导致功能问题。例如,使用 attributesconnection 等名称作为关联将是有问题的。

7.3 更新模式

关联非常有用,它们负责定义模型之间的关系,但它们不会更新您的数据库模式。您有责任维护您的数据库模式以匹配您的关联。这通常涉及两个主要任务:为 belongs_to 关联 创建外键,并为 has_many :throughhas_and_belongs_to_many 关联设置正确的联接表。您可以在 has many through vs has and belongs to many 部分 中了解更多关于何时使用 has_many :through vs has_and_belongs_to_many 的信息。

7.3.1belongs_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.belongs_to :author
    end
  end
end

而对于现有表,它可能看起来像这样

class AddAuthorToBooks < ActiveRecord::Migration[8.0]
  def change
    add_reference :books, :author
  end
end

7.3.2has_and_belongs_to_many 关联创建联接表

如果您创建了一个 has_and_belongs_to_many 关联,您需要显式地创建联接表。除非使用 :join_table 选项显式指定联接表的名称,否则 Active Record 将使用类名的词典顺序创建名称。因此,作者和书籍模型之间的联接将给出默认联接表名称“authors_books”,因为“a”在词典排序中排名高于“b”。

无论名称如何,您都必须使用适当的迁移手动生成联接表。例如,考虑这些关联

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

我们将 id: false 传递给 create_table,因为联接表不代表模型。如果您在 has_and_belongs_to_many 关联中观察到任何奇怪的行为,例如模型 ID 被篡改,或者有关冲突 ID 的异常,那么您很可能忘记在创建迁移时设置 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

您可以在 Active Record 迁移指南 中了解更多关于 create_join_table 方法的信息

7.3.3has_many :through 关联创建联接表

has_many :through 创建联接表与为 has_and_belongs_to_many 创建联接表之间的主要区别在于,has_many :through 的联接表需要一个 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

在这个例子中,SupplierAccount 类都在同一个模块 (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

要将模型与不同命名空间中的模型关联,您必须在关联声明中指定完整的类名

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 中,模型之间的关联通常是双向的,这意味着它们需要在两个相关模型中声明。考虑以下示例

class Author < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :author
end

Active Record 将尝试根据关联名称自动识别这两个模型共享一个双向关联。此信息允许 Active Record

  • 防止对已加载数据进行不必要的查询

    Active Record 避免对已加载数据进行额外的数据库查询。

    irb> author = Author.first
    irb> author.books.all? do |book|
    irb>   book.author.equal?(author) # No additional queries executed here
    irb> end
    => true
    
  • 防止数据不一致

    由于只加载了一个 Author 对象的副本,因此有助于防止不一致。

    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
    => true
    
  • 在更多情况下自动保存关联

    irb> author = Author.new
    irb> book = author.books.new
    irb> book.save!
    irb> book.persisted?
    => true
    irb> author.persisted?
    => true
    
  • 在更多情况下验证关联的 存在不存在

    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) # This executes an author query for every book
    irb> end
    => false
    
  • 引用具有不一致数据的模型的多个副本

    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
    => false
    
  • 无法自动保存关联

    irb> author = Author.new
    irb> book = author.books.new
    irb> book.save!
    irb> book.persisted?
    => true
    irb> author.persisted?
    => false
    
  • 无法验证存在或不存在

    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 使用在大多数情况下都能很好工作的智能默认值,但有时您可能希望自定义关联引用的行为。这种自定义可以通过在创建关联时传递选项块来实现。例如,此关联使用两个这样的选项

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

每个关联都支持许多选项,您可以在 ActiveRecord 关联 API 中每个关联的“选项”部分 中阅读更多相关信息。我们将在下面讨论一些常见的用例。

8.1.1 :class_name

如果无法从关联名称推导出其他模型的名称,则可以使用 :class_name 选项来提供模型名称。例如,如果一本书属于一个作者,但包含作者的模型的实际名称是 Patron,则您会这样设置

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

8.1.2 :dependent

控制所有者被销毁时与关联对象发生的情况

  • :destroy,当对象被销毁时,destroy 将在它的关联对象上调用。此方法不仅从数据库中删除关联记录,而且还确保执行任何定义的回调(如 before_destroyafter_destroy)。这对于在删除过程中执行自定义逻辑很有用,例如日志记录或清理相关数据。
  • :delete,当对象被销毁时,它所有的关联对象将直接从数据库中删除,而不会调用它们的 destroy 方法。此方法执行直接删除,并绕过关联模型中的任何回调或验证,使其更高效,但如果跳过了重要的清理任务,可能会导致数据完整性问题。当您需要快速删除记录并且确信关联记录不需要任何其他操作时,请使用 delete
  • :destroy_async: 当对象被销毁时,一个 ActiveRecord::DestroyAssociationAsyncJob 任务会被排队,它将在它的关联对象上调用销毁。Active Job 必须设置好才能工作。如果关联由数据库中的外键约束支持,请勿使用此选项。外键约束操作将在与删除其所有者相同的交易中发生。
  • :nullify 会导致外键被设置为 NULL。多态类型列也会在多态关联中被设置为 NULL。回调不会执行。
  • :restrict_with_exception 会导致在存在关联记录时引发 ActiveRecord::DeleteRestrictionError 异常
  • :restrict_with_error 会导致如果存在关联对象,则向所有者添加错误

您不应该在与另一个类上的 has_many 关联连接的 belongs_to 关联上指定此选项。这样做会导致数据库中出现孤立的记录,因为销毁父对象可能会尝试销毁它的子对象,而子对象又可能会尝试再次销毁父对象,导致不一致。

不要在具有 NOT NULL 数据库约束的关联中保留 :nullify 选项。设置 dependent:destroy 至关重要;否则,关联对象的外部键可能会被设置为 NULL,阻止对其进行更改。

:dependent 选项在 :through 选项中被忽略。当使用 :through 时,联接模型必须有一个 belongs_to 关联,并且删除只会影响联接记录,而不会影响关联记录。

当在范围关联上使用 dependent: :destroy 时,只有范围对象会被销毁。例如,在定义为 has_many :comments, -> { where published: true }, dependent: :destroyPost 模型中,调用帖子上的 destroy 将只删除已发布的评论,保留未发布的评论,其外键指向已删除的帖子。

您不能直接在 has_and_belongs_to_many 关联上使用 :dependent 选项。要管理联接表记录的删除,请手动处理它们或切换到 has_many :through 关联,它提供了更大的灵活性并支持 :dependent 选项。

8.1.3 :foreign_key

按照惯例,Rails 假设用于在此模型上保存外键的列是关联的名称,并在其后添加 _id 后缀。:foreign_key 选项允许您直接设置外键的名称

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

Rails 不会为您创建外键列。您需要在迁移中显式定义它们。

8.1.4 :primary_key

默认情况下,Rails 使用 id 列作为其表的主键。:primary_key 选项允许您指定另一个列作为主键。

例如,如果 users 表使用 guid 作为主键而不是 id,并且您希望 todos 表将 guid 作为外键 (user_id) 引用,则可以这样配置它

class User < ApplicationRecord
  self.primary_key = "guid" # Sets the primary key to guid instead of id
end

class Todo < ApplicationRecord
  belongs_to :user, primary_key: "guid" # References the guid column in users table
end

当您执行 @user.todos.create 时,@todo 记录的 user_id 值将被设置为 @userguid 值。

has_and_belongs_to_many 不支持 :primary_key 选项。对于这种类型的关联,您可以通过使用带有 has_many :through 关联的联接表来实现类似的功能,这提供了更大的灵活性并支持 :primary_key 选项。您可以在 has_many :through 部分 中阅读更多相关信息。

8.1.5 :touch

如果将:touch选项设置为true,那么每次保存或销毁相关对象时,关联对象的updated_atupdated_on时间戳将被设置为当前时间。

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

class Author < ApplicationRecord
  has_many :books
end

在这种情况下,保存或销毁书籍将更新关联作者的时间戳。您还可以指定要更新的特定时间戳属性。

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

has_and_belongs_to_many不支持:touch选项。对于这种类型的关联,您可以使用带有has_many :through关联的联接表来实现类似的功能。您可以在has_many :through部分中了解更多信息。

8.1.6 :validate

如果将:validate选项设置为true,那么每次保存此对象时,新的关联对象将被验证。默认情况下,此值为false:保存此对象时,不会验证新的关联对象。

has_and_belongs_to_many不支持:validate选项。对于这种类型的关联,您可以使用带有has_many :through关联的联接表来实现类似的功能。您可以在has_many :through部分中了解更多信息。

8.1.7 :inverse_of

:inverse_of选项指定belongs_to关联的名称,该关联是此关联的逆关联。有关更多详细信息,请参阅双向关联部分。

class Supplier < ApplicationRecord
  has_one :account, inverse_of: :supplier
end

class Account < ApplicationRecord
  belongs_to :supplier, inverse_of: :account
end

8.1.8 :source_type

:source_type选项指定has_many :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.9 :strict_loading

每次通过此关联加载关联记录时,强制执行严格加载。

8.1.10 :association_foreign_key

可以在has_and_belongs_to_many关系中找到:association_foreign_key。根据惯例,Rails 假设联接表中用于保存指向另一个模型的外键的列是该模型的名称,并在其后添加了_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选项在设置多对多自联接时很有用。

8.1.11 :join_table

可以在has_and_belongs_to_many关系中找到:join_table。如果根据词典顺序确定的联接表的默认名称不是您想要的,您可以使用:join_table选项来覆盖默认值。

8.2 范围

范围允许您指定常见的查询,这些查询可以作为关联对象上的方法调用来引用。这对于定义在应用程序中的多个地方重复使用的自定义查询很有用。例如:

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

您也可以通过哈希设置条件:

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方法指定在使用此关联时应急加载的二阶关联。例如,考虑以下模型:

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.account.representative),那么您可以通过在供应商到帐户的关联中包含代表来提高代码的效率:

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,那么在需要时会自动急加载作者。

8.2.1.3 readonly

如果使用readonly,那么通过关联检索到的关联对象将是只读的。

class Book < ApplicationRecord
  belongs_to :author, -> { readonly }
end

这在您希望阻止通过关联修改关联对象时很有用。例如,如果您有一个Book模型,该模型belongs_to :author,您可以使用readonly来阻止通过书籍修改作者:

@book.author = Author.first
@book.author.save! # This will raise an ActiveRecord::ReadOnlyRecord error
8.2.1.4 select

select方法允许您覆盖用于检索有关关联对象的数据的 SQL SELECT 子句。默认情况下,Rails 会检索所有列。

例如,如果您有一个Author模型,它有许多Book,但您只想检索每本书的title

class Author < ApplicationRecord
  has_many :books, -> { select(:id, :title) } # Only select id and title columns
end

class Book < ApplicationRecord
  belongs_to :author
end

现在,当您访问作者的书籍时,只会从books表中检索idtitle列。

如果您在belongs_to关联上使用select方法,则还应设置:foreign_key选项以确保结果正确。例如:

class Book < ApplicationRecord
  belongs_to :author, -> { select(:id, :name) }, foreign_key: "author_id" # Only select id and name columns
end

class Author < ApplicationRecord
  has_many :books
end

在这种情况下,当您访问书籍的作者时,只会从authors表中检索idname列。

8.2.2 集合范围

has_manyhas_and_belongs_to_many是处理记录集合的关联,因此您可以使用grouplimitorderselectdistinct等附加方法来自定义关联使用的查询。

8.2.2.1 group

group方法提供一个属性名称,使用查找器 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>]

在上述情况下,有两个读物,并且person.articles会将它们都列出来,即使这些记录指向同一篇文章。

现在让我们设置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>]

在上述情况下,仍然有两个读物。但是person.articles只显示一篇文章,因为集合只加载唯一记录。

如果您想确保在插入时,持久关联中的所有记录都是不同的(这样您就可以确保当您检查关联时,您永远不会找到重复的记录),您应该在表本身添加一个唯一索引。例如,如果您有一个名为readings的表,并且您想确保文章只能添加到一个人一次,您可以在迁移中添加以下内容:

add_index :readings, [:person_id, :article_id], unique: true

一旦您有了这个唯一索引,尝试将文章添加到同一个人两次将引发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?这样的方法检查唯一性可能会导致竞争条件。不要尝试使用include?来强制关联中的唯一性。例如,使用上面提到的文章示例,以下代码将是竞争的,因为多个用户可能同时尝试这样做:

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关联根据供应商的active状态进行范围限制。

通过利用关联扩展和使用关联所有者的范围,您可以在 Rails 应用程序中创建更动态且更具上下文感知的关联。

8.3 计数器缓存

Rails 中的:counter_cache选项有助于提高查找关联对象数量的效率。考虑以下模型:

class Book < ApplicationRecord
  belongs_to :author
end

class Author < ApplicationRecord
  has_many :books
end

默认情况下,查询@auth books.size会导致数据库调用执行COUNT(*)查询。为了优化这一点,您可以在所属模型(在本例中为Book)中添加计数器缓存。这样,Rails 可以直接从缓存中返回计数,而无需查询数据库。

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

class Author < ApplicationRecord
  has_many :books
end

使用此声明,Rails 将使缓存值保持最新,然后在响应size方法时返回该值,从而避免数据库调用。

虽然:counter_cache选项是在具有belongs_to声明的模型上指定的,但实际的列必须添加到关联(在本例中为has_many)模型中。在此示例中,您需要向Author模型添加books_count列:

class AddBooksCountToAuthors < ActiveRecord::Migration[8.0]
  def change
    add_column :authors, :books_count, :integer, default: 0, null: false
  end
end

您可以在counter_cache声明中指定自定义列名,而不是使用默认的books_count。例如,要使用count_of_books

class Book < ApplicationRecord
  belongs_to :author, counter_cache: :count_of_books
end

class Author < ApplicationRecord
  has_many :books
end

您只需要在关联的belongs_to端指定:counter_cache选项。

在现有的大型表上使用计数器缓存可能会很麻烦。为了避免锁定表过长时间,必须单独在添加列之前填充列值。此填充还必须在使用:counter_cache之前进行;否则,诸如sizeany?等依赖于计数器缓存的方法可能会返回不正确的结果。

要安全地填充值,同时使用子记录创建/删除更新计数器缓存列,并确保方法始终从数据库获取结果(避免潜在的不正确的计数器缓存值),请使用counter_cache: { active: false }。此设置确保方法始终从数据库获取结果,避免从未初始化的计数器缓存获取不正确的值。如果您需要指定自定义列名,请使用counter_cache: { active: false, column: :my_custom_counter }

如果由于某种原因您更改了所有者模型的主键值,并且没有同时更新已计数字段的外键,那么计数器缓存可能包含陈旧数据。换句话说,任何孤立的模型仍然会计入计数器。要修复陈旧的计数器缓存,请使用reset_counters

8.4 回调函数

普通回调函数 会挂钩到 ActiveRecord 对象的生命周期中,允许您在不同阶段对这些对象进行操作。例如,您可以使用一个 :before_save 回调函数,在对象保存之前执行一些操作。

关联回调函数类似于普通回调函数,但它们是由与 ActiveRecord 对象关联的集合生命周期中的事件触发的。有四种可用的关联回调函数:

  • 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,则不会将该书添加到集合中。

通过使用这些关联回调函数,您可以自定义关联的行为,确保在集合生命周期的关键点执行特定操作。

ActiveRecord 回调函数指南 中了解更多有关关联回调函数的信息。

8.5 扩展

Rails 提供了扩展关联代理对象功能的能力,这些代理对象通过匿名模块添加新的查找器、创建者或其他方法来管理关联。此功能允许您自定义关联以满足应用程序的特定需求。

您可以直接在模型定义中使用自定义方法扩展 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

在这种情况下,FindRecentExtension 模块用于在 Author 模型的 books 关联和 Supplier 模型的 deliveries 关联中添加一个 find_recent 方法。此方法检索在过去五天内创建的记录。

扩展可以使用 proxy_association 访问器与关联代理的内部进行交互。proxy_association 提供三个重要的属性:

  • proxy_association.owner 返回关联所属的对象。
  • proxy_association.reflection 返回描述关联的反射对象。
  • proxy_association.target 返回 belongs_tohas_one 的关联对象,或 has_manyhas_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 访问关联的名称。



返回顶部