跳至内容 跳至搜索

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

多对多

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

第一种方法使用带有 :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(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 等。

关联连接模型

可以使用 :through 选项配置 Has Many 关联,以使用显式的连接模型来检索数据。这与 has_and_belongs_to_many 关联类似。优点是可以为连接模型添加验证、回调和额外属性。考虑以下模式

class Author < ActiveRecord::Base
  has_many :authorships
  has_many :books, through: :authorships
end

class Authorship < ActiveRecord::Base
  belongs_to :author
  belongs_to :book
end

@author = Author.first
@author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to
@author.books                              # selects all books by using the Authorship join model

你也可以通过连接模型上的 has_many 关联进行操作

class Firm < ActiveRecord::Base
  has_many   :clients
  has_many   :invoices, through: :clients
end

class Client < ActiveRecord::Base
  belongs_to :firm
  has_many   :invoices
end

class Invoice < ActiveRecord::Base
  belongs_to :client
end

@firm = Firm.first
@firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm
@firm.invoices                            # selects all invoices by going through the Client join model

同样,你也可以通过连接模型上的 has_one 关联进行操作

class Group < ActiveRecord::Base
  has_many   :users
  has_many   :avatars, through: :users
end

class User < ActiveRecord::Base
  belongs_to :group
  has_one    :avatar
end

class Avatar < ActiveRecord::Base
  belongs_to :user
end

@group = Group.first
@group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group
@group.avatars                                # selects all avatars by going through the User join model.

通过连接模型上的 has_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

最后一行应该保存 through 记录(一个 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 模型的基础模型存储在多态关联的 type 列中。继续上面的 asset 示例,假设有 guest posts 和 member posts 使用 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|

上面的代码将加载所有评论及其所有关联的作者和 gravatars。你可以混合搭配任何符号、数组和哈希的组合来检索要加载的关联。

所有这些功能都不应让你误以为你可以以零性能成本提取大量数据,仅仅因为你减少了查询次数。数据库仍然需要将所有数据发送到 Active Record,并且仍然需要对其进行处理。所以它并不是解决性能问题的万能药,但它是在上述情况下减少查询次数的好方法。

由于一次只加载一个表,条件或排序不能引用除主表以外的表。在这种情况下,Active Record 会回退到先前使用的 LEFT OUTER JOIN 策略。例如

Post.includes([:author, :comments]).where(['comments.approved = ?', true])

这将生成一个带有连接的 SQL 查询,类似于:LEFT OUTER JOIN comments ON comments.post_id = posts.idLEFT OUTER JOIN authors ON authors.id = posts.author_id。请注意,使用此类条件可能会产生意外后果。在上面的示例中,没有已批准评论的帖子根本不会被返回,因为条件适用于整个 SQL 语句,而不只是关联。因此,没有已批准评论的帖子将不会被返回,因为条件适用于整个 SQL 语句,而不只是关联。

为此回退发生,你必须区分列引用,例如 order: "author.name DESC" 将起作用,但 order: "name DESC" 将不起作用。

如果你想加载所有帖子(包括没有已批准评论的帖子),请使用 ON 编写自己的 LEFT OUTER JOIN 查询

Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'")

在这种情况下,通常更自然地包含具有定义的条件的关联

class Post < ActiveRecord::Base
  has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment'
end

Post.includes(:approved_comments)

这将加载帖子并预加载 approved_comments 关联,该关联仅包含已批准的评论。

如果你预加载一个带有指定 :limit 选项的关联,该选项将被忽略,返回所有关联对象。

class Picture < ActiveRecord::Base
  has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment'
end

Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments.

多态关联支持预加载。

class Address < ActiveRecord::Base
  belongs_to :addressable, polymorphic: true
end

尝试预加载 addressable 模型的调用

Address.includes(:addressable)

这将执行一次查询来加载地址,并为每个 addressable 类型执行一次查询来加载 addressables。例如,如果所有 addressables 都是 Person 或 Company 类,则总共将执行 3 次查询。要加载的 addressable 类型列表是根据加载的地址确定的。如果 Active Record 需要回退到之前的预加载实现,则不支持此功能,并将引发 ActiveRecord::EagerLoadPolymorphicError。原因是父模型的类型是列值,因此其对应的表名无法放入该查询的 FROM/JOIN 子句中。

表别名

当表在连接中被多次引用时,Active Record 会使用表别名。如果一个表只被引用一次,则使用标准的表名。第二次,表被别名为 #{reflection_name}_#{parent_table_name}。索引会附加到后续对表名的任何使用上。

Post.joins(:comments)
# SELECT ... FROM posts INNER JOIN comments ON ...
Post.joins(:special_comments) # STI
# SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment'
Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name
# SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts

Acts as tree 示例

TreeMixin.joins(:children)
# SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
TreeMixin.joins(children: :parent)
# SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
#                        INNER JOIN parents_mixins ...
TreeMixin.joins(children: {parent: :children})
# SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
#                        INNER JOIN parents_mixins ...
#                        INNER JOIN mixins childrens_mixins_2

Has and Belongs to Many 连接表使用相同的想法,但添加了 _join 后缀。

Post.joins(:categories)
# SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
Post.joins(categories: :posts)
# SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
#                       INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
Post.joins(categories: {posts: :categories})
# SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
#                       INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
#                       INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2

如果你想使用 ActiveRecord::QueryMethods#joins 方法指定自己的自定义连接,那么这些表名将优先于预加载的关联。

Post.joins(:comments).joins("inner join comments ...")
# SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ...
Post.joins(:comments, :special_comments).joins("inner join comments ...")
# SELECT ... FROM posts INNER JOIN comments comments_posts ON ...
#                       INNER JOIN comments special_comments_posts ...
#                       INNER JOIN comments ...

表别名会根据特定数据库的最大表标识符长度自动截断。

模块

默认情况下,关联会在当前模块范围内查找对象。考虑

module MyApplication
  module Business
    class Firm < ActiveRecord::Base
      has_many :clients
    end

    class Client < ActiveRecord::Base; end
  end
end

当调用 Firm#clients 时,它会接着调用 MyApplication::Business::Client.find_all_by_firm_id(firm.id)。如果你想关联到另一个模块范围内的类,可以通过指定完整的类名来完成。

module MyApplication
  module Business
    class Firm < ActiveRecord::Base; end
  end

  module Billing
    class Account < ActiveRecord::Base
      belongs_to :firm, class_name: "MyApplication::Business::Firm"
    end
  end
end

双向关联

当你指定一个关联时,通常在关联模型上有一个关联,它以相反的方式指定相同的关系。例如,使用以下模型

class Dungeon < ActiveRecord::Base
  has_many :traps
  has_one :evil_wizard
end

class Trap < ActiveRecord::Base
  belongs_to :dungeon
end

class EvilWizard < ActiveRecord::Base
  belongs_to :dungeon
end

Dungeon 上的 traps 关联和 Trap 上的 dungeon 关联是彼此的反向关联,而 EvilWizard 上的 dungeon 关联的反向关联是 Dungeon 上的 evil_wizard 关联(反之亦然)。默认情况下,Active Record 可以根据类名猜测关联的反向关联。结果如下

d = Dungeon.first
t = d.traps.first
d.object_id == t.dungeon.object_id # => true

上面示例中的 Dungeon 实例 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 选项之前或之后声明的其他回调可能会影响其行为。

注意,: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')),你会希望“food”标签从帖子中解除链接,而不是从数据库中删除标签本身。

然而,有一些例子表明这种策略并不适用。例如,假设一个人有很多项目,每个项目有很多任务。如果我们删除一个人的一项任务,我们可能不希望项目也被删除。在这种情况下,delete 方法实际上不起作用:只有当连接模型上的关联是 belongs_to 时才能使用它。在其他情况下,你会被期望直接在关联记录或 :through 关联上执行操作。

对于常规的 has_many,没有“关联记录”和“链接”之间的区别,所以只有一个选择会被删除。

对于 has_and_belongs_to_manyhas_many :through,如果你想删除关联记录本身,你总是可以这样做:person.tasks.each(&:destroy)

已弃用的 Associations

Associations 可以通过传递 deprecated: true 来标记为已弃用。

has_many :posts, deprecated: true

当使用已弃用的关联时,会通过 Active Record 日志记录器发出警告,尽管可以通过配置获得更多选项。

消息包含一些有助于理解已弃用用法的信息

The association Author#posts is deprecated, the method post_ids was invoked (...)
The association Author#posts is deprecated, referenced in query to preload records (...)

上面示例中的点将是应用程序级别的用法发生位置,以帮助定位触发警告的原因。该位置是使用 Active Record backtrace cleaner 计算的。

什么被认为是使用?

  • 调用任何关联方法,例如 postsposts= 等。

  • 如果关联接受嵌套属性,则为这些属性赋值。

  • 如果关联是 through 关联,并且其一些嵌套关联已弃用,则每当使用顶级 through 关联时,你都会收到关于它们的警告。这适用于 through 关联本身是否已弃用。

  • 执行引用关联的查询。例如执行 eager_load(:posts)joins(author: :posts) 等。

  • 如果关联具有 :dependent 选项,则销毁关联记录会发出警告(因为这会产生如果关联被移除就不会发生的影响)。

  • 如果关联具有 :touch 选项,则保存或销毁记录会发出警告(因为这会产生如果关联被移除就不会发生的影响)。

不发出警告的情况

大多数以下边缘情况的原理是 Active Record 惰性地访问关联,在使用时。在此之前,对关联的引用基本上只是一个 Ruby 符号。

  • 如果 posts 已弃用,has_many :comments, through: :posts 不会发出警告。comments 关联的使用会报告 posts 的使用,正如我们上面解释的,但 has_many 本身的定义不会。

  • 同样,accepts_nested_attributes_for :posts 本身不会发出警告。对 posts 属性的赋值会发出警告,如上所述,但 accepts_nested_attributes_for 调用本身不会。

  • 同样,如果一个关联声明为已弃用关联的反向关联,宏本身也不会发出警告。

  • 同样,声明 validates_associated :posts 本身不会发出警告,尽管在使用时会报告访问。

  • Relation 查询方法,如 Author.includes(:posts) 本身不会发出警告。此时,它是一个关系,内部存储一个符号供以后使用。如前一节所述,你会在查询执行时/如果查询执行时收到警告。

  • 访问关联的反射对象,例如 Author.reflect_on_association(:posts)Author.reflect_on_all_associations 不会发出警告。

配置

已弃用用法的报告可以配置

config.active_record.deprecated_associations_options = { ... }

如果存在,则必须是具有 :mode 和/或 :backtrace 键的哈希。

模式

  • :warn 模式下,使用会发出警告,其中包含访问发生的应用程序级别位置(如果有)。这是默认模式。

  • :raise 模式下,使用会引发一个带有类似消息的 ActiveRecord::DeprecatedAssociationError,并在异常对象中包含干净的回溯。

  • :notify 模式下,会发布一个 deprecated_association.active_record Active Support 通知。事件负载包含关联反射(:reflection)、访问发生的应用程序级别位置(:location)(一个 Thread::Backtrace::Location 对象,或 nil),以及一个弃用消息(:message)。

回溯

如果 :backtrace 为 true,警告会在消息中包含干净的回溯,并且通知会在负载中有一个 :backtrace 键,其中包含一个干净的 Thread::Backtrace::Location 对象数组。异常始终会设置干净的堆栈跟踪。

干净的回溯是通过 Active Record backtrace cleaner 计算的。在 Rails 应用程序中,这默认与 Rails.backtrace_cleaner 相同。

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

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

选项

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

方法
B
H

实例公共方法

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

指定与其他类的“一对一”关联。只有当当前类包含外键时,才应使用此方法。如果另一个类包含外键,则应改用 has_one。有关何时使用 has_one 和何时使用 belongs_to 的更多详细信息,请参阅 Is it a belongs_to or has_one association?

将添加用于检索和查询单个关联对象的方法,该对象保存了 id。

association 是作为 name 参数传递的符号的占位符,因此 belongs_to :author 将添加(除其他外)author.nil?

association

返回关联对象。如果找不到,则返回 nil

association=(associate)

分配关联对象,提取主键,并将其设置为外键。不进行修改或删除现有记录的操作。

build_association(attributes = {})

返回一个关联类型的新对象,该对象已使用 attributes 实例化,并通过外键与此对象关联,但尚未保存。

create_association(attributes = {})

返回关联类型的一个新对象,该对象已使用 attributes 实例化,并通过外键与此对象关联,并且已经保存(如果通过了验证)。

create_association!(attributes = {})

create_association 相同,但如果记录无效,则会引发 ActiveRecord::RecordInvalid

reload_association

返回关联对象,强制从数据库读取。

reset_association

卸载关联对象。下一次访问将从数据库中查询它。

association_changed?

如果已分配了新的关联对象,并且下一次保存将更新外键,则返回 true。

association_previously_changed?

如果上一次保存将关联更新为引用新的关联对象,则返回 true。

示例

class Post < ActiveRecord::Base
  belongs_to :author
end

声明 belongs_to :author 会添加以下方法(及更多)

post = Post.find(7)
author = Author.find(19)

post.author           # similar to Author.find(post.author_id)
post.author = author  # similar to post.author_id = author.id
post.build_author     # similar to post.author = Author.new
post.create_author    # similar to post.author = Author.new; post.author.save; post.author
post.create_author!   # similar to post.author = Author.new; post.author.save!; post.author
post.reload_author
post.reset_author
post.author_changed?
post.author_previously_changed?

作用域

你可以将第二个参数 scope 作为可调用对象(即 proc 或 lambda)传递,以检索特定记录或自定义访问关联对象时生成的查询。

作用域示例

belongs_to :firm, -> { where(id: 2) }
belongs_to :user, -> { joins(:friends) }
belongs_to :level, ->(game) { where("game_level > ?", game.current_level) }

选项

声明还可以包含一个 options 哈希来专门化关联的行为。

:class_name

指定关联的类名。仅当该名称无法从关联名称推断出来时才使用它。因此,belongs_to :author 默认将链接到 Author 类,但如果实际类名为 Person,则必须使用此选项指定它。:class_name 不支持多态关联,因为在这种情况下,关联记录的类名存储在 type 列中。

:foreign_key

指定用于关联的外键。默认情况下,它被推断为关联名称后跟“_id”后缀。因此,定义 belongs_to :person 关联的类将使用“person_id”作为默认 :foreign_key。类似地,belongs_to :favorite_person, class_name: "Person" 将使用“favorite_person_id”作为外键。

设置 :foreign_key 选项会阻止自动检测关联的反向关联,因此通常最好也设置 :inverse_of 选项。

:foreign_type

指定用于存储关联对象的类型的列,如果这是一个多态关联。默认情况下,它被推断为关联名称后跟“_type”后缀。因此,定义 belongs_to :taggable, polymorphic: true 关联的类将使用“taggable_type”作为默认 :foreign_type

:primary_key

指定用于关联的关联对象的主键的方法。默认是 id

:dependent

如果设置为 :destroy,则在当前对象被销毁时,关联对象也会被销毁。如果设置为 :delete,则关联对象会被删除,但不会调用其 destroy 方法。如果设置为 :destroy_async,关联对象将被安排在后台作业中销毁。当 belongs_to 与另一个类的 has_many 关系结合使用时,不应指定此选项,因为可能留下孤立的记录。

:counter_cache

通过使用 CounterCache::ClassMethods#increment_counterCounterCache::ClassMethods#decrement_counter 来缓存关联对象数量在关联类上。当此类的对象被创建时,计数器缓存会增加,当它被销毁时,计数器缓存会减少。这要求在关联类上使用一个名为 #{table_name}_count 的列(例如,对于属于 Comment 类的 Comment 类,为 comments_count)——即在关联类(例如 Post 类)上创建 #{table_name}_count 的迁移(这样 Post.comments_count 将返回缓存的计数)。你也可以通过提供一个列名而不是 true/false 值来指定一个自定义计数器缓存列(例如,counter_cache: :my_custom_counter)。

在现有大表上开始使用计数器缓存可能会很麻烦,因为列值必须与列添加分开回填(以免锁定表太长时间),并且在 :counter_cache 使用之前(否则 size/any?/等方法,它们在内部使用计数器缓存,可能会产生不正确的结果)。为了在保持计数器缓存列与子记录的创建/移除保持同步的同时安全地回填值,并避免上述方法产生可能不正确的计数器缓存列值,请始终从数据库获取结果,使用 counter_cache: { active: false }。如果你还需要指定一个自定义列名,请使用 counter_cache: { active: false, column: :my_custom_counter }

注意:如果你启用了计数器缓存,那么你可能希望将计数器缓存属性添加到关联类中的 attr_readonly 列表(例如,class Post; attr_readonly :comments_count; end)。

:polymorphic

通过传递 true 指定此关联是一个多态关联。注意:由于多态关联依赖于在数据库中存储类名,请确保更新相应行的 *_type 多态类型列中的类名。

:validate

当设置为 true 时,在保存父对象时会验证添加到关联的新对象。默认值为 false。如果你想确保关联对象在每次更新时都被重新验证,请使用 validates_associated

:autosave

如果为 true,则在保存父对象时,始终保存关联对象或销毁标记为要销毁的对象。如果为 false,则永远不保存或销毁关联对象。默认情况下,仅在对象是新记录时才保存关联对象。

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

:touch

如果为 true,当此记录被保存或销毁时,关联对象将被“触碰”(updated_at / updated_on 属性设置为当前时间)。如果你指定一个符号,该属性将与当前时间一起更新,此外还有 updated_at / updated_on 属性。请注意,在触碰时不会执行任何验证,只有 after_touchafter_commitafter_rollback 回调将被执行。

:inverse_of

指定关联对象上作为此 belongs_to 关联反向的 has_onehas_many 关联的名称。有关更多详细信息,请参阅 Bi-directional associations

:optional

当设置为 true 时,将不会验证关联的存在性。

:required

当设置为 true 时,也将验证关联的存在性。这将验证关联本身,而不是 id。你可以使用 :inverse_of 来避免验证期间的额外查询。注意:required 默认设置为 true 且已弃用。如果你不希望验证关联是否存在,请使用 optional: true

:default

提供一个可调用对象(即 proc 或 lambda)来指定在验证之前应该使用特定记录初始化关联。请注意,如果记录存在,则不会执行可调用对象。

:strict_loading

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

:ensuring_owner_was

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

:query_constraints

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

:deprecated

如果为 true,则将关联标记为已弃用。使用已弃用的关联会发出报告。请查阅上面的类文档以获取详细信息。

选项示例

belongs_to :firm, foreign_key: "client_of"
belongs_to :person, primary_key: "name", foreign_key: "person_name"
belongs_to :author, class_name: "Person", foreign_key: "author_id"
belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
                          class_name: "Coupon", foreign_key: "coupon_id"
belongs_to :attachable, polymorphic: true
belongs_to :project, -> { readonly }
belongs_to :post, counter_cache: true
belongs_to :comment, touch: true
belongs_to :company, touch: :employees_last_updated_at
belongs_to :user, optional: true
belongs_to :account, default: -> { company.account }
belongs_to :account, strict_loading: true
belongs_to :note, query_constraints: [:organization_id, :note_id]
# File activerecord/lib/active_record/associations.rb, line 1824
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”在字母顺序上 precedes “P”。请注意,此优先级是使用 < 运算符为 String 计算的。这意味着如果字符串长度不同,并且在比较到最短长度时字符串相等,那么较长的字符串被认为比较短的字符串具有更高的词汇优先级。例如,你会期望表“paper_boxes”和“papers”生成连接表名“papers_paper_boxes”,但它实际上生成连接表名“paper_boxes_papers”。注意这个警告,如果你需要的话,请使用自定义的 :join_table 选项。如果你的表共享一个公共前缀,它将只在开头出现一次。例如,表“catalog_categories”和“catalog_products”生成连接表名“catalog_categories_products”。

连接表不应有主键或关联的模型。你必须手动生成连接表,例如使用迁移

class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[8.1]
  def change
    create_join_table :developers, :projects
  end
end

为每个列添加索引以加快连接过程也是个好主意。然而,在 MySQL 中,建议为两个列都添加复合索引,因为 MySQL 在查找过程中每个表只使用一个索引。

为检索和查询添加以下方法

collection 是作为 name 参数传递的符号的占位符,因此 has_and_belongs_to_many :categories 将添加(除其他外)categories.empty?

collection

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

collection<<(object, ...)

通过在连接表中创建关联来添加一个或多个对象(collection.pushcollection.concat 是此方法的别名)。请注意,此操作会立即触发更新 SQL,而不会等待父对象的保存或更新调用,除非父对象是新记录。

collection.delete(object, ...)

通过从连接表中删除其关联来从集合中移除一个或多个对象。这不会销毁对象。

collection.destroy(object, ...)

通过在连接表中的每个关联上运行 destroy 来移除一个或多个对象,覆盖任何 dependent 选项。这不会销毁对象。

collection=objects

通过删除和添加对象来替换集合的内容。如果父对象是新记录,则不会立即执行此操作。

collection_singular_ids

返回关联对象的 id 数组。

collection_singular_ids=ids

通过 ids 中标识的主键替换集合中的对象。

collection.clear

从集合中移除每个对象。这不会销毁对象。

collection.empty?

如果没有关联对象,则返回 true

collection.size

返回关联对象的数量。

collection.find(id)

查找响应 id 并且满足它必须与此对象关联的条件。使用与 ActiveRecord::FinderMethods#find 相同的规则。

collection.exists?(...)

检查具有给定条件的关联对象是否存在。使用与 ActiveRecord::FinderMethods#exists? 相同的规则。

collection.build(attributes = {})

返回集合类型的一个新对象,该对象已使用 attributes 实例化,并通过连接表与此对象关联,但尚未保存。

collection.create(attributes = {})

返回集合类型的一个新对象,该对象已使用 attributes 实例化,并通过连接表与此对象关联,并且已经保存(如果通过了验证)。

collection.reload

返回所有关联对象的 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

如果为 true,则在保存父对象时,始终保存关联对象或销毁标记为要销毁的对象。如果为 false,则永远不保存或销毁关联对象。默认情况下,仅保存新记录的关联对象。

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

:strict_loading

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

:deprecated

如果为 true,则将关联标记为已弃用。使用已弃用的关联会发出报告。请查阅上面的类文档以获取详细信息。

选项示例

has_and_belongs_to_many :projects
has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
has_and_belongs_to_many :nations, class_name: "Country"
has_and_belongs_to_many :categories, join_table: "prods_cats"
has_and_belongs_to_many :categories, -> { readonly }
has_and_belongs_to_many :categories, strict_loading: true
# File activerecord/lib/active_record/associations.rb, line 2008
        def has_and_belongs_to_many(name, scope = nil, **options, &extension)
          habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)

          builder = Builder::HasAndBelongsToMany.new(name, self, options)

          join_model = builder.through_model

          const_set(join_model.name, join_model)
          private_constant(join_model.name)

          middle_reflection = builder.middle_reflection(join_model)

          Builder::HasMany.define_callbacks(self, middle_reflection)
          Reflection.add_reflection(self, middle_reflection.name, middle_reflection)
          middle_reflection.parent_reflection = habtm_reflection

          include Module.new {
            class_eval <<-RUBY, __FILE__, __LINE__ + 1
              def destroy_associations
                association(:#{middle_reflection.name}).delete_all(:delete_all)
                association(:#{name}).reset
                super
              end
            RUBY
          }

          hm_options = {}
          hm_options[:through] = middle_reflection.name
          hm_options[:source] = join_model.right_reflection.name

          [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend, :strict_loading, :deprecated].each do |k|
            hm_options[k] = options[k] if options.key?(k)
          end

          has_many name, scope, **hm_options, &extension
          _reflections[name].parent_reflection = habtm_reflection
        end

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

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

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 来移除一个或多个对象,而不考虑任何 dependent 选项,确保回调被运行。

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

collection=objects

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

collection_singular_ids

返回关联对象的 id 数组

collection_singular_ids=ids

通过 ids 中标识的主键替换集合。此方法加载模型并调用 collection=。请参阅上面。

collection.clear

移除集合中的所有对象。这会销毁关联对象(如果它们与 dependent: :destroy 关联),直接从数据库删除它们(如果 dependent: :delete_all),否则将其外键设置为 NULL。如果使用了 :through 选项,则连接模型上不会调用 destroy 回调。连接模型将被直接删除。

collection.empty?

如果没有关联对象,则返回 true

collection.size

返回关联对象的数量。

collection.find(...)

根据与 ActiveRecord::FinderMethods#find 相同的规则查找关联对象。

collection.exists?(...)

检查具有给定条件的关联对象是否存在。使用与 ActiveRecord::FinderMethods#exists? 相同的规则。

collection.build(attributes = {}, ...)

返回集合类型的一个或多个新对象,这些对象已使用 attributes 实例化,并通过外键与此对象关联,但尚未保存。

collection.create(attributes = {})

返回集合类型的一个新对象,该对象已使用 attributes 实例化,通过外键与此对象关联,并且已经保存(如果通过了验证)。注意:这仅在基础模型已存在于数据库中时才有效,而不是在新(未保存)记录时!

collection.create!(attributes = {})

collection.create 相同,但如果记录无效,则会引发 ActiveRecord::RecordInvalid

collection.reload

返回所有关联对象的 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”后缀。因此,定义 has_many 关联的 Person 类将使用“person_id”作为默认的 :foreign_key

设置 :foreign_key 选项会阻止自动检测关联的反向关联,因此通常最好也设置 :inverse_of 选项。

:foreign_type

指定用于存储关联对象的类型的列,如果这是一个多态关联。默认情况下,它被推断为“as”选项指定的命名多态关联后跟“_type”后缀。因此,定义 has_many :tags, as: :taggable 关联的类将使用“taggable_type”作为默认的 :foreign_type

:primary_key

指定用于关联的主键的列名。默认是 id

:dependent

控制在销毁所有者时对关联对象执行的操作。注意,这些作为回调实现,Rails 按顺序执行回调。因此,其他声明的回调可能会影响 :dependent 的行为,而 :dependent 的行为也可能影响其他回调。

  • nil 不执行任何操作(默认)。

  • :destroy 导致所有关联对象也被销毁。

  • :destroy_async 在后台作业中销毁所有关联对象。警告: 如果关联由数据库中的外键约束支持,请勿使用此选项。外键约束操作将在删除其所有者的同一事务中执行。

  • :delete_all 导致所有关联对象直接从数据库中删除(因此不会执行回调)。

  • :nullify 导致将外键设置为 NULL。多态类型在多态关联上也会被置空。不会执行 Callbacks

  • :restrict_with_exception 如果存在任何关联记录,则会引发 ActiveRecord::DeleteRestrictionError 异常。

  • :restrict_with_error 如果存在任何关联对象,则会向所有者添加错误。

如果与 :through 选项一起使用,则连接模型上的关联必须是 belongs_to,并且被删除的记录是连接记录,而不是关联记录。

如果在作用域关联上使用 dependent: :destroy,则仅销毁作用域内的对象。例如,如果 Post 模型定义了 has_many :comments, -> { where published: true }, dependent: :destroy 并且在调用 post 的 destroy 时,只有已发布的评论会被销毁。这意味着数据库中任何未发布的评论仍然会包含一个指向已删除帖子的外键。

:counter_cache

此选项可用于配置自定义名称的 :counter_cache。只有在 belongs_to 关联上自定义了 :counter_cache 的名称时,才需要此选项。

:as

指定一个多态接口(参见 belongs_to)。

:through

指定一个通过该关联执行查询。

这可以是任何其他类型的关联,包括其他 :through 关联,但不能是多态关联。:class_name:primary_key:foreign_key 的选项将被忽略,因为关联使用源反射。

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

如果你打算修改关联(而不仅仅是从中读取),那么在连接模型上设置源关联的 :inverse_of 选项是一个好主意。这允许创建关联记录,这些记录在保存时将自动创建相应的连接模型记录。有关更多详细信息,请参阅 Association Join ModelsSetting Inverses

:disable_joins

指定是否应跳过关联的连接。如果设置为 true,将生成两个或更多查询。请注意,在某些情况下,如果应用了排序或限制,由于数据库限制,将在内存中进行。此选项仅适用于 has_many :through 关联,因为 has_many 本身不执行连接。

:source

指定 has_many :through 查询使用的源关联名称。只有当名称无法从关联推断出来时才使用它。has_many :subscribers, through: :subscriptions 将查找 Subscription 上的 :subscribers:subscriber,除非给出了 :source

:source_type

指定 has_many :through 查询使用的源关联类型,其中源关联是多态 belongs_to

:validate

当设置为 true 时,在保存父对象时会验证添加到关联的新对象。默认值为 true。如果你想确保关联对象在每次更新时都被重新验证,请使用 validates_associated

:autosave

如果为 true,则在保存父对象时,始终保存关联对象或销毁标记为要销毁的对象。如果为 false,则永远不保存或销毁关联对象。默认情况下,仅保存新记录的关联对象。此选项作为 before_save 回调实现。由于回调按定义的顺序运行,因此关联对象可能需要在任何用户定义的 before_save 回调中显式保存。

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

:inverse_of

指定关联对象上作为此 has_many 关联反向的 belongs_to 关联的名称。有关更多详细信息,请参阅 Bi-directional associations

:extend

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

:strict_loading

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

:ensuring_owner_was

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

:query_constraints

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

:index_errors

通过在错误属性名称中包含索引,从而区分关联记录的多个验证错误,例如 roles[2].level。当设置为 true 时,索引基于关联顺序,即数据库顺序,尚未持久化的新记录放在最后。当设置为 :nested_attributes_order 时,索引基于使用 accepts_nested_attributes_for 时嵌套属性 setter 收到的记录顺序。

:before_add

定义一个 关联回调,该回调在将对象添加到关联集合之前触发。

:after_add

定义一个 关联回调,该回调在将对象添加到关联集合之后触发。

:before_remove

定义一个 关联回调,该回调在关联集合中移除对象之前触发。

:after_remove

定义一个 关联回调,该回调在关联集合中移除对象之后触发。

:deprecated

如果为 true,则将关联标记为已弃用。使用已弃用的关联会发出报告。请查阅上面的类文档以获取详细信息。

选项示例

has_many :comments, -> { order("posted_on") }
has_many :comments, -> { includes(:author) }
has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
has_many :tracks, -> { order("position") }, dependent: :destroy
has_many :comments, dependent: :nullify
has_many :tags, as: :taggable
has_many :reports, -> { readonly }
has_many :subscribers, through: :subscriptions, source: :user
has_many :subscribers, through: :subscriptions, disable_joins: true
has_many :comments, strict_loading: true
has_many :comments, query_constraints: [:blog_id, :post_id]
has_many :comments, index_errors: :nested_attributes_order
# File activerecord/lib/active_record/associations.rb, line 1427
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 的更多详细信息,请参阅 Is it a belongs_to or has_one association?

将添加以下用于检索和查询单个关联对象的方法

association 是作为 name 参数传递的符号的占位符,因此 has_one :manager 将添加(除其他外)manager.nil?

association

返回关联对象。如果找不到,则返回 nil

association=(associate)

分配关联对象,提取主键,将其设置为外键,并保存关联对象。为了避免数据库不一致,当分配新对象时,会永久删除现有关联对象,即使新对象未保存到数据库。

build_association(attributes = {})

返回一个关联类型的新对象,该对象已使用 attributes 实例化,并通过外键与此对象关联,但尚未保存。

create_association(attributes = {})

返回关联类型的一个新对象,该对象已使用 attributes 实例化,并通过外键与此对象关联,并且已经保存(如果通过了验证)。

create_association!(attributes = {})

create_association 相同,但如果记录无效,则会引发 ActiveRecord::RecordInvalid

reload_association

返回关联对象,强制从数据库读取。

reset_association

卸载关联对象。下一次访问将从数据库中查询它。

示例

class Account < ActiveRecord::Base
  has_one :beneficiary
end

声明 has_one :beneficiary 会添加以下方法(及更多)

account = Account.find(5)
beneficiary = Beneficiary.find(8)

account.beneficiary               # similar to Beneficiary.find_by(account_id: 5)
account.beneficiary = beneficiary # similar to beneficiary.update(account_id: 5)
account.build_beneficiary         # similar to Beneficiary.new(account_id: 5)
account.create_beneficiary        # similar to Beneficiary.create(account_id: 5)
account.create_beneficiary!       # similar to Beneficiary.create!(account_id: 5)
account.reload_beneficiary
account.reset_beneficiary

作用域

你可以将第二个参数 scope 作为可调用对象(即 proc 或 lambda)传递,以检索特定记录或自定义访问关联对象时生成的查询。

作用域示例

has_one :author, -> { where(comment_id: 1) }
has_one :employer, -> { joins(:company) }
has_one :latest_post, ->(blog) { where("created_at > ?", blog.enabled_at) }

选项

声明还可以包含一个 options 哈希来专门化关联的行为。

选项有:

:class_name

指定关联的类名。只有当关联名称无法推断出类名时才使用它。所以 has_one :manager 默认会关联到 Manager 类,但如果实际类名是 Person,您就必须通过此选项指定它。

:dependent

控制当所有者被销毁时,关联对象会发生什么

  • nil 不执行任何操作(默认)。

  • :destroy 会导致关联对象也被销毁

  • :destroy_async 会在后台作业中销毁关联对象。警告: 如果您的数据库依赖外键约束支持此关联,请勿使用此选项。外键约束的操作将发生在与删除其所有者的同一事务中。

  • :delete 会直接从数据库中删除关联对象(因此回调不会执行)

  • :nullify 会将外键设置为 NULL。在多态关联中,多态类型列也会被置为 NULL。回调不会执行。

  • :restrict_with_exception 如果存在关联记录,则会引发 ActiveRecord::DeleteRestrictionError 异常

  • :restrict_with_error 如果存在关联对象,则会将错误添加到所有者上

请注意,使用 :through 选项时会忽略 :dependent 选项。

:foreign_key

指定用于此关联的外键。默认情况下,此名称被猜测为当前类的名称(小写)加上 "_id" 后缀。因此,一个创建一个 has_one 关联的 Person 类将使用 "person_id" 作为默认的 :foreign_key

设置 :foreign_key 选项会阻止自动检测关联的反向关联,因此通常最好也设置 :inverse_of 选项。

:foreign_type

指定用于存储关联对象类型的列,如果这是一个多态关联。默认情况下,此名称被猜测为在 "as" 选项中指定的多态关联名称加上 "_type" 后缀。因此,一个定义了 has_one :tag, as: :taggable 关联的类将使用 "taggable_type" 作为默认的 :foreign_type

:primary_key

指定用于关联的主键的返回方法。默认是 id

:as

指定一个多态接口(参见 belongs_to)。

:through

指定一个通过该关联执行查询。

through 关联必须是 has_onehas_one :through 或非多态 belongs_to。也就是说,一个非多态的单数关联。:class_name:primary_key:foreign_key 选项会被忽略,因为关联使用源反射。您只能通过连接模型上的 has_onebelongs_to 关联来使用 :through 查询。

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

如果你打算修改关联(而不仅仅是从中读取),那么在连接模型上设置源关联的 :inverse_of 选项是一个好主意。这允许创建关联记录,这些记录在保存时将自动创建相应的连接模型记录。有关更多详细信息,请参阅 Association Join ModelsSetting Inverses

:disable_joins

指定是否应跳过关联的连接。如果设置为 true,将生成两个或更多查询。请注意,在某些情况下,如果应用了 order 或 limit,由于数据库限制,将在内存中完成。此选项仅适用于 has_one :through 关联,因为单独的 has_one 不执行连接。

:source

指定 has_one :through 查询使用的源关联名称。仅当无法从关联推断出名称时才使用它。has_one :favorite, through: :favorites 将查找 Favorite 中的 :favorite,除非指定了 :source

:source_type

指定 has_one :through 查询使用的源关联类型,其中源关联是一个多态 belongs_to

:validate

当设置为 true 时,在保存父对象时会验证添加到关联的新对象。默认值为 false。如果你想确保关联对象在每次更新时都被重新验证,请使用 validates_associated

:autosave

如果为 true,则在保存父对象时,总是保存关联对象,或者在标记为销毁时将其销毁。如果为 false,则从不保存或销毁关联对象。

默认情况下,仅在关联对象是新记录时才保存它。将此选项设置为 true 也会启用对关联对象的验证,除非显式使用 validate: false 禁用。这是因为保存包含无效关联对象会导致失败,因此任何关联对象都将经过验证检查。

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

:touch

如果为 true,当此记录被保存或销毁时,关联对象将被“触碰”(updated_at / updated_on 属性设置为当前时间)。如果你指定一个符号,该属性将与当前时间一起更新,此外还有 updated_at / updated_on 属性。请注意,在触碰时不会执行任何验证,只有 after_touchafter_commitafter_rollback 回调将被执行。

:inverse_of

指定关联对象上用于表示此 has_one 关联的逆向的 belongs_to 关联的名称。有关更多详细信息,请参阅 双向关联

:required

当设置为 true 时,还将验证关联的存在性。这将验证关联本身,而不是 id。您可以使用 :inverse_of 来避免在验证期间进行额外的查询。

:strict_loading

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

:ensuring_owner_was

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

:query_constraints

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

:deprecated

如果为 true,则将关联标记为已弃用。使用已弃用的关联会发出报告。请查阅上面的类文档以获取详细信息。

选项示例

has_one :credit_card, dependent: :destroy  # destroys the associated credit card
has_one :credit_card, dependent: :nullify  # updates the associated records foreign
                                              # key value to NULL rather than destroying it
has_one :last_comment, -> { order('posted_on desc') }, class_name: "Comment"
has_one :project_manager, -> { where(role: 'project_manager') }, class_name: "Person"
has_one :attachment, as: :attachable
has_one :boss, -> { readonly }
has_one :club, through: :membership
has_one :club, through: :membership, disable_joins: true
has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
has_one :credit_card, required: true
has_one :credit_card, strict_loading: true
has_one :employment_record_book, query_constraints: [:organization_id, :employee_id]
# File activerecord/lib/active_record/associations.rb, line 1628
def has_one(name, scope = nil, **options)
  reflection = Builder::HasOne.build(self, name, scope, options)
  Reflection.add_reflection(self, name, reflection)
end