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
关联必须使用单数形式。如果您使用复数形式,例如在 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_to
与 has_one
或 has_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_one
或 belongs_to
关联时,您必须使用 build_
前缀来构建关联,而不是用于 has_many
或 has_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
的表中。
相应的迁移可能如下所示
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_one
或 belongs_to
关联时,您必须使用 build_
前缀来构建关联,而不是用于 has_many
或 has_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_one
和 belongs_to
关联不同,在声明 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
关联与另一个模型创建一对多关系。在数据库术语中,此关联表示另一个类将具有一个外键,该外键引用此类的实例。
在此迁移中,authors
表是用一个 name
列创建的,用于存储作者的姓名。books
表也被创建,它包含一个 belongs_to :author
关联。此关联在 books
和 authors
表之间建立外键关系。具体来说,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
关联时,声明类会获得与该关联相关的许多方法。其中一些是
collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {})
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload
我们将讨论一些常见的方法,但您可以在 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: :destroy
或 dependent: :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)的多个实例相关联。
相应的迁移可能如下所示
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
列创建。appointments
表充当连接表,使用 physician_id
和 patient_id
列创建,在 physicians
和 patients
之间建立多对多关系。
您也可以考虑在 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
。
用于设置这些关联的相应迁移可能如下所示
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
关联创建与另一个模型的直接多对多关系,没有中间模型。此关联表明,声明模型的每个实例都引用另一个模型的零个或多个实例。
例如,考虑一个具有 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
不需要中间模型,它也需要一个单独的表来建立两个模型之间多对多关系。这个中间表用于存储相关数据,映射两个模型实例之间的关联。该表不需要主键,因为其目的仅仅是管理关联记录之间的关系。相应的迁移可能如下所示
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
关联时,声明类会获得与该关联相关的许多方法。其中一些是
collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {})
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload
我们将讨论一些常见的关联方法,但您可以从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.concat
和collection.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_to
和has_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 :through
和has_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
在上面的上下文中,imageable
是为关联选择的名称。它是一个符号名称,代表Picture
模型与其他模型(如Employee
和Product
)之间的多态关联。重要的是在所有关联模型中一致地使用相同的名称(imageable
),以正确建立多态关联。
当您在Picture
模型中声明belongs_to :imageable, polymorphic: true
时,您是在说一个Picture
可以通过此关联属于任何模型(如Employee
或Product
)。
您可以将多态belongs_to
声明视为设置一个接口,任何其他模型都可以使用它。这允许您使用@employee.pictures
从Employee
模型的实例中检索图片集合。同样,您可以使用@product.pictures
从Product
模型的实例中检索图片集合。
此外,如果您有一个Picture
模型的实例,您可以通过@picture.imageable
获取其父对象,该对象可以是Employee
或Product
。
要手动设置多态关联,您需要在模型中声明一个外键列(imageable_id
)和一个类型列(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
可以是Employee
或Product
的 ID,而imageable_type
是关联模型类的名称,因此可以是Employee
或Product
。
虽然手动创建多态关联是可以接受的,但建议使用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 :manager
在employees
表中添加一个manager_id
列。foreign_key: { to_table: :employees }
确保manager_id
列引用了employees
表的id
列。
传递给 foreign_key
的 to_table
选项以及更多内容在 SchemaStatements#add_reference
中有解释。
通过这种设置,你可以在 Rails 应用程序中轻松访问员工的下属和经理。
要获取员工的下属
employee = Employee.find(1)
subordinates = employee.subordinates
要获取员工的经理
manager = employee.manager
5 单表继承 (STI)
单表继承 (STI) 是 Rails 中的一种模式,它允许将多个模型存储在单个数据库表中。当你有不同类型的实体共享公共属性和行为,但也有特定行为时,这很有用。
例如,假设我们有 Car
、Motorcycle
和 Bicycle
模型。这些模型将共享诸如 color
和 price
之类的字段,但每个模型都将具有独特的行为。它们还将分别拥有自己的控制器。
5.1 生成基础车辆模型
首先,我们使用共享字段生成基础 Vehicle
模型
$ bin/rails generate model vehicle type:string color:string price:decimal{10.2}
在这里,type
字段对于 STI 至关重要,因为它存储模型名称 (Car
、Motorcycle
或 Bicycle
)。STI 需要此字段来区分存储在同一表中的不同模型。
5.2 生成子模型
接下来,我们生成从 Vehicle 继承的 Car
、Motorcycle
和 Bicycle
模型。这些模型将不会拥有自己的表;相反,它们将使用 vehicles
表。
要生成 Car
模型
$ bin/rails generate model car --parent=Vehicle
为此,我们可以使用 --parent=PARENT
选项,它将生成一个从指定父类继承的模型,并且没有等效的迁移(因为表已经存在)。
这将生成一个从 Vehicle
继承的 Car
模型
class Car < Vehicle
end
这意味着添加到 Vehicle 的所有行为也适用于 Car,例如关联、公共方法等。创建一辆汽车将在 vehicles
表中以 "Car" 作为 type
字段保存它。
对 Motorcycle
和 Bicycle
重复相同的过程。
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
然后,我们将生成用于委托的新 Message
和 Comment
模型
$ 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
参数指定用于委托的字段,并将 Message
和 Comment
类型包含为委托类。entryable_type
和 entryable_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
这种设置允许 Entry
将 title
方法委托给它的子类,其中 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
的实例方法的名称。这是因为使用与现有方法冲突的名称创建关联会导致意外的后果,例如覆盖基本方法并导致功能问题。例如,使用 attributes
或 connection
等名称作为关联将是有问题的。
7.3 更新模式
关联非常有用,它们负责定义模型之间的关系,但它们不会更新您的数据库模式。您有责任维护您的数据库模式以匹配您的关联。这通常涉及两个主要任务:为 belongs_to 关联
创建外键,并为 has_many :through
和 has_and_belongs_to_many
关联设置正确的联接表。您可以在 has many through vs has and belongs to many
部分 中了解更多关于何时使用 has_many :through vs has_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.belongs_to :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
关联创建联接表
如果您创建了一个 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.3 为 has_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
在这个例子中,Supplier
和 Account
类都在同一个模块 (MyApplication::Business
) 内定义。这种组织允许您根据范围将模型组织到文件夹中,而无需在每个关联中显式指定范围。
# app/models/my_application/business/supplier.rb
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account
end
end
end
# app/models/my_application/business/account.rb
module MyApplication
module Business
class Account < ApplicationRecord
belongs_to :supplier
end
end
end
重要的是要注意,虽然模型范围有助于组织代码,但它不会改变数据库表的命名约定。例如,如果您有一个 MyApplication::Business::Supplier
模型,相应的数据库表仍然应该遵循命名约定,并命名为 my_application_business_suppliers
。
但是,如果 Supplier
和 Account
模型在不同的范围内定义,则关联默认情况下将不起作用
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account
end
end
module Billing
class Account < ApplicationRecord
belongs_to :supplier
end
end
end
要将模型与不同命名空间中的模型关联,您必须在关联声明中指定完整的类名
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_destroy
和after_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: :destroy
的 Post
模型中,调用帖子上的 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
值将被设置为 @user
的 guid
值。
has_and_belongs_to_many
不支持 :primary_key
选项。对于这种类型的关联,您可以通过使用带有 has_many
:through
关联的联接表来实现类似的功能,这提供了更大的灵活性并支持 :primary_key
选项。您可以在 has_many :through
部分 中阅读更多相关信息。
8.1.5 :touch
如果将:touch
选项设置为true
,那么每次保存或销毁相关对象时,关联对象的updated_at
或updated_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
表中检索id
和title
列。
如果您在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
表中检索id
和name
列。
8.2.2 集合范围
has_many
和has_and_belongs_to_many
是处理记录集合的关联,因此您可以使用group
、limit
、order
、select
和distinct
等附加方法来自定义关联使用的查询。
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
之前进行;否则,诸如size
、any?
等依赖于计数器缓存的方法可能会返回不正确的结果。
要安全地填充值,同时使用子记录创建/删除更新计数器缓存列,并确保方法始终从数据库获取结果(避免潜在的不正确的计数器缓存值),请使用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_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 访问关联的名称。