Docs 菜单

关联

Mongoid 中的关联允许您在模型之间创建关系。 在本指南中,您可以学习;了解Mongoid 支持的不同类型的关联以及如何在您的应用程序中使用它们。

引用关联允许您在两个模型之间创建一种关系,其中一个模型引用另一个模型。 Mongoid 支持以下引用关联类型:

  • has_one

  • has_many

  • belongs_to

  • has_and_belongs_to_many

以下部分描述了如何使用每种关联类型。

您可以使用 has_one 宏来声明由一个类表示的文档也包含由单独的子类表示的文档。 以下示例创建了一个与 Studio 类具有 has_one关系的 Band 类:

class Band
include Mongoid::Document
has_one :studio
end

当您声明 has_one 关联时,子类还必须使用引用父类的 belongs_to 关联。 以下示例显示了前面的 Band 类中引用的 Studio 类:

class Studio
include Mongoid::Document
belongs_to :band
end

要学习;了解有关belongs_to 宏的详情,请参阅 属于部分。

您可以使用验证来确保子类存在于父类中,如以下示例所示:

class Band
include Mongoid::Document
has_one :studio
validates_presence_of :studio
end

要学习;了解有关 Mongoid 中验证的更多信息,请参阅 验证指南。

您可以使用 has_many 宏来声明一个类表示的文档包含由另一个类表示的多个子文档。 以下示例创建了一个与 Members 类具有 has_many关系的 Band 类:

class Band
include Mongoid::Document
has_many :members
end

当您声明 has_many 关联时,子类还必须使用引用父类的 belongs_to 关联。 以下示例显示了前面的 Band 类中引用的 Member 类:

class Member
include Mongoid::Document
belongs_to :band
end

要学习;了解有关belongs_to 宏的更多信息,请参阅“属于”部分。

您可以使用验证来确保子类存在于父类中,如以下示例所示:

class Band
include Mongoid::Document
has_many :members
validates_presence_of :members
end

要学习;了解有关 Mongoid 中验证的更多信息,请参阅 验证指南。

您可以对 has_many 关联使用 any? 方法,以确定该关联是否包含任何文档,而无需从数据库中检索整个文档设立。

以下示例使用 any? 方法来确定 Band 类中的文档是否包含任何 Members 文档:

band = Band.first
band.members.any?

您还可以使用带有过滤的 any? 方法来查找与指定条件匹配的文档,如以下示例所示:

band = Band.first
band.members.any? { |member| member.instrument == 'piano' }

您可以向 any? 方法提供类名,以按类名过滤结果。 这对于多态关联非常有用:

class Drummer < Member
end
band = Band.first
band.members.any?(Drummer)

注意

将关联类的数据加载到 Mongoid 后,对 any? 方法的后续调用不会查询数据库。 相反,Mongoid 使用已加载到内存中的数据。

您还可以调用 exists? 方法来确定关联中是否有任何持久化文档。 exists? 方法始终查询数据库并仅检查已保存到数据库的文档。 exists? 方法不允许过滤,也不接受任何参数。

以下示例使用 exists? 方法来确定 Band 类中是否存在任何持久的 Members 文档:

band = Band.create!
# Member is not persisted.
band.members.build
band.members.exists?
# Outputs: false
# Persist the member
band.members.map(&:save!)
band.members.exists?
# Outputs: true

使用 belongs_to 宏声明一个类表示的文档是另一个类表示的文档的子文档。 默认下,父类的 _id字段存储在子类中。 以下示例创建一个 Members 类,其中包含与 Band 类的 belongs_to 关联:

class Members
include Mongoid::Document
belongs_to :band
end

您可以通过将 optional 选项设置为 true,允许 Mongoid 将文档持久保存到数据库,而不存储关联父类的 _id,如以下示例所示:

class Members
include Mongoid::Document
belongs_to :band, optional: true
end

提示

通过在应用程序的配置设置中将 belongs_to_required_by_default 配置选项设置为 false,您可以全局更改 belongs_to 关联的默认行为,以便不需要其父类。

您可以在子类中指定 belongs_to 关联,而无需在父类中指定匹配的 has_onehas_many 关联。 这样做时,您无法从父类访问权限子文档的字段,但可以访问权限存储在子类中的父字段,例如父文档的 _id字段。 在以下示例中,Band 类无法访问权限Members 类,但 Members 类可以访问权限Band 类:

class Band
include Mongoid::Document
end
class Members
include Mongoid::Document
belongs_to :band
end

为清楚起见,您可以选择将 inverse_of 选项设立为 nil,以指示父类不包含与子类的 has_onehas_many 关联,如以下示例所示:

class Band
include Mongoid::Document
end
class Members
include Mongoid::Document
belongs_to :band, inverse_of: nil
end

使用 has_and_belongs_to_many 宏声明类模型包含与另一个类的多对多关系。 在多对多关系中,一个类中的每个文档都可以与另一个类中的多个文档相关联。 以下示例创建了一个与 Members 类具有 has_and_belongs_to_many关系的Band 类。 一个 Band文档可以引用多个 Members 文档,一个 Members文档可以引用多个 Band 文档。

class Band
include Mongoid::Document
has_and_belongs_to_many :members
end
class Members
include Mongoid::Document
has_and_belongs_to_many :bands
end

当您声明 has_and_belongs_to_many 关联时,两个模型实例存储关联文档的 _id 值的列表。 您可以将 inverse_of 选项设立为 nil,以将关联文档的 _id 值仅存储在一个模型实例中。 以下示例提示 Mongoid 将关联文档的 _id 值仅存储在 Band 类中:

class Band
include Mongoid::Document
has_and_belongs_to_many :tags, inverse_of: nil
end
class Tag
include Mongoid::Document
end

提示

当您更新具有 has_and_belongs_to_many 关联的文档时,Mongoid 会设置更新文档的 updated_at字段,但不会设立关联文档的 updated_at字段。

您可以使用聚合管道跨引用的关联查询文档。 聚合管道允许您跨多个集合创建查询并将数据处理为指定的格式。 要学习;了解有关使用聚合管道的更多信息,请参阅聚合指南。

对于简单的查询,可以直接查询关联。 直接查询集合时,只能查询集合本身中的字段和值。 您无法直接查询与您正在查询的集合关联的集合。

示例,考虑以下 BandTour 类:

class Band
include Mongoid::Document
has_many :tours
field :name, type: String
end
class Tour
include Mongoid::Document
belongs_to :band
field :year, type: Integer
end

以下示例查询 Tour 类中 year 值为 2000 或更大的文档,并保存这些文档的 band_id。 然后,它会在 Band 类中查询具有这些 band_id 值的文档。

band_ids = Tour.where(year: {'$gte' => 2000}).pluck(:band_id)
bands = Band.find(band_ids)

您可以使用嵌入式关联在同一集合中存储不同类型的文档。 Mongoid 支持与以下宏的嵌入式关联:

  • embeds_one

  • embeds_many

  • embedded_in

  • recursively_embeds_one

  • recursively_embeds_many

以下各节介绍如何使用这些关联类型。

要指定类模型包含不同类类型的嵌入式文档,请在父类中使用 embeds_one 宏,并在嵌入式类中使用 embedded_in 宏。 以下示例创建了一个带有嵌入式 Label 类的 Band 类:

class Band
include Mongoid::Document
embeds_one :label
end
class Label
include Mongoid::Document
field :name, type: String
embedded_in :band
end

Mongoid 将嵌入 embeds_one 宏的文档存储在父文档中,作为与嵌入式类同名的字段。 前面的 Label 文档存储在 Band文档中,如以下示例所示:

# Band document
{
"_id" : ObjectId("..."),
"label" : {
"_id" : ObjectId("..."),
"name" : "Periphery",
}
}

您可以使用 store_as 选项以其他名称存储嵌入式文档,如以下示例所示:

class Band
include Mongoid::Document
embeds_one :label, store_as: "record_label"
end

要指定类模型包含不同类类型的多个嵌入式文档,请在父类中使用 embeds_many 宏,并在嵌入式类中使用 embedded_in 宏。 以下示例创建了一个包含多个嵌入式 Album 类型文档的 Band 类:

class Band
include Mongoid::Document
embeds_many :albums
end
class Album
include Mongoid::Document
field :name, type: String
embedded_in :band
end

Mongoid 将嵌入 embeds_many 宏的文档存储在父文档中作为与嵌入式类同名的大量字段。 前面的 Album 文档存储在 Band文档中,如以下示例所示:

{
"_id" : ObjectId("..."),
"albums" : [
{
"_id" : ObjectId("..."),
"name" : "Omega",
}
]
}

您可以使用 store_as 选项以其他名称存储嵌入式文档,如以下示例所示:

class Band
include Mongoid::Document
embeds_many :albums, store_as: "records"
end

您可以使用 recursively_embeds_onerecursively_embeds_many 宏将一个或多个相同类型的文档嵌入到父类中。 这两个宏都通过 parent_* 方法和 child_* 方法为父文档和子文档提供访问器,其中 * 表示类的名称。 以下示例创建了一个 Band 类,该类以递归方式嵌入多个其他 Band 文档来表示多个乐队名称:

class Band
include Mongoid::Document
field :name, type: String
recursively_embeds_many
end

您可以通过 parent_bandchild_band 方法访问权限父文档和子文档,如以下示例所示:

root = Band.new(name: "Linkin Park")
# Add child bands
child_one = root.child_band.build(name: "Lincoln Park")
child_two = root.child_band.build(name: "Xero")
# Access parent band
child_one.parent_band
# Outputs: root

您可以在使用点表示法查询父类集合时访问权限嵌入式文档。

以下示例使用点表示法查询嵌入 Band 类中的 Tour 类型文档。 此查询返回 tours.year 值为 2000 或更大的文档:

Band.where('tours.year' => {'$gte' => 2000})

您可以使用 pluck投影方法来检索嵌入式文档,而无需检索其关联的父文档,如以下示例所示:

# Get awards for bands that have toured since 2000
Band.where('tours.year' => {'$gte' => 2000}).pluck(:awards)

您可以使用 Mongoid查询方法执行嵌入式匹配,这样您就可以查询已加载到应用程序中的文档的嵌入式关联。Mongoid 实现嵌入式匹配,无需向服务器发送查询。

嵌入式匹配支持以下查询符:

以下示例使用 $gte操作符查询加载的 Band文档的嵌入式 tours字段:

band = Band.where(name: 'Astral Projection').first
tours = band.tours.where(year: {'$gte' => 2000})

对已加载文档的嵌入式匹配具有以下已知限制:

  • 以下功能未实现嵌入式匹配:

  • Mongoid 将 Range 参数扩展为具有 $gte$lte 条件的哈希值。 在某些情况下,这可能会导致无效查询并引发 InvalidQuery 异常。

  • 使用 $regex操作符时,您无法在将正则表达式对象指定为模式的同时为 $options字段提供选项。 仅当正则表达式模式是字符串时才能提供选项。

  • MongoDB Server 4.0 及更早版本不严格验证 $type 参数。

默认下,Mongoid 会向嵌入式文档添加一个 _id字段。 您可以通过在模型中显式指定 _id字段并默认默认值,从嵌入式文档中省略此字段。 以下示例指示 Mongoid 不要将 _id字段添加到 Albums 类中:

class Album
include Mongoid::Document
field :name, type: String
field :_id, type: Object
embedded_in :band
end

在前面的 Albums 类中,不会自动添加 _id字段。 如果没有默认值,Mongoid 不会将该值存储在数据库中,除非您在模型中提供默认值。

您可以使用以下方法之一从 embeds_many 关联中删除子文档:

  • clear

  • delete_all

  • destroy_all

clear方法使用 $unset操作符操作符从父文档中删除整个嵌入式关联。clear 方法不会运行任何 destroy 回调。 以下示例使用 clear 方法从 Band 类中删除所有嵌入式关联:

band = Band.find(...)
band.tours.clear

delete_all方法使用 $pullAll操作符操作符删除嵌入式关联中的文档。delete_all 加载关联(如果尚未加载),然后只删除应用程序中存在的文档。 delete_all 方法不会运行任何 destroy 回调。 以下示例使用 delete_all 方法从 Band 类中删除所有嵌入的 Album 文档:

band = Band.find(...)
band.tours.delete_all

destroy_all方法还使用 $pullAll操作符操作符删除嵌入式关联中的文档。它还运行在关联文档上定义的任何 destroy 回调。 以下示例使用 destroy_all 方法从 Band 类中删除所有嵌入的 Album 文档:

band = Band.find(...)
band.tours.destroy_all

您可以使用 Mongoid 来自定义关联在应用程序中的行为方式。 以下部分描述了自定义关联行为的方法。

扩展允许您向关联添加自定义功能。 您可以通过在关联定义中指定区块来定义关联的扩展,如以下示例所示:

class Band
include Mongoid::Document
embeds_many :albums do
def find_by_name(name)
where(name: name).first
end
end
end
band.albums.find_by_name("Omega") # returns album "Omega"

您可以使用 class_name 宏为关联指定自定义类名。 当您想要将关联命名为类名以外的名称时,这非常有用。 以下示例使用 class_name 宏指定名为 records 的嵌入式关联表示 Album 类:

class Band
include Mongoid::Document
embeds_many :records, class_name: "Album"
end

默认下,Mongoid 在查找关联时使用父类的 _id字段。 您可以使用 primary_keyforeign_key 宏来指定要使用的不同字段。 以下示例为 Band 类上的 albums 关联指定新的主节点 (primary node in the replica set)和外键:

class Band
include Mongoid::Document
field :band_id, type: String
has_many :albums, primary_key: 'band_id', foreign_key: 'band_id_ref'
end
class Album
include Mongoid::Document
field :band_id_ref, type: String
belongs_to :band, primary_key: 'band_id', foreign_key: 'band_id_ref'
end

如果要指定 has_and_belongs_to_many 关联,则还可以使用 inverse_primary_keyinverse_foreign_key 宏。 inverse_primary_key 宏指定本地模型上远程模型用于查找文档的字段。 inverse_foreign_key 宏指定远程模型上用于存储在 inverse_primary_key 中找到的值的字段。

以下示例为 has_and_belongs_to_many 关联中的 BandMembers 类指定了新的主节点 (primary node in the replica set)键和外键:

class Band
include Mongoid::Document
field :band_id, type: String
field :member_ids, type: Array
has_many :members,
primary_key: 'member_id', foreign_key: 'member_ids',
inverse_primary_key: 'band_id', inverse_foreign_key: 'band_ids'
end
class Member
include Mongoid::Document
field :member_id, type: String
field :band_ids, type: Array
has_many :bands,
primary_key: 'band_id', foreign_key: 'band_ids',
inverse_primary_key: 'member_id', inverse_foreign_key: 'member_ids'
end

您可以使用 scope 参数指定关联的范围。 scope 参数确定 Mongoid 将哪些文档视为关联的一部分。 作用域关联在查询时仅返回与作用域条件匹配的文档。 您可以将 scope设立为元数为零的 Proc 或引用关联模型上命名范围的 Symbol。 以下示例在 Band 类中的关联上设置自定义作用域:

class Band
include Mongoid::Document
has_many :albums, scope: -> { where(published: true) }
# Uses a scope called "upcoming" on the Tour model
has_many :tours, scope: :upcoming
end

注意

您可以将与范围条件不匹配的文档添加到关联中。 Mongoid 将文档保存到数据库,并且它们将显示在关联的内存中。 但是,在查询关联时,您将看不到这些文档。

当 Mongoid 将关联加载到内存时,默认下,它使用 validates_associated 宏来验证是否存在任何子项。 Mongoid 验证子项的以下关联类型:

  • embeds_many

  • embeds_one

  • has_many

  • has_one

  • has_and_belongs_to_many

您可以在定义关联时将 validate 宏设置为 false,以关闭此验证行为,如以下示例所示:

class Band
include Mongoid::Document
embeds_many :albums, validate: false
end

Mongoid 支持一对一和一对多关联的子类的多态性。 多态关联允许单个关联包含不同类类型的对象。 您可以通过在子关联中将 polymorphic 选项设置为 true 并将 as 选项添加到父关联中来定义多态关联。 以下示例在 Band 类中创建多态关联:

class Tour
include Mongoid::Document
has_one :band, as: :featured
end
class Label
include Mongoid::Document
has_one :band, as: :featured
end
class Band
include Mongoid::Document
belongs_to :featured, polymorphic: true
end

在前面的示例中,Band 类中的 :featured 关联可以包含 LabelAlbum文档。

重要

Mongoid 仅支持从子节点到父节点的多态性。 您不能将父 has_onehas_many 关联指定为多态。

has_and_belongs_to_many 关联不支持多态关联。

从版本 9.0.2 开始, Mongoid 通过全局注册表支持自定义多态类型。 您可以指定备用键来表示不同的类,从而将代码与数据解耦。 以下示例将字符串 "artist" 指定为 Band 类的备用键:

class Band
include Mongoid::Document
identify_as 'artist'
has_many :albums, as: :record
end

在前面的示例中,identify_as 指令指示 Mongoid 将 Band 类作为字符串 "artist"存储在数据库中。

您还可以指定多个别名,如以下示例所示:

class Band
include Mongoid::Document
identify_as 'artist', 'group', 'troupe'
has_many :albums, as: :record
end

在前面的示例中,artist 是默认名称,其他名称仅用于查找记录。 这样,您就可以在不破坏数据关联的情况下重构代码。

多态类型别名是全局性的。 您指定的键在整个代码库中必须是唯一的。 但是,您可以注册可用于不同模型子集的替代解析程序。 在这种情况下,每个解析程序的键必须是唯一的。 以下示例展示了如何注册备用解析程序:

Mongoid::ModelResolver.register_resolver Mongoid::ModelResolver.new, :mus
Mongoid::ModelResolver.register_resolver Mongoid::ModelResolver.new, :tool
module Music
class Band
include Mongoid::Document
identify_as 'bnd', resolver: :mus
end
end
module Tools
class Band
include Mongoid::Document
identify_as 'bnd', resolver: :tool
end
end

Music::BandTools::Band 的别名都为 "bnd",但每个模型都使用自己的解析程序来避免冲突。

您可以为引用的关联提供 dependent 选项,以指定删除文档时 Mongoid 如何处理关联的文档。 您可以指定以下选项:

  • delete_all:删除所有子文档而不运行任何模型回调。

  • destroy:删除子文档并运行所有模型回调。

  • nullify:将子文档的外键设置为 nil。 如果子文档仅被父文档引用,则该子文档可能会成为孤立文档。

  • restrict_with_exception:如果子文档不为空,则引发异常。

  • restrict_with_error:如果子文档不为空,则取消操作并返回 false

如果您不指定任何 dependent 选项,则 Mongoid 在删除父文档时将保持子文档不变。 子文档会继续引用已删除的父文档,如果仅通过父文档引用,则子文档将成为孤立文档。

以下示例在 Band 类上指定了 dependent 选项:

class Band
include Mongoid::Document
has_many :albums, dependent: :delete_all
belongs_to :label, dependent: :nullify
end

默认下,Mongoid 在保存父文档时不会自动保存非嵌入式关联中的关联文档。 这可能会导致对不存在的文档进行悬空引用。

您可以对引用的关联使用 autosave 选项,以便在保存父文档时自动保存关联的文档。 以下示例创建了一个具有关联 Album 类的 Band 类,并指定了 autosave 选项:

class Band
include Mongoid::Document
has_many :albums
end
class Album
include Mongoid::Document
belongs_to :band, autosave: true
end
band = Band.new
album = Album.create!(band: band)
# The band is persisted at this point.

注意

Mongoid 会自动将自动保存功能添加到使用 accepts_nested_attributes_for 选项的关联中。

不需要为嵌入式关联指定 autosave 选项,因为 Mongoid 会将嵌入式文档保存在父文档中。

您可以将 autobuild 选项添加到一对一关联,例如 has_oneembeds_one,以便在访问 nil 关联时自动实例化新文档。 以下示例将 autobuild 选项添加到 Band 类的关联中:

class Band
include Mongoid::Document
embeds_one :label, autobuild: true
has_one :producer, autobuild: true
end

当 Mongoid 接触文档时,它会将文档的updated_at 字段更新为当前日期和时间。您可以将 touch 选项添加到任何 belongs_to 关联中,以确保 Mongoid 在每次子文档更新时都会接触到父文档。 以下示例将 touch 选项添加到 Band 类的关联中:

class Band
include Mongoid::Document
field :name
belongs_to :label, touch: true
end

您还可以使用 touch 选项,以字符串或符号的形式指定父关联上的另一个字段。 当 Mongoid 接触父关联时,它会将 updated_at字段和指定字段设置为当前日期和时间。

以下示例指示 Mongoid 触摸 bands_updated_at字段:

class Band
include Mongoid::Document
belongs_to :label, touch: :bands_updated_at
end

注意

在嵌入式关联中,当触及嵌入式文档时,Mongoid 会递归触及其父文档。 因此,无需向 embedded_in 关联添加 touch 属性。

Mongoid 不支持在 embedded_in 关联中指定要触及的其他字段。

您可以使用 counter_cache 选项来存储属于关联字段的对象数量。 当您指定此选项时,Mongoid 会在关联模型上存储一个额外的属性来存储计数。 因此,您必须在关联的类中指定 Mongoid::Attributes::Dynamic 模块。

以下示例将 counter_cache 选项添加到 Band 类,并在 Label 类中指定 Mongoid::Attributes::Dynamic

class Band
include Mongoid::Document
belongs_to :label, counter_cache: true
end
class Label
include Mongoid::Document
include Mongoid::Attributes::Dynamic
has_many :bands
end

当您定义关联时,Mongoid 会存储有关该关联的元数据。 您可以通过对模型类或文档调用 reflect_on_association 方法,或直接访问特定文档的元数据访问权限元数据。 以下示例展示了如何使用 reflect_on_association 方法并通过直接访问权限访问权限元数据:

# Get the metadata for a named association from the class or document
Model.reflect_on_association(:<association_name>)
# Directly access metadata on a document
model.associations[:<association_name>]

注意

将上示例中的 <association_name> 替换为关联名称。

所有关联都包含存储相关文档信息的属性。 关联包含以下属性:

  • _target:被代理的文档或多个文档

  • _base:定义关联的文档

  • _association:有关关联的信息

以下示例将访问上述每个属性:

class Band
include Mongoid::Document
embeds_many :songs
end
Band.songs = [ song ]
Band.songs._target # returns [ song ]
Band.songs._base # returns band
Band.songs._association # returns the association metadata

下表显示了存储在 _association 属性中的信息:

方法
说明

Association#as

多态子项的父项名称。

Association#as?

返回 as 选项是否存在。

Association#autobuilding?

返回关联是否为自动构建。

Association#autosaving?

返回关联是否自动保存。

Association#cascading_callbacks?

返回关联是否具有从父级向下级联的 callback。

Association#class_name

被代理文档的类名。

Association#cyclic?

返回关联是否为循环关联。

Association#dependent

关联的依赖选项。

Association#destructive?

如果关联具有依赖的删除或销毁方法,则返回 true

Association#embedded?

返回是否将该关联嵌入到另一个文档中。

Association#forced_nil_inverse?

返回该关联是否定义了 nil 反向。

Association#foreign_key

外键字段的名称。

Association#foreign_key_check

外键字段的脏检查方法的名称。

Association#foreign_key_setter

外键字段 setter 的名称。

Association#indexed?

返回该外键是否自动索引。

Association#inverses

所有反向关联的名称。

Association#inverse

单个反向关联的名称。

Association#inverse_class_name

反向关联的类名。

Association#inverse_foreign_key

反向外键字段的名称。

Association#inverse_klass

反向关联的类。

Association#inverse_association

反向关联的元数据。

Association#inverse_of

反向关联的显式定义名称。

Association#inverse_setter

用于设立反向的方法的名称。

Association#inverse_type

反向的多态类型字段的名称。

Association#inverse_type_setter

多态类型字段反向 setter 的名称。

Association#key

属性哈希中用于获取关联的字段的名称。

Association#klass

关联中代理文档的类。

Association#name

关联名称。

Association#options

返回 self,以实现API与 ActiveRecord 的兼容性。

Association#order

关联上的自定义排序选项。

Association#polymorphic?

返回关联是否为多态。

Association#setter

要设立关联的字段的名称。

Association#store_as

要在其中存储嵌入式关联的属性的名称。

Association#touchable?

返回关联是否具有触摸选项。

Association#type

要获取多态类型的字段的名称。

Association#type_setter

要设立多态类型的字段的名称。

Association#validate?

返回该关联是否有相关的验证。