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
多对多¶ ↑
有两种方法可以构建多对多关系。
第一种方法使用带有 :through 选项和连接模型的 has_many 关联,因此有两个阶段的关联。
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 关联?¶ ↑
两者都表示 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 等。
关联连接模型¶ ↑
可以使用 :through 选项配置 Has Many 关联,以使用显式的连接模型来检索数据。这与 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
最后一行应该保存 through 记录(一个 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 关联指南。
自动推断反向关联使用基于类名的启发式方法,因此它可能不适用于所有关联,特别是名称非标准的关联。
你可以通过设置 :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 模型的基础模型存储在多态关联的 type 列中。继续上面的 asset 示例,假设有 guest posts 和 member posts 使用 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|
上面的代码将加载所有评论及其所有关联的作者和 gravatars。你可以混合搭配任何符号、数组和哈希的组合来检索要加载的关联。
所有这些功能都不应让你误以为你可以以零性能成本提取大量数据,仅仅因为你减少了查询次数。数据库仍然需要将所有数据发送到 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 语句,而不只是关联。因此,没有已批准评论的帖子将不会被返回,因为条件适用于整个 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
尝试预加载 addressable 模型的调用
Address.includes(:addressable)
这将执行一次查询来加载地址,并为每个 addressable 类型执行一次查询来加载 addressables。例如,如果所有 addressables 都是 Person 或 Company 类,则总共将执行 3 次查询。要加载的 addressable 类型列表是根据加载的地址确定的。如果 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
Acts as tree 示例
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 关联指南。
从关联中删除¶ ↑
依赖关联¶ ↑
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')),你会希望“food”标签从帖子中解除链接,而不是从数据库中删除标签本身。
然而,有一些例子表明这种策略并不适用。例如,假设一个人有很多项目,每个项目有很多任务。如果我们删除一个人的一项任务,我们可能不希望项目也被删除。在这种情况下,delete 方法实际上不起作用:只有当连接模型上的关联是 belongs_to 时才能使用它。在其他情况下,你会被期望直接在关联记录或 :through 关联上执行操作。
对于常规的 has_many,没有“关联记录”和“链接”之间的区别,所以只有一个选择会被删除。
对于 has_and_belongs_to_many 和 has_many :through,如果你想删除关联记录本身,你总是可以这样做:person.tasks.each(&:destroy)。
已弃用的 Associations¶ ↑
Associations 可以通过传递 deprecated: true 来标记为已弃用。
has_many :posts, deprecated: true
当使用已弃用的关联时,会通过 Active Record 日志记录器发出警告,尽管可以通过配置获得更多选项。
消息包含一些有助于理解已弃用用法的信息
The association Author#posts is deprecated, the method post_ids was invoked (...) The association Author#posts is deprecated, referenced in query to preload records (...)
上面示例中的点将是应用程序级别的用法发生位置,以帮助定位触发警告的原因。该位置是使用 Active Record backtrace cleaner 计算的。
什么被认为是使用?¶ ↑
-
调用任何关联方法,例如
posts、posts=等。 -
如果关联接受嵌套属性,则为这些属性赋值。
-
如果关联是 through 关联,并且其一些嵌套关联已弃用,则每当使用顶级 through 关联时,你都会收到关于它们的警告。这适用于 through 关联本身是否已弃用。
-
执行引用关联的查询。例如执行
eager_load(:posts)、joins(author: :posts)等。 -
如果关联具有
:dependent选项,则销毁关联记录会发出警告(因为这会产生如果关联被移除就不会发生的影响)。 -
如果关联具有
:touch选项,则保存或销毁记录会发出警告(因为这会产生如果关联被移除就不会发生的影响)。
不发出警告的情况¶ ↑
大多数以下边缘情况的原理是 Active Record 惰性地访问关联,在使用时。在此之前,对关联的引用基本上只是一个 Ruby 符号。
-
如果
posts已弃用,has_many :comments, through: :posts不会发出警告。comments关联的使用会报告posts的使用,正如我们上面解释的,但has_many本身的定义不会。 -
同样,
accepts_nested_attributes_for :posts本身不会发出警告。对 posts 属性的赋值会发出警告,如上所述,但accepts_nested_attributes_for调用本身不会。 -
同样,如果一个关联声明为已弃用关联的反向关联,宏本身也不会发出警告。
-
同样,声明
validates_associated :posts本身不会发出警告,尽管在使用时会报告访问。 -
Relation查询方法,如Author.includes(:posts)本身不会发出警告。此时,它是一个关系,内部存储一个符号供以后使用。如前一节所述,你会在查询执行时/如果查询执行时收到警告。 -
访问关联的反射对象,例如
Author.reflect_on_association(:posts)或Author.reflect_on_all_associations不会发出警告。
配置¶ ↑
已弃用用法的报告可以配置
config.active_record.deprecated_associations_options = { ... }
如果存在,则必须是具有 :mode 和/或 :backtrace 键的哈希。
模式¶ ↑
-
在
:warn模式下,使用会发出警告,其中包含访问发生的应用程序级别位置(如果有)。这是默认模式。 -
在
:raise模式下,使用会引发一个带有类似消息的ActiveRecord::DeprecatedAssociationError,并在异常对象中包含干净的回溯。 -
在
:notify模式下,会发布一个deprecated_association.active_recordActive Support 通知。事件负载包含关联反射(:reflection)、访问发生的应用程序级别位置(:location)(一个 Thread::Backtrace::Location 对象,或nil),以及一个弃用消息(:message)。
回溯¶ ↑
如果 :backtrace 为 true,警告会在消息中包含干净的回溯,并且通知会在负载中有一个 :backtrace 键,其中包含一个干净的 Thread::Backtrace::Location 对象数组。异常始终会设置干净的堆栈跟踪。
干净的回溯是通过 Active Record backtrace cleaner 计算的。在 Rails 应用程序中,这默认与 Rails.backtrace_cleaner 相同。
使用 ActiveRecord::AssociationTypeMismatch 进行类型安全¶ ↑
如果你尝试将一个对象分配给一个与推断或指定的 :class_name 不匹配的关联,你将得到一个 ActiveRecord::AssociationTypeMismatch。
选项¶ ↑
所有关联宏都可以通过选项进行专门化。这使得比简单的、可推断的情况更复杂的情况成为可能。
实例公共方法
belongs_to(name, scope = nil, **options) Link
指定与其他类的“一对一”关联。只有当当前类包含外键时,才应使用此方法。如果另一个类包含外键,则应改用 has_one。有关何时使用 has_one 和何时使用 belongs_to 的更多详细信息,请参阅 Is it a belongs_to or has_one association?。
将添加用于检索和查询单个关联对象的方法,该对象保存了 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,则必须使用此选项指定它。:class_name不支持多态关联,因为在这种情况下,关联记录的类名存储在 type 列中。 :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来缓存关联对象数量在关联类上。当此类的对象被创建时,计数器缓存会增加,当它被销毁时,计数器缓存会减少。这要求在关联类上使用一个名为#{table_name}_count的列(例如,对于属于 Comment 类的 Comment 类,为 comments_count)——即在关联类(例如 Post 类)上创建#{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-
如果为 true,当此记录被保存或销毁时,关联对象将被“触碰”(
updated_at/updated_on属性设置为当前时间)。如果你指定一个符号,该属性将与当前时间一起更新,此外还有updated_at/updated_on属性。请注意,在触碰时不会执行任何验证,只有after_touch、after_commit和after_rollback回调将被执行。 :inverse_of-
指定关联对象上作为此
belongs_to关联反向的has_one或has_many关联的名称。有关更多详细信息,请参阅 Bi-directional associations。 :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的大小匹配。 :deprecated-
如果为 true,则将关联标记为已弃用。使用已弃用的关联会发出报告。请查阅上面的类文档以获取详细信息。
选项示例
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]
has_and_belongs_to_many(name, scope = nil, **options, &extension) Link
指定与其他类的“多对多”关系。这通过中间连接表将两个类关联起来。除非连接表明确指定为选项,否则它将使用类名称的词汇顺序进行推断。因此,Developer 和 Project 之间的连接将得到默认连接表名“developers_projects”,因为“D”在字母顺序上 precedes “P”。请注意,此优先级是使用 < 运算符为 String 计算的。这意味着如果字符串长度不同,并且在比较到最短长度时字符串相等,那么较长的字符串被认为比较短的字符串具有更高的词汇优先级。例如,你会期望表“paper_boxes”和“papers”生成连接表名“papers_paper_boxes”,但它实际上生成连接表名“paper_boxes_papers”。注意这个警告,如果你需要的话,请使用自定义的 :join_table 选项。如果你的表共享一个公共前缀,它将只在开头出现一次。例如,表“catalog_categories”和“catalog_products”生成连接表名“catalog_categories_products”。
连接表不应有主键或关联的模型。你必须手动生成连接表,例如使用迁移
class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[8.1] def change create_join_table :developers, :projects end end
为每个列添加索引以加快连接过程也是个好主意。然而,在 MySQL 中,建议为两个列都添加复合索引,因为 MySQL 在查找过程中每个表只使用一个索引。
为检索和查询添加以下方法
collection 是作为 name 参数传递的符号的占位符,因此 has_and_belongs_to_many :categories 将添加(除其他外)categories.empty?。
collectioncollection<<(object, ...)-
通过在连接表中创建关联来添加一个或多个对象(
collection.push和collection.concat是此方法的别名)。请注意,此操作会立即触发更新 SQL,而不会等待父对象的保存或更新调用,除非父对象是新记录。 collection.delete(object, ...)-
通过从连接表中删除其关联来从集合中移除一个或多个对象。这不会销毁对象。
collection.destroy(object, ...)-
通过在连接表中的每个关联上运行 destroy 来移除一个或多个对象,覆盖任何 dependent 选项。这不会销毁对象。
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-
如果为 true,则在保存父对象时,始终保存关联对象或销毁标记为要销毁的对象。如果为 false,则永远不保存或销毁关联对象。默认情况下,仅保存新记录的关联对象。
注意
NestedAttributes::ClassMethods#accepts_nested_attributes_for将:autosave设置为true。 :strict_loading-
每次通过此关联加载关联记录时,都会强制执行严格加载。
:deprecated-
如果为 true,则将关联标记为已弃用。使用已弃用的关联会发出报告。请查阅上面的类文档以获取详细信息。
选项示例
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
# File activerecord/lib/active_record/associations.rb, line 2008 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, :deprecated].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) Link
指定“一对多”关联。将添加以下用于检索和查询关联对象集合的方法
collection 是作为 name 参数传递的符号的占位符,因此 has_many :clients 将添加(除其他外)clients.empty?。
collectioncollection<<(object, ...)-
通过设置外键为集合的主键来添加一个或多个对象到集合中。请注意,此操作会立即触发更新 SQL,而不会等待父对象的保存或更新调用,除非父对象是新记录。这还将运行关联对象的验证和回调。
collection.delete(object, ...)-
通过将外键设置为
NULL来从集合中移除一个或多个对象。如果对象与dependent: :destroy关联,则会额外销毁对象,如果它们与dependent: :delete_all关联,则会删除它们。如果使用了
:through选项,则默认情况下会删除连接记录(而不是置空),但你可以指定dependent: :destroy或dependent: :nullify来覆盖此行为。 collection.destroy(object, ...)-
通过在每个记录上运行
destroy来移除一个或多个对象,而不考虑任何 dependent 选项,确保回调被运行。如果使用了
:through选项,则会销毁连接记录,而不是对象本身。 collection=objects-
通过删除和添加对象来替换集合的内容。如果
:through选项为 true,则连接模型中的回调将被触发,但不包括 destroy 回调,因为默认情况下删除是直接进行的。你可以指定dependent: :destroy或dependent: :nullify来覆盖此行为。 collection_singular_ids-
返回关联对象的 id 数组
collection_singular_ids=ids-
通过
ids中标识的主键替换集合。此方法加载模型并调用collection=。请参阅上面。 collection.clear-
移除集合中的所有对象。这会销毁关联对象(如果它们与
dependent: :destroy关联),直接从数据库删除它们(如果dependent: :delete_all),否则将其外键设置为NULL。如果使用了:through选项,则连接模型上不会调用 destroy 回调。连接模型将被直接删除。 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”后缀。因此,定义
has_many关联的 Person 类将使用“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。多态类型在多态关联上也会被置空。不会执行Callbacks。 -
:restrict_with_exception如果存在任何关联记录,则会引发 ActiveRecord::DeleteRestrictionError 异常。 -
:restrict_with_error如果存在任何关联对象,则会向所有者添加错误。
如果与
:through选项一起使用,则连接模型上的关联必须是belongs_to,并且被删除的记录是连接记录,而不是关联记录。如果在作用域关联上使用
dependent: :destroy,则仅销毁作用域内的对象。例如,如果 Post 模型定义了has_many :comments, -> { where published: true }, dependent: :destroy并且在调用 post 的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选项是一个好主意。这允许创建关联记录,这些记录在保存时将自动创建相应的连接模型记录。有关更多详细信息,请参阅 Association Join Models 和 Setting Inverses。 :disable_joins-
指定是否应跳过关联的连接。如果设置为 true,将生成两个或更多查询。请注意,在某些情况下,如果应用了排序或限制,由于数据库限制,将在内存中进行。此选项仅适用于
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-
指定关联对象上作为此
has_many关联反向的belongs_to关联的名称。有关更多详细信息,请参阅 Bi-directional associations。 :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 时嵌套属性 setter 收到的记录顺序。 - :before_add
-
定义一个 关联回调,该回调在将对象添加到关联集合之前触发。
- :after_add
-
定义一个 关联回调,该回调在将对象添加到关联集合之后触发。
- :before_remove
-
定义一个 关联回调,该回调在从关联集合中移除对象之前触发。
- :after_remove
-
定义一个 关联回调,该回调在从关联集合中移除对象之后触发。
:deprecated-
如果为 true,则将关联标记为已弃用。使用已弃用的关联会发出报告。请查阅上面的类文档以获取详细信息。
选项示例
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
has_one(name, scope = nil, **options) Link
指定与其他类的“一对一”关联。只有当另一个类包含外键时,才应使用此方法。如果当前类包含外键,则应改用 belongs_to。有关何时使用 has_one 和何时使用 belongs_to 的更多详细信息,请参阅 Is it a belongs_to or has_one association?。
将添加以下用于检索和查询单个关联对象的方法
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。回调不会执行。 -
:restrict_with_exception如果存在关联记录,则会引发 ActiveRecord::DeleteRestrictionError 异常 -
:restrict_with_error如果存在关联对象,则会将错误添加到所有者上
请注意,使用
:through选项时会忽略:dependent选项。 -
:foreign_key-
指定用于此关联的外键。默认情况下,此名称被猜测为当前类的名称(小写)加上 "_id" 后缀。因此,一个创建一个
has_one关联的 Person 类将使用 "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-
指定一个通过该关联执行查询。
through关联必须是has_one、has_one :through或非多态belongs_to。也就是说,一个非多态的单数关联。:class_name、:primary_key和:foreign_key选项会被忽略,因为关联使用源反射。您只能通过连接模型上的has_one或belongs_to关联来使用:through查询。如果连接模型上的关联是
belongs_to,则可以修改集合,并且:through模型上的记录将根据需要自动创建和移除。否则,集合是只读的,因此你应该直接操作:through关联。如果你打算修改关联(而不仅仅是从中读取),那么在连接模型上设置源关联的
:inverse_of选项是一个好主意。这允许创建关联记录,这些记录在保存时将自动创建相应的连接模型记录。有关更多详细信息,请参阅 Association Join Models 和 Setting Inverses。 :disable_joins-
指定是否应跳过关联的连接。如果设置为 true,将生成两个或更多查询。请注意,在某些情况下,如果应用了 order 或 limit,由于数据库限制,将在内存中完成。此选项仅适用于
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-
如果为 true,当此记录被保存或销毁时,关联对象将被“触碰”(
updated_at/updated_on属性设置为当前时间)。如果你指定一个符号,该属性将与当前时间一起更新,此外还有updated_at/updated_on属性。请注意,在触碰时不会执行任何验证,只有after_touch、after_commit和after_rollback回调将被执行。 :inverse_of-
指定关联对象上用于表示此
has_one关联的逆向的belongs_to关联的名称。有关更多详细信息,请参阅 双向关联。 :required-
当设置为
true时,还将验证关联的存在性。这将验证关联本身,而不是 id。您可以使用:inverse_of来避免在验证期间进行额外的查询。 :strict_loading-
每次通过此关联加载关联记录时,都会强制执行严格加载。
:ensuring_owner_was-
指定一个将在所有者上调用的实例方法。该方法必须返回 true,才能在后台作业中删除关联记录。
:query_constraints-
用作复合外键。定义用于查询关联对象的列列表。这是一个可选选项。默认情况下,
Rails会尝试自动推导值。当设置值时,Array的大小必须与关联模型的主键或query_constraints的大小匹配。 :deprecated-
如果为 true,则将关联标记为已弃用。使用已弃用的关联会发出报告。请查阅上面的类文档以获取详细信息。
选项示例
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 desc') }, 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 1628 def has_one(name, scope = nil, **options) reflection = Builder::HasOne.build(self, name, scope, options) Reflection.add_reflection(self, name, reflection) end