Active Record 关联
关联是一组宏式类方法,用于通过外键将对象绑定在一起。它们表达了诸如“项目有一个项目经理”或“项目属于一个投资组合”之类的关系。每个宏都会在类中添加一些方法,这些方法根据集合或关联符号和选项哈希进行专门化。它的工作方式与 Ruby 自身的 attr*
方法非常相似。
class Project < ActiveRecord::Base
belongs_to :portfolio
has_one :project_manager
has_many :milestones
has_and_belongs_to_many :categories
end
现在,项目类具有以下方法(以及更多方法)来简化其关系的遍历和操作
project = Project.first
project.portfolio
project.portfolio = Portfolio.first
project.reload_portfolio
project.project_manager
project.project_manager = ProjectManager.first
project.reload_project_manager
project.milestones.empty?
project.milestones.size
project.milestones
project.milestones << Milestone.first
project.milestones.delete(Milestone.first)
project.milestones.destroy(Milestone.first)
project.milestones.find(Milestone.first.id)
project.milestones.build
project.milestones.create
project.categories.empty?
project.categories.size
project.categories
project.categories << Category.first
project.categories.delete(category1)
project.categories.destroy(category1)
警告
不要创建与 实例方法 ActiveRecord::Base
同名的关联。由于关联会在其模型中添加一个具有该名称的方法,因此使用与 ActiveRecord::Base
提供的方法同名的关联将覆盖通过 ActiveRecord::Base
继承的方法,并导致问题。例如,attributes
和 connection
将是不好的关联名称选择,因为这些名称已经在 ActiveRecord::Base
实例方法列表中存在。
自动生成的方法
另请参见下面的“实例公共方法”(来自 belongs_to
)以了解更多详细信息。
单一关联(一对一)
| | belongs_to |
generated methods | belongs_to | :polymorphic | has_one
----------------------------------+------------+--------------+---------
other | X | X | X
other=(other) | X | X | X
build_other(attributes={}) | X | | X
create_other(attributes={}) | X | | X
create_other!(attributes={}) | X | | X
reload_other | X | X | X
other_changed? | X | X |
other_previously_changed? | X | X |
集合关联(一对多 / 多对多)
| | | has_many
generated methods | habtm | has_many | :through
----------------------------------+-------+----------+----------
others | X | X | X
others=(other,other,...) | X | X | X
other_ids | X | X | X
other_ids=(id,id,...) | X | X | X
others<< | X | X | X
others.push | X | X | X
others.concat | X | X | X
others.build(attributes={}) | X | X | X
others.create(attributes={}) | X | X | X
others.create!(attributes={}) | X | X | X
others.size | X | X | X
others.length | X | X | X
others.count | X | X | X
others.sum(*args) | X | X | X
others.empty? | X | X | X
others.clear | X | X | X
others.delete(other,other,...) | X | X | X
others.delete_all | X | X | X
others.destroy(other,other,...) | X | X | X
others.destroy_all | X | X | X
others.find(*args) | X | X | X
others.exists? | X | X | X
others.distinct | X | X | X
others.reset | X | X | X
others.reload | X | X | X
覆盖生成的方法
关联方法是在包含到模型类中的模块中生成的,因此可以轻松覆盖。因此,可以使用 super
调用原始的生成方法
class Car < ActiveRecord::Base
belongs_to :owner
belongs_to :old_owner
def owner=(new_owner)
self.old_owner = self.owner
super
end
end
关联方法模块在生成的属性方法模块之后立即包含,这意味着关联将覆盖具有相同名称的属性的方法。
基数和关联
Active Record 关联可用于描述模型之间的一对一、一对多和多对多关系。每个模型都使用关联来描述其在关系中的作用。
关联始终用于具有外键的模型中。belongs_to
一对一
在基础中使用
,并在关联模型中使用 has_one
。belongs_to
class Employee < ActiveRecord::Base
has_one :office
end
class Office < ActiveRecord::Base
belongs_to :employee # foreign key - employee_id
end
一对多
在基础中使用
,并在关联模型中使用 has_many
。belongs_to
class Manager < ActiveRecord::Base
has_many :employees
end
class Employee < ActiveRecord::Base
belongs_to :manager # foreign key - manager_id
end
多对多
有两种方法可以构建多对多关系。
第一种方法使用
关联,并带有 has_many
:through
选项和一个连接模型,因此有两个关联阶段。
class Assignment < ActiveRecord::Base
belongs_to :programmer # foreign key - programmer_id
belongs_to :project # foreign key - project_id
end
class Programmer < ActiveRecord::Base
has_many :assignments
has_many :projects, through: :assignments
end
class Project < ActiveRecord::Base
has_many :assignments
has_many :programmers, through: :assignments
end
对于第二种方法,在两个模型中都使用
。这需要一个连接表,该表没有相应的模型或主键。has_and_belongs_to_many
class Programmer < ActiveRecord::Base
has_and_belongs_to_many :projects # foreign keys in the join table
end
class Project < ActiveRecord::Base
has_and_belongs_to_many :programmers # foreign keys in the join table
end
选择哪种方法来构建多对多关系并不总是那么简单。如果您需要将关系模型用作其自己的实体,请使用 has_many
:through
。当使用传统模式或从未直接使用关系本身时,请使用
。has_and_belongs_to_many
是 belongs_to
还是 has_one
关联?
belongs_to
has_one
两者都表达了 1-1 关系。区别主要在于外键的位置,外键位于声明
关系的类的表中。belongs_to
class User < ActiveRecord::Base
# I reference an account.
belongs_to :account
end
class Account < ActiveRecord::Base
# One user references me.
has_one :user
end
这些类的表可能看起来像这样
CREATE TABLE users (
id bigint NOT NULL auto_increment,
account_id bigint default NULL,
name varchar default NULL,
PRIMARY KEY (id)
)
CREATE TABLE accounts (
id bigint NOT NULL auto_increment,
name varchar default NULL,
PRIMARY KEY (id)
)
未保存的对象和关联
您可以在将对象和关联保存到数据库之前对其进行操作,但应该注意一些特殊行为,主要涉及关联对象的保存。
您可以在
、has_one
、belongs_to
或 has_many
关联上设置 has_and_belongs_to_many
:autosave
选项。将其设置为 true
将始终保存成员,而将其设置为 false
将从不保存成员。有关 :autosave
选项的更多详细信息,请访问
。AutosaveAssociation
一对一关联
-
将对象分配给
关联会自动保存该对象和正在替换的对象(如果有),以更新它们的外键 - 除非父对象未保存(has_one
new_record? == true
)。 -
如果这些保存中的任何一个失败(由于其中一个对象无效),则会引发
异常,并且分配将被取消。ActiveRecord::RecordNotSaved
-
如果您希望将对象分配给
关联而不保存它,请使用has_one
#build_association
方法(在下面有文档)。正在替换的对象仍将被保存以更新其外键。 -
将对象分配给
关联不会保存对象,因为外键字段位于父级上。它也不会保存父级。belongs_to
集合
-
将对象添加到集合(
或has_many
)会自动保存该对象,除非父对象(集合的所有者)尚未存储在数据库中。has_and_belongs_to_many
-
如果保存添加到集合中的任何对象(通过
push
或类似方法)失败,则push
返回false
。 -
如果在替换集合(通过
association=
)时保存失败,则会引发
异常,并且分配将被取消。ActiveRecord::RecordNotSaved
-
您可以使用
collection.build
方法(在下面有文档)将对象添加到集合而不自动保存它。 -
所有未保存的(
new_record? == true
)集合成员在父级保存时会自动保存。
自定义查询
关联是根据 Relation
对象构建的,您可以使用
语法对其进行自定义。例如,要添加一个条件Relation
class Blog < ActiveRecord::Base
has_many :published_posts, -> { where(published: true) }, class_name: 'Post'
end
在 -> { ... }
块中,您可以使用所有常见的
方法。Relation
访问所有者对象
有时在构建查询时访问所有者对象很有用。所有者作为参数传递给块。例如,以下关联将查找发生在用户生日的所有事件
class User < ActiveRecord::Base
has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event'
end
注意:连接或急切加载此类关联是不可能的,因为这些操作发生在实例创建之前。此类关联可以预加载,但这样做会执行 N+1 个查询,因为每个记录都会有一个不同的范围(类似于预加载多态范围)。
关联回调
与挂钩到 Active Record 对象生命周期的普通回调类似,您还可以定义在将对象添加到关联集合或从关联集合中删除对象时触发的回调。
class Firm < ActiveRecord::Base
has_many :clients,
dependent: :destroy,
after_add: :congratulate_client,
after_remove: :log_after_remove
def congratulate_client(client)
# ...
end
def log_after_remove(client)
# ...
end
end
可以通过三种方式定义Callbacks
-
一个符号,引用在具有关联集合的类上定义的方法。例如,
after_add: :congratulate_client
调用Firm#congratulate_client(client)
。 -
一个可调用对象,其签名接受具有关联集合的记录和正在添加或删除的记录。例如,
after_add: ->(firm, client) { ... }
。 -
一个响应回调名称的对象。例如,传递
after_add: CallbackObject.new
将调用CallbackObject#after_add(firm, client)
。
可以通过将回调作为数组传递来堆叠回调。例如
class CallbackObject
def after_add(firm, client)
firm.log << "after_adding #{client.id}"
end
end
class Firm < ActiveRecord::Base
has_many :clients,
dependent: :destroy,
after_add: [
:congratulate_client,
-> (firm, client) { firm.log << "after_adding #{client.id}" },
CallbackObject.new
],
after_remove: :log_after_remove
end
可能的回调是:before_add
、after_add
、before_remove
和 after_remove
。
如果任何 before_add
回调抛出异常,则该对象将不会添加到集合中。
类似地,如果任何 before_remove
回调抛出异常,则该对象将不会从集合中删除。
注意:要触发删除回调,您必须使用 destroy
/ destroy_all
方法。例如
-
firm.clients.destroy(client)
-
firm.clients.destroy(*clients)
-
firm.clients.destroy_all
以下 delete
/ delete_all
方法不会触发删除回调
-
firm.clients.delete(client)
-
firm.clients.delete(*clients)
-
firm.clients.delete_all
关联扩展
控制对关联访问的代理对象可以通过匿名模块进行扩展。这对于添加仅作为此关联一部分使用的新的查找器、创建器和其他工厂类型的方法特别有用。
class Account < ActiveRecord::Base
has_many :people do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by(first_name: first_name, last_name: last_name)
end
end
end
person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
person.first_name # => "David"
person.last_name # => "Heinemeier Hansson"
如果您需要在许多关联之间共享相同的扩展,您可以使用命名扩展模块。
module FindOrCreateByNameExtension
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by(first_name: first_name, last_name: last_name)
end
end
class Account < ActiveRecord::Base
has_many :people, -> { extending FindOrCreateByNameExtension }
end
class Company < ActiveRecord::Base
has_many :people, -> { extending FindOrCreateByNameExtension }
end
一些扩展只能在了解关联内部的情况下才能使用。扩展可以使用以下方法访问相关状态(其中 items
是关联的名称)
-
record.association(:items).owner
- 返回关联所属的对象。 -
record.association(:items).reflection
- 返回描述关联的反射对象。 -
record.association(:items).target
- 返回
和belongs_to
的关联对象,或has_one
和has_many
的关联对象集合。has_and_belongs_to_many
但是,在实际的扩展代码中,您将无法访问上面的 record
。在这种情况下,您可以访问 proxy_association
。例如,record.association(:items)
和 record.items.proxy_association
将返回相同的对象,允许您在关联扩展中进行 proxy_association.owner
之类的调用。
关联连接模型
Has Many 关联可以使用 :through
选项进行配置,以使用显式连接模型来检索数据。它的操作方式类似于
关联。优势在于您能够在连接模型上添加验证、回调和额外属性。考虑以下模式has_and_belongs_to_many
class Author < ActiveRecord::Base
has_many :authorships
has_many :books, through: :authorships
end
class Authorship < ActiveRecord::Base
belongs_to :author
belongs_to :book
end
@author = Author.first
@author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to
@author.books # selects all books by using the Authorship join model
您也可以通过连接模型上的
关联进行操作has_many
class Firm < ActiveRecord::Base
has_many :clients
has_many :invoices, through: :clients
end
class Client < ActiveRecord::Base
belongs_to :firm
has_many :invoices
end
class Invoice < ActiveRecord::Base
belongs_to :client
end
@firm = Firm.first
@firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm
@firm.invoices # selects all invoices by going through the Client join model
类似地,您也可以通过连接模型上的
关联进行操作has_one
class Group < ActiveRecord::Base
has_many :users
has_many :avatars, through: :users
end
class User < ActiveRecord::Base
belongs_to :group
has_one :avatar
end
class Avatar < ActiveRecord::Base
belongs_to :user
end
@group = Group.first
@group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group
@group.avatars # selects all avatars by going through the User join model.
通过连接模型上的
或 has_one
关联进行操作的一个重要警告是,这些关联是只读的。例如,以下操作在前面的示例之后将不起作用has_many
@group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
@group.avatars.delete(@group.avatars.last) # so would this
设置逆关系
如果您在连接模型上使用 belongs_to
,建议在 belongs_to
上设置 :inverse_of
选项,这将确保以下示例正常工作(其中 tags
是一个 has_many
:through
关联)。
@post = Post.first
@tag = @post.tags.build name: "ruby"
@tag.save
最后一行应该保存通过记录(一个 Tagging
)。只有在设置了 :inverse_of
时,这才能正常工作。
class Tagging < ActiveRecord::Base
belongs_to :post
belongs_to :tag, inverse_of: :taggings
end
如果您没有设置 :inverse_of
记录,关联将尽力将自身与正确的反向匹配。自动反向检测仅适用于 has_many
、has_one
和 belongs_to
关联。
关联上的 :foreign_key
和 :through
选项也会阻止自动找到关联的反向,就像某些情况下自定义范围一样。有关更多详细信息,请参阅 Active Record Associations 指南。
自动猜测反向关联使用基于类名的启发式方法,因此它可能不适用于所有关联,特别是那些具有非标准名称的关联。
您可以通过将 :inverse_of
选项设置为 false
来关闭自动检测反向关联,如下所示
class Tagging < ActiveRecord::Base
belongs_to :tag, inverse_of: false
end
嵌套关联
您实际上可以使用 :through
选项指定 **任何** 关联,包括本身具有 :through
选项的关联。例如
class Author < ActiveRecord::Base
has_many :posts
has_many :comments, through: :posts
has_many :commenters, through: :comments
end
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :commenter
end
@author = Author.first
@author.commenters # => People who commented on posts written by the author
设置此关联的等效方法是
class Author < ActiveRecord::Base
has_many :posts
has_many :commenters, through: :posts
end
class Post < ActiveRecord::Base
has_many :comments
has_many :commenters, through: :comments
end
class Comment < ActiveRecord::Base
belongs_to :commenter
end
使用嵌套关联时,您将无法修改关联,因为没有足够的信息来知道要进行何种修改。例如,如果您尝试在上面的示例中添加一个 Commenter
,将无法知道如何设置中间的 Post
和 Comment
对象。
多态关联
模型上的多态关联不受其可以关联的模型类型的限制。相反,它们指定了 has_many
关联必须遵守的接口。
class Asset < ActiveRecord::Base
belongs_to :attachable, polymorphic: true
end
class Post < ActiveRecord::Base
has_many :assets, as: :attachable # The :as option specifies the polymorphic interface to use.
end
@asset.attachable = @post
这是通过在除了外键之外还使用类型列来指定关联记录来实现的。在 Asset 示例中,您需要一个 attachable_id
整数列和一个 attachable_type
字符串列。
将多态关联与单表继承 (STI) 结合使用有点棘手。为了使关联按预期工作,请确保将 STI 模型的基础模型存储在多态关联的类型列中。继续上面的资产示例,假设存在使用 posts 表进行 STI 的访客帖子和成员帖子。在这种情况下,posts 表中必须有一个 type
列。
注意:在分配 attachable
时,正在调用 attachable_type=
方法。attachable
的 class_name
作为 String
传递。
class Asset < ActiveRecord::Base
belongs_to :attachable, polymorphic: true
def attachable_type=(class_name)
super(class_name.constantize.base_class.to_s)
end
end
class Post < ActiveRecord::Base
# because we store "Post" in attachable_type now dependent: :destroy will work
has_many :assets, as: :attachable, dependent: :destroy
end
class GuestPost < Post
end
class MemberPost < Post
end
缓存
所有方法都基于一个简单的缓存原则,该原则会将上次查询的结果保留下来,除非明确指示不保留。缓存甚至在方法之间共享,以便更便宜地使用宏添加的方法,而无需过多担心首次运行的性能。
project.milestones # fetches milestones from the database
project.milestones.size # uses the milestone cache
project.milestones.empty? # uses the milestone cache
project.milestones.reload.size # fetches milestones from the database
project.milestones # uses the milestone cache
关联的预加载
预加载是一种查找特定类对象和多个命名关联的方法。它是防止可怕的 N+1 问题最简单的方法之一,其中获取 100 个帖子,每个帖子都需要显示其作者,会触发 101 个数据库查询。通过使用预加载,查询数量将从 101 个减少到 2 个。
class Post < ActiveRecord::Base
belongs_to :author
has_many :comments
end
考虑使用上面类别的以下循环
Post.all.each do |post|
puts "Post: " + post.title
puts "Written by: " + post.author.name
puts "Last comment on: " + post.comments.first.created_on
end
要遍历这 100 个帖子,我们将生成 201 个数据库查询。首先,让我们只针对检索作者进行优化
Post.includes(:author).each do |post|
这引用了也使用 :author
符号的 belongs_to
关联的名称。加载帖子后,find
将从每个帖子中收集 author_id
,并使用一个查询加载所有引用的作者。这样做会将查询数量从 201 个减少到 102 个。
我们可以通过在查找器中引用两个关联来进一步改进情况
Post.includes(:author, :comments).each do |post|
这将使用一个查询加载所有评论。这将总查询数量减少到 3 个。通常,查询数量将是 1 加上命名关联的数量(除非某些关联是多态 belongs_to
- 见下文)。
要包含关联的深层层次结构,请使用哈希
Post.includes(:author, { comments: { author: :gravatar } }).each do |post|
上面的代码将加载所有评论以及所有关联的作者和 gravatar。您可以混合匹配任何符号、数组和哈希的组合来检索要加载的关联。
所有这些功能不应该让你误以为你可以提取大量数据而不会有任何性能损失,仅仅因为你减少了查询次数。数据库仍然需要将所有数据发送到 Active Record,并且仍然需要对其进行处理。因此,它不是解决性能问题的万能药,但它是在上述情况下减少查询次数的好方法。
由于一次只加载一个表,因此条件或顺序不能引用除主表以外的表。如果是这种情况,Active Record 会回退到以前使用的基于 LEFT OUTER JOIN
的策略。例如
Post.includes([:author, :comments]).where(['comments.approved = ?', true])
这将导致一个带有连接的单个 SQL 查询,例如:LEFT OUTER JOIN comments ON comments.post_id = posts.id
和 LEFT OUTER JOIN authors ON authors.id = posts.author_id
。请注意,使用这样的条件可能会产生意想不到的后果。在上面的示例中,没有已批准评论的帖子根本不会返回,因为条件适用于整个 SQL 语句,而不仅仅适用于关联。
您必须对该回退发生的列引用进行消歧,例如 order: "author.name DESC"
将起作用,但 order: "name DESC"
不会。
如果您要加载所有帖子(包括没有已批准评论的帖子),则使用 ON
编写您自己的 LEFT OUTER JOIN
查询
Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'")
在这种情况下,通常更自然的是包含一个关联,该关联在其上定义了条件
class Post < ActiveRecord::Base
has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment'
end
Post.includes(:approved_comments)
这将加载帖子并预加载 approved_comments
关联,其中只包含已批准的那些评论。
如果您预加载具有指定 :limit
选项的关联,它将被忽略,返回所有关联的对象
class Picture < ActiveRecord::Base
has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment'
end
Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments.
预加载支持多态关联。
class Address < ActiveRecord::Base
belongs_to :addressable, polymorphic: true
end
尝试预加载可寻址模型的调用
Address.includes(:addressable)
这将执行一个查询来加载地址,并使用每个可寻址类型的查询来加载可寻址模型。例如,如果所有可寻址模型都属于 Person 类或 Company 类,那么总共将执行 3 个查询。要加载的可寻址类型列表是根据加载的地址决定的。如果 Active Record 必须回退到以前实现的预加载,则不支持此功能,并将引发 ActiveRecord::EagerLoadPolymorphicError
。原因是父模型的类型是列值,因此其相应的表名不能放在该查询的 FROM
/JOIN
子句中。
表别名
Active Record 在表在连接中被多次引用时使用表别名。如果表只被引用一次,则使用标准表名。第二次,表被别名为 #{reflection_name}_#{parent_table_name}
。索引附加到任何后续使用表名的操作中。
Post.joins(:comments)
# SELECT ... FROM posts INNER JOIN comments ON ...
Post.joins(:special_comments) # STI
# SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment'
Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name
# SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts
充当树示例
TreeMixin.joins(:children)
# SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
TreeMixin.joins(children: :parent)
# SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
# INNER JOIN parents_mixins ...
TreeMixin.joins(children: {parent: :children})
# SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
# INNER JOIN parents_mixins ...
# INNER JOIN mixins childrens_mixins_2
Has and Belongs to Many 连接表使用相同的思路,但添加了 _join
后缀
Post.joins(:categories)
# SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
Post.joins(categories: :posts)
# SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
Post.joins(categories: {posts: :categories})
# SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
# INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2
如果您希望使用 ActiveRecord::QueryMethods#joins
方法指定您自己的自定义连接,这些表名将优先于预加载关联
Post.joins(:comments).joins("inner join comments ...")
# SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ...
Post.joins(:comments, :special_comments).joins("inner join comments ...")
# SELECT ... FROM posts INNER JOIN comments comments_posts ON ...
# INNER JOIN comments special_comments_posts ...
# INNER JOIN comments ...
表别名会根据特定数据库自动截断,以符合表标识符的最大长度。
模块
默认情况下,关联将在当前模块范围内查找对象。考虑
module MyApplication
module Business
class Firm < ActiveRecord::Base
has_many :clients
end
class Client < ActiveRecord::Base; end
end
end
当调用 Firm#clients
时,它将依次调用 MyApplication::Business::Client.find_all_by_firm_id(firm.id)
。如果您想与另一个模块范围内的类关联,可以通过指定完整的类名来实现。
module MyApplication
module Business
class Firm < ActiveRecord::Base; end
end
module Billing
class Account < ActiveRecord::Base
belongs_to :firm, class_name: "MyApplication::Business::Firm"
end
end
end
双向关联
当您指定关联时,关联模型上通常会有一个关联,它以相反的方式指定相同的关联。例如,使用以下模型
class Dungeon < ActiveRecord::Base
has_many :traps
has_one :evil_wizard
end
class Trap < ActiveRecord::Base
belongs_to :dungeon
end
class EvilWizard < ActiveRecord::Base
belongs_to :dungeon
end
Dungeon
上的 traps
关联和 Trap
上的 dungeon
关联是彼此的反向,EvilWizard
上的 dungeon
关联的反向是 Dungeon
上的 evil_wizard
关联(反之亦然)。默认情况下,Active Record 可以根据类的名称猜测关联的反向。结果如下
d = Dungeon.first
t = d.traps.first
d.object_id == t.dungeon.object_id # => true
上面的示例中,Dungeon
实例 d
和 t.dungeon
指的是同一个内存中的实例,因为关联与类的名称匹配。如果我们在模型定义中添加 :inverse_of
,结果将相同
class Dungeon < ActiveRecord::Base
has_many :traps, inverse_of: :dungeon
has_one :evil_wizard, inverse_of: :dungeon
end
class Trap < ActiveRecord::Base
belongs_to :dungeon, inverse_of: :traps
end
class EvilWizard < ActiveRecord::Base
belongs_to :dungeon, inverse_of: :evil_wizard
end
有关更多信息,请参阅 :inverse_of
选项的文档以及 Active Record Associations 指南。
从关联中删除
依赖关联
has_many
、has_one
和 belongs_to
关联支持 :dependent
选项。这允许您指定在删除所有者时应删除关联记录。
例如
class Author
has_many :posts, dependent: :destroy
end
Author.find(1).destroy # => Will destroy all of the author's posts, too
:dependent
选项可以具有不同的值,这些值指定删除操作的执行方式。有关更多信息,请参阅不同特定关联类型的此选项的文档。如果没有给出选项,则在销毁记录时,行为是不会对关联记录执行任何操作。
请注意,:dependent
是使用 Rails 的回调系统实现的,该系统通过按顺序处理回调来工作。因此,在 :dependent
选项之前或之后声明的其他回调会影响其执行方式。
请注意,:dependent
选项被忽略,因为 has_one
:through
关联。
删除或销毁?
has_many
和 has_and_belongs_to_many
关联具有 destroy
、delete
、destroy_all
和 delete_all
方法。
对于 has_and_belongs_to_many
,delete
和 destroy
是相同的:它们会导致连接表中的记录被删除。
对于 has_many
,destroy
和 destroy_all
始终会调用要删除的记录的 destroy
方法,以便运行回调。但是 delete
和 delete_all
则会根据 :dependent
选项指定的策略进行删除,或者如果没有给出 :dependent
选项,则会遵循默认策略。默认策略是无操作(保留带有父 ID 的外键),除了 has_many
:through
,其默认策略是 delete_all
(删除连接记录,不运行其回调)。
还存在一个 clear
方法,它与 delete_all
相同,只是它返回关联而不是已删除的记录。
删除什么?
这里存在一个潜在的陷阱:has_and_belongs_to_many
和 has_many
:through
关联在联接表中具有记录,以及关联的记录。因此,当我们调用这些删除方法之一时,究竟应该删除什么?
答案是,假设对关联的删除是关于移除所有者和关联对象之间的链接,而不是必需的关联对象本身。因此,对于 has_and_belongs_to_many
和 has_many
:through
,联接记录将被删除,但关联记录不会被删除。
如果您仔细考虑,这很有道理:如果您要调用 post.tags.delete(Tag.find_by(name: 'food'))
,您希望“食物”标签与帖子解除关联,而不是从数据库中删除标签本身。
但是,有些例子表明这种策略没有意义。例如,假设一个人拥有许多项目,每个项目都有许多任务。如果我们删除了一个人的任务,我们可能不希望项目被删除。在这种情况下,delete 方法实际上不起作用:只有当联接模型上的关联是 belongs_to
时,才能使用它。在其他情况下,您应该直接对关联记录或 :through
关联执行操作。
对于常规的 has_many
,没有“关联记录”和“链接”之间的区别,因此对于要删除的内容只有一个选择。
对于 has_and_belongs_to_many
和 has_many
:through
,如果您想删除关联记录本身,始终可以使用类似于 person.tasks.each(&:destroy)
的方法。
类型安全与 ActiveRecord::AssociationTypeMismatch
如果您尝试将一个对象分配给与推断或指定的 :class_name
不匹配的关联,您将收到 ActiveRecord::AssociationTypeMismatch
错误。
选项
所有关联宏都可以通过选项进行专门化。这使得比简单且可猜测的用例更复杂的用例成为可能。
实例公共方法
belongs_to(name, scope = nil, **options) Link
指定与另一个类的单对一关联。此方法仅应在当前类包含外键时使用。如果另一个类包含外键,则应使用 has_one
。有关何时使用 has_one
和何时使用 belongs_to
的更多详细信息,请参阅 它是 belongs_to 还是 has_one 关联?
将添加用于检索和查询单个关联对象的方法,当前对象为此对象保存一个 ID
association
是作为 name
参数传递的符号的占位符,因此 belongs_to :author
将在其他方法中添加 author.nil?
。
association
-
返回关联的对象。如果没有找到,则返回
nil
。 association=(associate)
-
分配关联对象,提取主键,并将其设置为外键。不会进行现有记录的修改或删除。
build_association(attributes = {})
-
返回一个新的关联类型对象,该对象已使用
attributes
实例化并通过外键链接到当前对象,但尚未保存。 create_association(attributes = {})
-
返回一个新的关联类型对象,该对象已使用
attributes
实例化,通过外键链接到当前对象,并且已经保存(如果它通过了验证)。 create_association!(attributes = {})
-
执行与
create_association
相同的操作,但如果记录无效,则会引发ActiveRecord::RecordInvalid
。 reload_association
-
返回关联的对象,强制进行数据库读取。
reset_association
-
卸载关联的对象。下次访问将从数据库中查询它。
association_changed?
-
如果已分配一个新的关联对象,并且下次保存将更新外键,则返回 true。
association_previously_changed?
-
如果上一次保存更新关联以引用一个新的关联对象,则返回 true。
示例
class Post < ActiveRecord::Base
belongs_to :author
end
声明 belongs_to :author
将添加以下方法(以及更多)
post = Post.find(7)
author = Author.find(19)
post.author # similar to Author.find(post.author_id)
post.author = author # similar to post.author_id = author.id
post.build_author # similar to post.author = Author.new
post.create_author # similar to post.author = Author.new; post.author.save; post.author
post.create_author! # similar to post.author = Author.new; post.author.save!; post.author
post.reload_author
post.reset_author
post.author_changed?
post.author_previously_changed?
范围
您可以传递一个第二个参数 scope
作为可调用对象(即 proc 或 lambda)来检索特定记录或自定义访问关联对象时生成的查询。
范围示例
belongs_to :firm, -> { where(id: 2) }
belongs_to :user, -> { joins(:friends) }
belongs_to :level, ->(game) { where("game_level > ?", game.current_level) }
选项
声明还可以包含一个 options
哈希表来专门化关联的行为。
:class_name
-
指定关联的类名。仅当无法从关联名中推断出该名称时才使用它。因此,
belongs_to :author
默认情况下将链接到 Author 类,但如果实际类名是 Person,则必须使用此选项指定它。 :foreign_key
-
指定用于关联的外键。默认情况下,这被猜测为关联名加上“_id”后缀。因此,定义
belongs_to :person
关联的类将使用“person_id”作为默认的:foreign_key
。类似地,belongs_to :favorite_person, class_name: "Person"
将使用“favorite_person_id”作为外键。设置
:foreign_key
选项将阻止自动检测关联的反向,因此通常最好也设置:inverse_of
选项。 :foreign_type
-
指定用于存储关联对象类型的列,如果这是多态关联。默认情况下,这被猜测为关联名加上“_type”后缀。因此,定义
belongs_to :taggable, polymorphic: true
关联的类将使用“taggable_type”作为默认的:foreign_type
。 :primary_key
-
指定返回用于关联的关联对象主键的方法。默认情况下,这是
id
。 :dependent
-
如果设置为
:destroy
,则在删除当前对象时会删除关联的对象。如果设置为:delete
,则会删除关联的对象,不调用其 destroy 方法。如果设置为:destroy_async
,则关联的对象将在后台作业中安排删除。当belongs_to
与另一个类上的has_many
关系一起使用时,不应指定此选项,因为可能会留下孤儿记录。 :counter_cache
-
通过使用
CounterCache::ClassMethods#increment_counter
和CounterCache::ClassMethods#decrement_counter
,缓存关联类上所属对象的数量。当创建当前类的对象时,计数器缓存会递增,而当删除当前类的对象时,计数器缓存会递减。这要求关联类(例如 Post 类)使用名为#{table_name}_count
(例如,对于属于 Comment 类的comments_count
)的列,即关联类上创建了#{table_name}_count
的迁移(这样,Post.comments_count
将返回缓存的计数)。您还可以通过提供列名而不是true
/false
值来指定自定义计数器缓存列(例如,counter_cache: :my_custom_counter
)。在现有的大型表上开始使用计数器缓存可能很麻烦,因为必须在添加列之外单独回填列值(以避免长时间锁定表)并在使用
:counter_cache
之前(否则,像size
/any?
/等方法,这些方法在内部使用计数器缓存,可能会产生不正确的结果)。为了在保持计数器缓存列随着子记录的创建/删除而更新的同时安全地回填值,并避免上述方法使用可能不正确的计数器缓存列值并始终从数据库中获取结果,请使用counter_cache: { active: false }
。如果您还需要指定自定义列名,请使用counter_cache: { active: false, column: :my_custom_counter }
。注意:如果您已启用计数器缓存,则可能需要将计数器缓存属性添加到关联类中的
attr_readonly
列表中(例如class Post; attr_readonly :comments_count; end
)。 :polymorphic
-
通过传递
true
指定此关联是多态关联。注意:由于多态关联依赖于在数据库中存储类名,因此请确保更新相应行的*_type
多态类型列中的类名。 :validate
-
当设置为
true
时,在保存父对象时验证添加到关联的新对象。默认情况下为false
。如果您希望确保关联对象在每次更新时都重新验证,请使用validates_associated
。 :autosave
-
如果为 true,则在保存父对象时始终保存关联对象或销毁它(如果标记为销毁)。如果为 false,则从不保存或销毁关联对象。默认情况下,仅当关联对象是新记录时才保存它。
注意,
NestedAttributes::ClassMethods#accepts_nested_attributes_for
将:autosave
设置为true
。 :touch
-
如果为真,则当此记录保存或销毁时,相关联的对象将被触碰(
updated_at
/updated_on
属性设置为当前时间)。如果您指定一个符号,该属性将与当前时间一起更新,此外还有updated_at
/updated_on
属性。请注意,触碰时不会执行任何验证,并且只会执行after_touch
、after_commit
和after_rollback
回调。 :inverse_of
-
指定相关联对象上
has_one
或has_many
关联的名称,该关联是此belongs_to
关联的反向关联。有关详细信息,请参阅 双向关联。 :optional
-
当设置为
true
时,关联将不会进行其存在性验证。 :required
-
当设置为
true
时,关联也将进行其存在性验证。这将验证关联本身,而不是 ID。您可以使用:inverse_of
来避免在验证期间进行额外的查询。注意:required
默认设置为true
并且已弃用。如果您不想进行关联存在性验证,请使用optional: true
。 :default
-
提供一个可调用对象(例如 proc 或 lambda)来指定在验证之前应该用特定记录初始化关联。请注意,如果记录存在,则不会执行可调用对象。
:strict_loading
-
每次通过此关联加载相关联的记录时,强制执行严格加载。
:ensuring_owner_was
-
指定要在所有者上调用的实例方法。该方法必须返回 true,以便相关联的记录在后台作业中被删除。
:query_constraints
-
用作复合外键。定义用于查询相关联对象的列列表。这是一个可选选项。默认情况下,
Rails
将尝试自动推断该值。当设置该值时,Array
的大小必须与相关联模型的主键或query_constraints
的大小匹配。
选项示例
belongs_to :firm, foreign_key: "client_of"
belongs_to :person, primary_key: "name", foreign_key: "person_name"
belongs_to :author, class_name: "Person", foreign_key: "author_id"
belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
class_name: "Coupon", foreign_key: "coupon_id"
belongs_to :attachable, polymorphic: true
belongs_to :project, -> { readonly }
belongs_to :post, counter_cache: true
belongs_to :comment, touch: true
belongs_to :company, touch: :employees_last_updated_at
belongs_to :user, optional: true
belongs_to :account, default: -> { company.account }
belongs_to :account, strict_loading: true
belongs_to :note, query_constraints: [:organization_id, :note_id]
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/associations.rb, line 1689 def belongs_to(name, scope = nil, **options) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection end
has_and_belongs_to_many(name, scope = nil, **options, &extension) 链接
指定与另一个类的多对多关系。这通过中间联接表关联两个类。除非将联接表显式指定为选项,否则将使用类名的词法顺序进行猜测。因此,开发人员和项目之间的联接将生成默认的联接表名称“developers_projects”,因为“D”在字母顺序上位于“P”之前。请注意,此优先级是使用 String
的 <
运算符计算的。这意味着,如果字符串的长度不同,并且字符串在比较到最短长度时相等,那么较长的字符串被认为比较短的字符串具有更高的词法优先级。例如,人们会期望表“paper_boxes”和“papers”生成联接表名称“papers_paper_boxes”,因为“paper_boxes”的名称长度,但实际上它生成联接表名称“paper_boxes_papers”。请注意此注意事项,如果您需要,请使用自定义的 :join_table
选项。如果您的表共享一个公共前缀,它只会在开头出现一次。例如,表“catalog_categories”和“catalog_products”生成联接表名称“catalog_categories_products”。
联接表不应该有主键或与之相关的模型。您必须使用以下迁移手动生成联接表
class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[8.0]
def change
create_join_table :developers, :projects
end
end
为这些列中的每一列添加索引以加快联接过程也是一个好主意。但是,在 MySQL 中,建议为这两列添加复合索引,因为 MySQL 在查找期间每个表只使用一个索引。
添加以下方法用于检索和查询
collection
是作为 name
参数传递的符号的占位符,因此 has_and_belongs_to_many :categories
将添加 categories.empty?
等方法。
collection
collection<<(object, ...)
-
通过在联接表中创建关联(
collection.push
和collection.concat
是此方法的别名)将一个或多个对象添加到集合中。请注意,此操作会立即触发更新 SQL,而不会等待父对象的保存或更新调用,除非父对象是新记录。 collection.delete(object, ...)
-
通过从联接表中删除关联来从集合中删除一个或多个对象。这不会销毁对象。
collection.destroy(object, ...)
-
通过对联接表中的每个关联运行 destroy 来从集合中删除一个或多个对象,覆盖任何依赖选项。这不会销毁对象。
collection=objects
-
通过删除和添加适当的对象来替换集合的内容。
collection_singular_ids
-
返回相关联对象 ID 的数组。
collection_singular_ids=ids
-
通过
ids
中的主键标识的对象来替换集合。 collection.clear
-
从集合中删除所有对象。这不会销毁对象。
collection.empty?
-
如果没有相关联的对象,则返回
true
。 collection.size
-
返回相关联对象的数量。
collection.find(id)
-
查找响应
id
并且满足必须与该对象关联的条件的相关联对象。使用与ActiveRecord::FinderMethods#find
相同的规则。 collection.exists?(...)
-
检查是否存在具有给定条件的相关联对象。使用与
ActiveRecord::FinderMethods#exists?
相同的规则。 collection.build(attributes = {})
-
返回一个新的集合类型对象,该对象已使用
attributes
初始化并通过联接表链接到该对象,但尚未保存。 collection.create(attributes = {})
-
返回一个新的集合类型对象,该对象已使用
attributes
初始化,通过联接表链接到该对象,并且已经保存(如果它通过了验证)。 collection.reload
示例
class Developer < ActiveRecord::Base
has_and_belongs_to_many :projects
end
声明 has_and_belongs_to_many :projects
将添加以下方法(以及更多方法)
developer = Developer.find(11)
project = Project.find(9)
developer.projects
developer.projects << project
developer.projects.delete(project)
developer.projects.destroy(project)
developer.projects = [project]
developer.project_ids
developer.project_ids = [9]
developer.projects.clear
developer.projects.empty?
developer.projects.size
developer.projects.find(9)
developer.projects.exists?(9)
developer.projects.build # similar to Project.new(developer_id: 11)
developer.projects.create # similar to Project.create(developer_id: 11)
developer.projects.reload
声明可以包含一个 options
哈希来专门化关联的行为。
范围
您可以传递一个第二个参数 scope
作为可调用对象(例如 proc 或 lambda)来检索特定的一组记录,或者在您访问相关联的集合时自定义生成的查询。
范围示例
has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
has_and_belongs_to_many :categories, ->(post) {
where("default_category = ?", post.default_category)
}
扩展
extension
参数允许您将代码块传递给 has_and_belongs_to_many
关联。这对于添加新的查找器、创建器和其他工厂类型方法非常有用,这些方法可以作为关联的一部分使用。
扩展示例
has_and_belongs_to_many :contractors do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by(first_name: first_name, last_name: last_name)
end
end
选项
:class_name
-
指定关联的类名。仅当无法从关联名称推断出该名称时才使用它。因此,
has_and_belongs_to_many :projects
默认情况下将链接到 Project 类,但如果实际类名为 SuperProject,则必须使用此选项指定它。 :join_table
-
如果基于词法顺序的默认名称不是您想要的,请指定联接表的名称。警告:如果您要覆盖任一类的表名,则
table_name
方法必须在任何has_and_belongs_to_many
声明下声明才能起作用。 :foreign_key
-
指定用于关联的外键。默认情况下,它被猜测为该类的名称,以小写形式并添加“_id”后缀。因此,一个创建与 Project 的
has_and_belongs_to_many
关联的 Person 类将使用“person_id”作为默认的:foreign_key
。设置
:foreign_key
选项将阻止自动检测关联的反向,因此通常最好也设置:inverse_of
选项。 :association_foreign_key
-
指定关联接收方使用的外键。默认情况下,它被猜测为相关联类的名称,以小写形式并添加“_id”后缀。因此,如果 Person 类创建与 Project 的
has_and_belongs_to_many
关联,则该关联将使用“project_id”作为默认的:association_foreign_key
。 :validate
-
当设置为
true
时,在保存父对象时验证添加到关联中的新对象。默认情况下为true
。如果您想确保在每次更新时重新验证相关联的对象,请使用validates_associated
。 :autosave
-
如果为真,则在保存父对象时始终保存相关联的对象,或者如果对象被标记为要销毁,则销毁它们。如果为假,则永远不要保存或销毁相关联的对象。默认情况下,只保存新的相关联的对象。
注意,
NestedAttributes::ClassMethods#accepts_nested_attributes_for
将:autosave
设置为true
。 :strict_loading
-
每次通过此关联加载相关联的记录时,强制执行严格加载。
选项示例
has_and_belongs_to_many :projects
has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
has_and_belongs_to_many :nations, class_name: "Country"
has_and_belongs_to_many :categories, join_table: "prods_cats"
has_and_belongs_to_many :categories, -> { readonly }
has_and_belongs_to_many :categories, strict_loading: true
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/associations.rb, line 1870 def has_and_belongs_to_many(name, scope = nil, **options, &extension) habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self) builder = Builder::HasAndBelongsToMany.new name, self, options join_model = builder.through_model const_set join_model.name, join_model private_constant join_model.name middle_reflection = builder.middle_reflection join_model Builder::HasMany.define_callbacks self, middle_reflection Reflection.add_reflection self, middle_reflection.name, middle_reflection middle_reflection.parent_reflection = habtm_reflection include Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 def destroy_associations association(:#{middle_reflection.name}).delete_all(:delete_all) association(:#{name}).reset super end RUBY } hm_options = {} hm_options[:through] = middle_reflection.name hm_options[:source] = join_model.right_reflection.name [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend, :strict_loading].each do |k| hm_options[k] = options[k] if options.key? k end has_many name, scope, **hm_options, &extension _reflections[name].parent_reflection = habtm_reflection end
has_many(name, scope = nil, **options, &extension) 链接
指定一对多关联。将添加以下方法用于检索和查询相关联对象的集合
collection
是作为 name
参数传递的符号的占位符,因此 has_many :clients
将添加 clients.empty?
等方法。
collection
collection<<(object, ...)
-
通过将关联对象的外部键设置为集合的主键,将一个或多个对象添加到集合中。 请注意,此操作会立即触发更新 SQL,而不会等待父对象的保存或更新调用,除非父对象是新记录。 这也会运行关联对象(s)的验证和回调。
collection.delete(object, ...)
-
通过将关联对象的外部键设置为
NULL
,从集合中删除一个或多个对象。 如果对象与dependent: :destroy
关联,则会额外销毁对象,如果对象与dependent: :delete_all
关联,则会删除对象。如果使用
:through
选项,则默认情况下会删除(而不是将为NULL
)连接记录,但您可以指定dependent: :destroy
或dependent: :nullify
来覆盖此行为。 collection.destroy(object, ...)
-
通过对每个记录运行
destroy
来从集合中删除一个或多个对象,无论任何依赖选项,确保运行回调。如果使用
:through
选项,则会破坏连接记录,而不是对象本身。 collection=objects
-
通过根据需要删除和添加对象来替换集合内容。 如果
:through
选项为真,则除了销毁回调之外,连接模型中的回调都会被触发,因为默认情况下删除是直接的。 您可以指定dependent: :destroy
或dependent: :nullify
来覆盖此行为。 collection_singular_ids
-
返回一个包含关联对象的 ID 的数组。
collection_singular_ids=ids
-
使用
ids
中的主键标识的对象替换集合。 此方法加载模型并调用collection=
。 见上文。 collection.clear
-
从集合中删除所有对象。 如果对象与
dependent: :destroy
关联,则会销毁关联对象,如果对象与dependent: :delete_all
关联,则会直接从数据库中删除对象,否则将它们的外部键设置为NULL
。 如果:through
选项为真,则不会在连接模型上调用任何销毁回调。 连接模型将直接删除。 collection.empty?
-
如果没有相关联的对象,则返回
true
。 collection.size
-
返回相关联对象的数量。
collection.find(...)
-
根据与
ActiveRecord::FinderMethods#find
相同的规则查找关联对象。 collection.exists?(...)
-
检查是否存在具有给定条件的相关联对象。使用与
ActiveRecord::FinderMethods#exists?
相同的规则。 collection.build(attributes = {}, ...)
-
返回一个或多个已使用
attributes
实例化并通过外部键链接到此对象的集合类型的新对象,但尚未保存。 collection.create(attributes = {})
-
返回一个已使用
attributes
实例化并通过外部键链接到此对象的集合类型的新对象,并且该对象已经保存(如果它通过了验证)。 注意:这只有在基本模型已存在于数据库中时才有效,而不是在它是新(未保存)记录时! collection.create!(attributes = {})
-
与
collection.create
相同,但在记录无效时会引发ActiveRecord::RecordInvalid
。 collection.reload
示例
class Firm < ActiveRecord::Base
has_many :clients
end
声明has_many :clients
会添加以下方法(以及更多)
firm = Firm.find(2)
client = Client.find(6)
firm.clients # similar to Client.where(firm_id: 2)
firm.clients << client
firm.clients.delete(client)
firm.clients.destroy(client)
firm.clients = [client]
firm.client_ids
firm.client_ids = [6]
firm.clients.clear
firm.clients.empty? # similar to firm.clients.size == 0
firm.clients.size # similar to Client.count "firm_id = 2"
firm.clients.find # similar to Client.where(firm_id: 2).find(6)
firm.clients.exists?(name: 'ACME') # similar to Client.exists?(name: 'ACME', firm_id: 2)
firm.clients.build # similar to Client.new(firm_id: 2)
firm.clients.create # similar to Client.create(firm_id: 2)
firm.clients.create! # similar to Client.create!(firm_id: 2)
firm.clients.reload
声明还可以包含一个 options
哈希表来专门化关联的行为。
范围
您可以传递一个第二个参数 scope
作为可调用对象(例如 proc 或 lambda)来检索特定的一组记录,或者在您访问相关联的集合时自定义生成的查询。
范围示例
has_many :comments, -> { where(author_id: 1) }
has_many :employees, -> { joins(:address) }
has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) }
扩展
extension
参数允许您将一个代码块传递到has_many
关联中。 这对于添加新的查找器、创建者和其他工厂类型方法以用作关联的一部分非常有用。
扩展示例
has_many :employees do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by(first_name: first_name, last_name: last_name)
end
end
选项
:class_name
-
指定关联的类名。 仅当无法从关联名称推断出该名称时才使用它。 因此,
has_many :products
默认情况下将链接到Product
类,但如果真正的类名为SpecialProduct
,则必须使用此选项指定它。 :foreign_key
-
指定用于关联的外部键。 默认情况下,这将被猜测为该类的名称(小写)和“_id”后缀。 因此,Person 类创建一个
has_many
关联将使用“person_id”作为默认的:foreign_key
。设置
:foreign_key
选项将阻止自动检测关联的反向,因此通常最好也设置:inverse_of
选项。 :foreign_type
-
如果这是一个多态关联,则指定用于存储关联对象的类型的列。 默认情况下,这将被猜测为在“as”选项上指定的,并带有“_type”后缀的多态关联的名称。 因此,定义了
has_many :tags, as: :taggable
关联的类将使用“taggable_type”作为默认的:foreign_type
。 :primary_key
-
指定要用作关联主键的列的名称。 默认情况下,它是
id
。 :dependent
-
控制所有者被销毁时对关联对象的影响。 请注意,这些是作为回调实现的,Rails 按顺序执行回调。 因此,其他类似的回调可能会影响
:dependent
行为,而:dependent
行为也可能会影响其他回调。-
nil
不执行任何操作(默认)。 -
:destroy
会导致所有关联对象也被销毁。 -
:destroy_async
在后台作业中销毁所有关联对象。 警告:如果关联由数据库中的外部键约束支持,请勿使用此选项。 外部键约束操作将在删除其所有者的同一个事务中发生。 -
:delete_all
会导致所有关联对象直接从数据库中删除(因此不会执行回调)。 -
:nullify
会导致将外部键设置为NULL
。 多态类型也会在多态关联上变为NULL
。Callbacks
不会执行。 -
:restrict_with_exception
会导致在存在任何关联记录时引发 ActiveRecord::DeleteRestrictionError 异常。 -
:restrict_with_error
会导致在存在任何关联对象时向所有者添加错误。
如果与
:through
选项一起使用,则连接模型上的关联必须是belongs_to
,并且被删除的记录是连接记录,而不是关联记录。如果在作用域关联上使用
dependent: :destroy
,则仅会销毁作用域对象。 例如,如果 Post 模型定义了has_many :comments, -> { where published: true }, dependent: :destroy
,并且在帖子中调用了destroy
,则只会销毁已发布的评论。 这意味着数据库中的任何未发布的评论仍然包含指向现已删除帖子的外部键。 -
:counter_cache
-
此选项可用于配置自定义命名的
:counter_cache
。 只有在您在belongs_to
关联上自定义了:counter_cache
的名称时,才需要此选项。 :as
-
指定多态接口(参见
belongs_to
)。 :through
-
指定要执行查询的关联。 这可以是任何其他类型的关联,包括其他
:through
关联。:class_name
、:primary_key
和:foreign_key
的选项将被忽略,因为关联使用源反射。如果连接模型上的关联是
belongs_to
,则可以修改集合,并且会根据需要自动创建和删除:through
模型上的记录。 否则,集合是只读的,因此您应该直接操作:through
关联。如果您要修改关联(而不仅仅是从关联中读取),那么最好在连接模型上的源关联上设置
:inverse_of
选项。 这允许构建关联记录,这些记录会在保存时自动创建适当的连接模型记录。 有关更多详细信息,请参见关联连接模型和设置反向关联。 :disable_joins
-
指定是否应跳过关联的连接。 如果设置为 true,则将生成两个或多个查询。 请注意,在某些情况下,如果应用了 order 或 limit,则由于数据库限制,它将在内存中完成。 此选项仅适用于
has_many :through
关联,因为has_many
本身不执行连接。 :source
-
指定
has_many
:through
查询使用的源关联名称。 仅当无法从关联推断出名称时才使用它。has_many :subscribers, through: :subscriptions
将在 Subscription 上查找:subscribers
或:subscriber
,除非给出:source
。 :source_type
-
指定
has_many
:through
查询使用的源关联类型,其中源关联是多态belongs_to
。 :validate
-
当设置为
true
时,在保存父对象时验证添加到关联中的新对象。默认情况下为true
。如果您想确保在每次更新时重新验证相关联的对象,请使用validates_associated
。 :autosave
-
如果为 true,则始终保存关联对象或在标记为销毁时销毁它们,在保存父对象时。 如果为 false,则永远不会保存或销毁关联对象。 默认情况下,仅保存是新记录的关联对象。 此选项作为
before_save
回调实现。 由于回调按定义顺序运行,因此可能需要在任何用户定义的before_save
回调中显式保存关联对象。注意,
NestedAttributes::ClassMethods#accepts_nested_attributes_for
将:autosave
设置为true
。 :inverse_of
-
指定关联对象上
belongs_to
关联的名称,该关联是此has_many
关联的反向关联。 有关更多详细信息,请参见双向关联。 :extend
-
指定一个模块或模块数组,这些模块将被扩展到返回的关联对象中。 这对于在关联上定义方法很有用,尤其是在它们应该在多个关联对象之间共享时。
:strict_loading
-
当设置为
true
时,每次通过此关联加载关联记录时都会强制严格加载。 :ensuring_owner_was
-
指定要在所有者上调用的实例方法。该方法必须返回 true,以便相关联的记录在后台作业中被删除。
:query_constraints
-
用作复合外键。定义用于查询相关联对象的列列表。这是一个可选选项。默认情况下,
Rails
将尝试自动推断该值。当设置该值时,Array
的大小必须与相关联模型的主键或query_constraints
的大小匹配。 :index_errors
-
允许通过在错误属性名称中包含索引来区分来自关联记录的多个验证错误,例如
roles[2].level
。 当设置为true
时,索引基于关联顺序,即数据库顺序,尚未持久化的新记录位于最后。 当设置为:nested_attributes_order
时,索引基于通过嵌套属性设置器接收的记录顺序,当使用accepts_nested_attributes_for时。 - :before_add
-
定义一个关联回调,该回调会在将对象添加到关联集合之前触发。
- :after_add
-
定义一个关联回调,该回调会在将对象添加到关联集合之后触发。
- :before_remove
-
定义一个关联回调,该回调会在从关联集合中删除对象之前触发。
- :after_remove
-
定义一个关联回调,该回调会在从关联集合中删除对象之后触发。
选项示例
has_many :comments, -> { order("posted_on") }
has_many :comments, -> { includes(:author) }
has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
has_many :tracks, -> { order("position") }, dependent: :destroy
has_many :comments, dependent: :nullify
has_many :tags, as: :taggable
has_many :reports, -> { readonly }
has_many :subscribers, through: :subscriptions, source: :user
has_many :subscribers, through: :subscriptions, disable_joins: true
has_many :comments, strict_loading: true
has_many :comments, query_constraints: [:blog_id, :post_id]
has_many :comments, index_errors: :nested_attributes_order
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/associations.rb, line 1302 def has_many(name, scope = nil, **options, &extension) reflection = Builder::HasMany.build(self, name, scope, options, &extension) Reflection.add_reflection self, name, reflection end
has_one(name, scope = nil, **options) 链接
指定与另一个类的单对一关联。 此方法仅在另一个类包含外部键时才应使用。 如果当前类包含外部键,则应改用belongs_to
。 有关何时使用has_one
以及何时使用belongs_to
的更多详细信息,请参见它是属于还是一对一关联吗?。
将添加用于检索和查询单个关联对象的以下方法
association
是作为name
参数传递的符号的占位符,因此has_one :manager
将添加manager.nil?
等。
association
-
返回关联的对象。如果没有找到,则返回
nil
。 association=(associate)
-
分配关联对象,提取主键,将其设置为外部键并保存关联对象。 为了避免数据库不一致,在分配新对象时永久删除现有的关联对象,即使新对象未保存到数据库。
build_association(attributes = {})
-
返回一个新的关联类型对象,该对象已使用
attributes
实例化并通过外键链接到当前对象,但尚未保存。 create_association(attributes = {})
-
返回一个新的关联类型对象,该对象已使用
attributes
实例化,通过外键链接到当前对象,并且已经保存(如果它通过了验证)。 create_association!(attributes = {})
-
执行与
create_association
相同的操作,但如果记录无效,则会引发ActiveRecord::RecordInvalid
。 reload_association
-
返回关联的对象,强制进行数据库读取。
reset_association
-
卸载关联的对象。下次访问将从数据库中查询它。
示例
class Account < ActiveRecord::Base
has_one :beneficiary
end
声明has_one :beneficiary
会添加以下方法(以及更多)
account = Account.find(5)
beneficiary = Beneficiary.find(8)
account.beneficiary # similar to Beneficiary.find_by(account_id: 5)
account.beneficiary = beneficiary # similar to beneficiary.update(account_id: 5)
account.build_beneficiary # similar to Beneficiary.new(account_id: 5)
account.create_beneficiary # similar to Beneficiary.create(account_id: 5)
account.create_beneficiary! # similar to Beneficiary.create!(account_id: 5)
account.reload_beneficiary
account.reset_beneficiary
范围
您可以传递一个第二个参数 scope
作为可调用对象(即 proc 或 lambda)来检索特定记录或自定义访问关联对象时生成的查询。
范围示例
has_one :author, -> { where(comment_id: 1) }
has_one :employer, -> { joins(:company) }
has_one :latest_post, ->(blog) { where("created_at > ?", blog.enabled_at) }
选项
声明还可以包含一个 options
哈希表来专门化关联的行为。
选项是
:class_name
-
指定关联的类名。 仅当无法从关联名称推断出该名称时才使用它。 因此,
has_one :manager
默认情况下将链接到 Manager 类,但如果真正的类名为 Person,则必须使用此选项指定它。 :dependent
-
控制关联对象在其所有者销毁时发生的情况
-
nil
不执行任何操作(默认)。 -
:destroy
会导致关联对象也被销毁 -
:destroy_async
会导致关联对象在后台作业中被销毁。**警告:** 如果关联由数据库中的外键约束支持,请勿使用此选项。外键约束操作将在删除其所有者的同一事务中发生。 -
:delete
会导致关联对象直接从数据库中删除(因此回调不会执行) -
:nullify
会导致外键被设置为NULL
。多态类型列在多态关联中也会被设置为NULL
。Callbacks
不会执行。 -
:restrict_with_exception
会导致如果存在关联记录,则引发 ActiveRecord::DeleteRestrictionError 异常 -
:restrict_with_error
会导致如果存在关联对象,则向所有者添加错误
请注意,在使用
:through
选项时,:dependent
选项将被忽略。 -
:foreign_key
-
指定用于关联的外键。默认情况下,这将被推测为该类的名称,以小写字母表示,并附加“_id”。因此,Person 类创建
has_one
关联将使用“person_id”作为默认的:foreign_key
。设置
:foreign_key
选项将阻止自动检测关联的反向,因此通常最好也设置:inverse_of
选项。 :foreign_type
-
指定用于存储关联对象类型的列,如果这是一个多态关联。默认情况下,这将被推测为在“as”选项上指定的,带有“_type”后缀的多态关联的名称。因此,定义了
has_one :tag, as: :taggable
关联的类将使用“taggable_type”作为默认的:foreign_type
。 :primary_key
-
指定返回用于关联的主键的方法。默认情况下,这是
id
。 :as
-
指定多态接口(参见
belongs_to
)。 :through
-
指定通过它执行查询的连接模型。
:class_name
、:primary_key
和:foreign_key
的选项将被忽略,因为关联使用源反射。您只能通过连接模型上的has_one
或belongs_to
关联使用:through
查询。如果连接模型上的关联是
belongs_to
,则可以修改集合,并且会根据需要自动创建和删除:through
模型上的记录。 否则,集合是只读的,因此您应该直接操作:through
关联。如果您要修改关联(而不仅仅是从关联中读取),那么最好在连接模型上的源关联上设置
:inverse_of
选项。 这允许构建关联记录,这些记录会在保存时自动创建适当的连接模型记录。 有关更多详细信息,请参见关联连接模型和设置反向关联。 :disable_joins
-
指定是否应为关联跳过连接。如果设置为 true,将生成两个或多个查询。请注意,在某些情况下,如果应用了排序或限制,由于数据库限制,它将在内存中完成。此选项仅适用于
has_one :through
关联,因为has_one
本身不执行连接。 :source
-
指定由
has_one
:through
查询使用的源关联名称。仅当名称无法从关联中推断时才使用它。has_one :favorite, through: :favorites
将在 Favorite 上查找:favorite
,除非提供:source
。 :source_type
-
指定由
has_one
:through
查询使用的源关联类型,其中源关联是多态的belongs_to
. :validate
-
当设置为
true
时,在保存父对象时验证添加到关联的新对象。默认情况下为false
。如果您希望确保关联对象在每次更新时都重新验证,请使用validates_associated
。 :autosave
-
如果为
true
,则在保存父对象时始终保存关联对象或销毁它(如果标记为销毁)。如果为false
,则从不保存或销毁关联对象。默认情况下,仅当关联对象是新记录时才保存它。将此选项设置为
true
也会在关联对象上启用验证,除非使用validate: false
显式禁用。这是因为保存具有无效关联对象的将失败,因此任何关联对象都将经过验证检查。注意,
NestedAttributes::ClassMethods#accepts_nested_attributes_for
将:autosave
设置为true
。 :touch
-
如果为真,则当此记录保存或销毁时,相关联的对象将被触碰(
updated_at
/updated_on
属性设置为当前时间)。如果您指定一个符号,该属性将与当前时间一起更新,此外还有updated_at
/updated_on
属性。请注意,触碰时不会执行任何验证,并且只会执行after_touch
、after_commit
和after_rollback
回调。 :inverse_of
-
指定关联对象上
belongs_to
关联的名称,它是此has_one
关联的逆关联。有关更多详细信息,请参阅 双向关联。 :required
-
当设置为
true
时,关联本身也将被验证。这将验证关联本身,而不是 id。您可以使用:inverse_of
避免在验证期间进行额外的查询。 :strict_loading
-
每次通过此关联加载相关联的记录时,强制执行严格加载。
:ensuring_owner_was
-
指定要在所有者上调用的实例方法。该方法必须返回 true,以便相关联的记录在后台作业中被删除。
:query_constraints
-
用作复合外键。定义用于查询相关联对象的列列表。这是一个可选选项。默认情况下,
Rails
将尝试自动推断该值。当设置该值时,Array
的大小必须与相关联模型的主键或query_constraints
的大小匹配。
选项示例
has_one :credit_card, dependent: :destroy # destroys the associated credit card
has_one :credit_card, dependent: :nullify # updates the associated records foreign
# key value to NULL rather than destroying it
has_one :last_comment, -> { order('posted_on') }, class_name: "Comment"
has_one :project_manager, -> { where(role: 'project_manager') }, class_name: "Person"
has_one :attachment, as: :attachable
has_one :boss, -> { readonly }
has_one :club, through: :membership
has_one :club, through: :membership, disable_joins: true
has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
has_one :credit_card, required: true
has_one :credit_card, strict_loading: true
has_one :employment_record_book, query_constraints: [:organization_id, :employee_id]
来源: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/associations.rb, line 1498 def has_one(name, scope = nil, **options) reflection = Builder::HasOne.build(self, name, scope, options) Reflection.add_reflection self, name, reflection end