关联
在此页面上
Overview
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
有很多
您可以使用 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_one
或 has_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_one
或 has_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
字段。
查询引用的关联
您可以使用聚合管道跨引用的关联查询文档。 聚合管道允许您跨多个集合创建查询并将数据处理为指定的格式。 要学习;了解有关使用聚合管道的更多信息,请参阅聚合指南。
对于简单的查询,可以直接查询关联。 直接查询集合时,只能查询集合本身中的字段和值。 您无法直接查询与您正在查询的集合关联的集合。
示例,考虑以下 Band
和 Tour
类:
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_one
和 recursively_embeds_many
宏将一个或多个相同类型的文档嵌入到父类中。 这两个宏都通过 parent_*
方法和 child_*
方法为父文档和子文档提供访问器,其中 *
表示类的名称。 以下示例创建了一个 Band
类,该类以递归方式嵌入多个其他 Band
文档来表示多个乐队名称:
class Band include Mongoid::Document field :name, type: String recursively_embeds_many end
您可以通过 parent_band
和 child_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})
对已加载文档的嵌入式匹配具有以下已知限制:
以下功能未实现嵌入式匹配:
执行JavaScript代码的操作符,例如 $where
通过其他服务器功能实现的操作符,例如 $expr 和 $jsonSchema
Mongoid 将
Range
参数扩展为具有$gte
和$lte
条件的哈希值。 在某些情况下,这可能会导致无效查询并引发InvalidQuery
异常。使用
$regex
操作符时,您无法在将正则表达式对象指定为模式的同时为$options
字段提供选项。 仅当正则表达式模式是字符串时才能提供选项。MongoDB Server 4.0 及更早版本不严格验证
$type
参数。
省略_id字段
默认下,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_key
和 foreign_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_key
和 inverse_foreign_key
宏。 inverse_primary_key
宏指定本地模型上远程模型用于查找文档的字段。 inverse_foreign_key
宏指定远程模型上用于存储在 inverse_primary_key
中找到的值的字段。
以下示例为 has_and_belongs_to_many
关联中的 Band
和 Members
类指定了新的主节点 (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
关联可以包含 Label
或 Album
文档。
重要
Mongoid 仅支持从子节点到父节点的多态性。 您不能将父 has_one
或 has_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::Band
和 Tools::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_one
和 embeds_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
属性中的信息:
方法 | 说明 |
---|---|
| 多态子项的父项名称。 |
| 返回 |
| 返回关联是否为自动构建。 |
| 返回关联是否自动保存。 |
| 返回关联是否具有从父级向下级联的 callback。 |
| 被代理文档的类名。 |
| 返回关联是否为循环关联。 |
| 关联的依赖选项。 |
| 如果关联具有依赖的删除或销毁方法,则返回 |
| 返回是否将该关联嵌入到另一个文档中。 |
| 返回该关联是否定义了 |
| 外键字段的名称。 |
| 外键字段的脏检查方法的名称。 |
| 外键字段 setter 的名称。 |
| 返回该外键是否自动索引。 |
| 所有反向关联的名称。 |
| 单个反向关联的名称。 |
| 反向关联的类名。 |
| 反向外键字段的名称。 |
| 反向关联的类。 |
| 反向关联的元数据。 |
| 反向关联的显式定义名称。 |
| 用于设立反向的方法的名称。 |
| 反向的多态类型字段的名称。 |
| 多态类型字段反向 setter 的名称。 |
| 属性哈希中用于获取关联的字段的名称。 |
| 关联中代理文档的类。 |
| 关联名称。 |
| 返回 |
| 关联上的自定义排序选项。 |
| 返回关联是否为多态。 |
| 要设立关联的字段的名称。 |
| 要在其中存储嵌入式关联的属性的名称。 |
| 返回关联是否具有触摸选项。 |
| 要获取多态类型的字段的名称。 |
| 要设立多态类型的字段的名称。 |
| 返回该关联是否有相关的验证。 |