跳至内容 跳至搜索

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 继承的方法,并导致问题。例如,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

多对多

有两种方法可以构建多对多关系。

第一种方法使用 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 关联?

两者都表达了 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(client)
    # ...
  end

  def log_after_remove(client)
    # ...
  end
end

Callbacks 可以通过三种方式定义

  1. 一个符号,引用在具有关联集合的类上定义的方法。例如,after_add: :congratulate_client 调用 Firm#congratulate_client(client)

  2. 一个可调用对象,其签名接受具有关联集合的记录和正在添加或删除的记录。例如,after_add: ->(firm, client) { ... }

  3. 一个响应回调名称的对象。例如,传递 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_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 之类的调用。

关联连接模型

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_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 选项,这将确保以下示例正常工作(其中 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_manyhas_onebelongs_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,将无法知道如何设置中间的 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|

这引用了也使用 :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.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 Associations 指南

从关联中删除

依赖关联

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 选项之前或之后声明的其他回调会影响其执行方式。

请注意,:dependent 选项被忽略,因为 has_one :through 关联。

删除或销毁?

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')),您希望“食物”标签与帖子解除关联,而不是从数据库中删除标签本身。

但是,有些例子表明这种策略没有意义。例如,假设一个人拥有许多项目,每个项目都有许多任务。如果我们删除了一个人的任务,我们可能不希望项目被删除。在这种情况下,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。有关何时使用 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_counterCounterCache::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_touchafter_commitafter_rollback 回调。

:inverse_of

指定相关联对象上 has_onehas_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]
# 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

返回所有相关联对象的 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 作为可调用对象(例如 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
# 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

返回所有相关联对象的 Relation。如果没有找到,将返回一个空 Relation

collection<<(object, ...)

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

collection.delete(object, ...)

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

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

collection.destroy(object, ...)

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

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

collection=objects

通过根据需要删除和添加对象来替换集合内容。 如果:through选项为真,则除了销毁回调之外,连接模型中的回调都会被触发,因为默认情况下删除是直接的。 您可以指定dependent: :destroydependent: :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

返回所有相关联对象的 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 作为可调用对象(例如 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。 多态类型也会在多态关联上变为NULLCallbacks不会执行。

  • :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
# 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。多态类型列在多态关联中也会被设置为NULLCallbacks 不会执行。

  • :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_onebelongs_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_touchafter_commitafter_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]
# 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