跳至内容 跳至搜索

活动记录关联

关联是一组类宏方法,用于通过外键将对象联系在一起。它们表示诸如“项目有一个项目经理”或“项目属于一个投资组合”之类的关系。每个宏都会向类添加一些方法,这些方法根据集合或关联符号和选项哈希进行专门化。它的工作方式与 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 继承的方法,并破坏某些内容。例如,attributesconnection 将是关联名称的不良选择,因为这些名称已存在于 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_onebelongs_tohas_manyhas_and_belongs_to_many 关联中设置 :autosave 选项。将其设置为 true始终保存成员,而将其设置为 false从不保存成员。有关 :autosave 选项的更多详细信息,请访问 AutosaveAssociation

一对一关联

  • 将对象分配给 has_one 关联将自动保存该对象和被替换的对象(如果存在),以便更新其外键 - 除非父对象未保存(new_record? == true)。

  • 如果其中任何一个保存失败(由于其中一个对象无效),则会引发 ActiveRecord::RecordNotSaved 异常,并且取消分配。

  • 如果您希望将对象分配给 has_one 关联而不保存它,请使用 #build_association 方法(如下所述)。将仍然保存被替换的对象以更新其外键。

  • 将对象分配给 belongs_to 关联不会保存对象,因为外键字段属于父级。它也不会保存父级。

集合

  • 将对象添加到集合(has_manyhas_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(record)
    # ...
  end

  def log_after_remove(record)
    # ...
  end
end

可以通过将回调作为数组传递来堆叠回调。示例

class Firm < ActiveRecord::Base
  has_many :clients,
           dependent: :destroy,
           after_add: [:congratulate_client, -> (firm, record) { firm.log << "after_adding#{record.id}" }],
           after_remove: :log_after_remove
end

可能的回调有:before_addafter_addbefore_removeafter_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_tohas_one 的关联对象,或返回 has_manyhas_and_belongs_to_many 的关联对象集合。

但是,在实际的扩展代码中,你无法像上面那样访问 record。在这种情况下,你可以访问 proxy_association。例如,record.association(:items)record.items.proxy_association 将返回同一个对象,让你可以在关联扩展中进行诸如 proxy_association.owner 的调用。

关联连接模型

可以使用 :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_onehas_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 选项,这意味着以下示例将正确工作(其中 tagshas_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_manyhas_onebelongs_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,则将无法知道如何设置中间PostComment对象。

多态关联

模型上的多态关联不受可以关联的模型类型的限制。相反,它们指定了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=方法。attachableclass_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|

这引用了 belongs_to 关联的名称,该关联也使用了 :author 符号。在加载帖子后,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|

以上代码将加载所有评论及其所有关联的作者和头像。您可以混合和匹配符号、数组和哈希的任意组合,以检索您要加载的关联。

所有这些功能都不应让您误以为您可以提取大量数据而不会造成性能损失,仅仅因为您减少了查询数量。数据库仍然需要将所有数据发送到 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.idLEFT 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 实例 dt.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_manyhas_onebelongs_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 选项之前或之后声明的其他回调可能会影响其执行操作。

请注意,对于 has_one :through 关联,:dependent 选项将被忽略。

删除还是销毁?

has_manyhas_and_belongs_to_many 关联具有 destroydeletedestroy_alldelete_all 方法。

对于 has_and_belongs_to_manydeletedestroy 是相同的:它们会导致连接表中的记录被删除。

对于 has_manydestroydestroy_all 始终会调用要删除的记录的 destroy 方法,以便运行回调。但是,deletedelete_all 将根据 :dependent 选项指定策略执行删除,或者如果没有给出 :dependent 选项,则它将遵循默认策略。默认策略是不执行任何操作(保留具有已设置父 ID 的外键),但对于 has_many :through 除外,其默认策略是 delete_all(删除连接记录,而不运行其回调)。

还有一个 clear 方法,它与 delete_all 相同,只是它返回关联而不是已删除的记录。

删除什么?

这里有一个潜在的陷阱:has_and_belongs_to_manyhas_many :through 关联在连接表中具有记录,以及关联的记录。因此,当我们调用其中一个删除方法时,究竟应该删除什么?

答案是,假设关联上的删除是关于删除所有者和关联对象之间的链接,而不是必须删除关联对象本身。因此,对于 has_and_belongs_to_manyhas_many :through,连接记录将被删除,但关联记录不会被删除。

如果你仔细想想,这很有道理:如果你要调用 post.tags.delete(Tag.find_by(name: 'food')),你希望“food”标签与文章解除关联,而不是从数据库中删除标签本身。

但是,有一些示例表明此策略没有道理。例如,假设一个人有很多项目,每个项目都有很多任务。如果我们删除了此人的一项任务,我们可能不希望删除该项目。在此场景中,delete 方法实际上不起作用:它只能在关联模型上的关联是 belongs_to 时使用。在其他情况下,你应该直接对关联记录或 :through 关联执行操作。

使用常规 has_many 时,“关联记录”和“链接”之间没有区别,因此对于要删除的内容只有一个选择。

使用 has_and_belongs_to_manyhas_many :through 时,如果你想删除关联记录本身,你始终可以执行类似于 person.tasks.each(&:destroy) 的操作。

使用 ActiveRecord::AssociationTypeMismatch 进行类型安全检查

如果你尝试将对象分配给与推断或指定的 :class_name 不匹配的关联,你将收到 ActiveRecord::AssociationTypeMismatch

选项

所有关联宏都可以通过选项进行专门化。这使得比简单且可猜测的用例更复杂的情况成为可能。

方法
B
H

实例公共方法

belongs_to(name, scope = nil, **options)

指定与另一个类的单对单关联。仅当此类包含外键时,才应使用此方法。如果另一个类包含外键,则应改用 has_one。另请参阅 ActiveRecord::Associations::ClassMethods 的概述,了解何时使用 has_one,何时使用 belongs_to

将添加方法,用于检索和查询此对象持有 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 作为可调用对象(即过程或 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_async,则计划在后台作业中销毁关联对象。不应在 belongs_to 与另一个类上的 has_many 关系结合使用时指定此选项,因为可能会留下孤立的记录。

:counter_cache

通过使用 CounterCache::ClassMethods#increment_counterCounterCache::ClassMethods#decrement_counter 在关联类上缓存属于对象的数目。在创建此类的对象时增加计数器缓存,在销毁它时减少计数器缓存。这需要在关联类(例如 Post 类)上使用名为 #{table_name}_count 的列(例如,对于属于 Comment 类的 comments_count) - 这是在关联类上创建 #{table_name}_count 的迁移(这样 Post.comments_count 将返回缓存的计数,请参见下面的注释)。您还可以通过向此选项提供列名称(例如 counter_cache: :my_custom_counter)而不是 true/false 值来指定自定义计数器缓存列。注意:指定计数器缓存会使用 attr_readonly 将其添加到该模型的只读属性列表中。

:polymorphic

通过传递 true 指定此关联是多态关联。注意:如果您启用了计数器缓存,则可能希望将计数器缓存属性添加到关联类中的 attr_readonly 列表中(例如 class Post; attr_readonly :comments_count; end)。

: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_touchafter_commitafter_rollback 回调。

:inverse_of

指定关联对象上 has_onehas_many 关联的名称,该关联是此 belongs_to 关联的反向关联。有关更多详细信息,请参阅 ActiveRecord::Associations::ClassMethods 中关于双向关联的概述。

:optional

如果设置为 true,则不会验证关联的存在性。

:required

如果设置为 true,则还会验证关联的存在性。这将验证关联本身,而不是 ID。可以使用 :inverse_of 来避免在验证期间进行额外的查询。注意:required 默认设置为 true,并且已弃用。如果不希望验证关联存在性,请使用 optional: true

:default

提供一个可调用对象(即过程或 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
belong_to  :note, query_constraints: [:organization_id, :note_id]
# File activerecord/lib/active_record/associations.rb, line 1886
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)

指定与另一个类之间的多对多关系。这通过中间联接表关联两个类。除非联接表明确指定为选项,否则会使用类名的词法顺序猜测联接表。因此,Developer 和 Project 之间的联接将给出默认联接表名称“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[7.1]
  def change
    create_join_table :developers, :projects
  end
end

为每个这些列添加索引以加快联接过程也是一个好主意。但是,在 MySQL 中,建议为这两个列添加复合索引,因为 MySQL 在查找期间只使用每个表的一个索引。

添加以下方法用于检索和查询

collection 是作为 name 参数传递的符号的占位符,因此 has_and_belongs_to_many :categories 将添加 categories.empty? 等内容。

collection

返回所有关联对象的 Relation。如果找不到任何对象,则返回一个空 Relation

collection<<(object, ...)

通过在联接表中创建关联,向集合中添加一个或多个对象(collection.pushcollection.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

返回所有关联对象的一个 Relation,强制进行数据库读取。如果没有找到任何对象,则返回一个空的 Relation

示例

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 作为可调用对象(即过程或 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

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

选项示例

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 2067
        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.to_s].parent_reflection = habtm_reflection
        end

has_many(name, scope = nil, **options, &extension)

指定一对多关联。将添加以下方法用于检索和查询关联对象集合

collection 是作为 name 参数传递的符号的占位符,因此 has_many :clients 会添加 clients.empty? 等内容。

collection

返回所有关联对象的 Relation。如果找不到任何对象,则返回一个空 Relation

collection<<(object, ...)

通过将一个或多个对象的外部键设置为该集合的主键来将它们添加到该集合。请注意,此操作会立即触发更新 SQL,而无需等待父对象上的保存或更新调用,除非父对象是新记录。这还将运行关联对象验证和回调。

collection.delete(object, ...)

通过将一个或多个对象的外部键设置为 NULL 来将它们从集合中移除。如果对象与 dependent: :destroy 关联,则它们将被另外销毁;如果它们与 dependent: :delete_all 关联,则它们将被删除。

如果使用了 :through 选项,则默认情况下会删除连接记录(而不是将其置空),但你可以指定 dependent: :destroydependent: :nullify 来覆盖此设置。

collection.destroy(object, ...)

通过对每条记录运行 destroy 来从集合中移除一个或多个对象,无论任何依赖选项如何,确保运行回调。

如果使用了 :through 选项,则会销毁连接记录,而不是对象本身。

collection=objects

通过根据需要删除和添加对象来替换集合内容。如果 :through 选项为 true,则连接模型中的回调将被触发,但销毁回调除外,因为默认情况下直接进行删除。你可以指定 dependent: :destroydependent: :nullify 来覆盖此设置。

collection_singular_ids

返回关联对象的 id 数组

collection_singular_ids=ids

使用 ids 中的主键替换集合。此方法加载模型并调用 collection=。请参见上文。

collection.clear

从集合中移除每个对象。如果关联对象与 dependent: :destroy 关联,则销毁它们;如果与 dependent: :delete_all 直接关联,则直接从数据库中删除它们;否则,将它们的外部键设置为 NULL。如果 :through 选项为 true,则不会在连接模型上调用任何销毁回调。连接模型将被直接删除。

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

返回所有关联对象的一个 Relation,强制进行数据库读取。如果没有找到任何对象,则返回一个空的 Relation

示例

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 作为可调用对象(即过程或 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。多态类型也会在多态关联中被置空。不会执行 回调

  • :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 关联的反向关联。有关更多详细信息,请参阅 ActiveRecord::Associations::ClassMethods 中关于双向关联的概述。

:extend

指定将扩展到返回的关联对象中的模块或模块数组。对于定义关联上的方法非常有用,尤其是在它们应该在多个关联对象之间共享时。

:strict_loading

设置为 true 时,每次通过此关联加载关联记录时,都会强制严格加载。

:ensuring_owner_was

指定要在所有者上调用的实例方法。该方法必须返回 true,才能在后台作业中删除关联记录。

:query_constraints

用作复合外键。定义用于查询关联对象的列列表。这是一个可选选项。默认情况下,Rails 将尝试自动派生该值。设置该值时,Array 大小必须与关联模型的主键或 query_constraints 大小匹配。

选项示例

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]
# File activerecord/lib/active_record/associations.rb, line 1522
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。另请参阅 ActiveRecord::Associations::ClassMethods 的概述,了解何时使用 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 作为可调用对象(即过程或 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

指定通过其执行查询的连接模型。:class_name:primary_key:foreign_key 的选项将被忽略,因为关联使用源反射。你只能通过连接模型上的 has_onebelongs_to 关联使用 :through 查询。

如果关联模型上的关联是 belongs_to,则可以修改集合,并且 :through 模型上的记录将根据需要自动创建和删除。否则,集合是只读的,因此你应该直接操作 :through 关联。

如果你要修改关联(而不是仅仅从中读取),那么最好在关联模型上的源关联中设置 :inverse_of 选项。这允许构建关联记录,这些记录在保存时将自动创建适当的关联模型记录。(请参阅上面的“关联模型”和“设置反向”部分。)

: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,则永远不保存或销毁关联对象。默认情况下,仅当关联对象为新记录时才保存它。

请注意,NestedAttributes::ClassMethods#accepts_nested_attributes_for:autosave 设置为 true

:touch

如果为 true,则在保存或销毁此记录时,将触及关联对象(将 updated_at / updated_on 属性设置为当前时间)。如果指定一个符号,则除了 updated_at / updated_on 属性外,还将使用当前时间更新该属性。请注意,触及时不会执行任何验证,并且只会执行 after_touchafter_commitafter_rollback 回调。

:inverse_of

指定关联对象上 belongs_to 关联的名称,该关联是此 has_one 关联的反向关联。有关更多详细信息,请参阅 ActiveRecord::Associations::ClassMethods 中关于双向关联的概述。

: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]
# File activerecord/lib/active_record/associations.rb, line 1708
def has_one(name, scope = nil, **options)
  reflection = Builder::HasOne.build(self, name, scope, options)
  Reflection.add_reflection self, name, reflection
end