关联
在此页面上
引用的关联
Mongoid 支持 ActiveRecord 用户熟悉的 has_one
、has_many
、belongs_to
和 has_and_belongs_to_many
关联。
有一个
使用 has_one
宏声明声明父项在单独的集合中存储有子项的宏。默认情况下,子项是可选的:
class Band include Mongoid::Document has_one :studio end
使用 has_one
时,子模型必须使用 belongs_to
来声明与父模型的关联:
class Studio include Mongoid::Document belongs_to :band end
根据上述定义,每个子文档都包含对各自父文档的引用:
band = Band.create!(studio: Studio.new) # => #<Band _id: 600114fa48966848ad5bd392, > band.studio # => #<Studio _id: 600114fa48966848ad5bd391, band_id: BSON::ObjectId('600114fa48966848ad5bd392')>
要使用验证,要求有子文档存在:
class Band include Mongoid::Document has_one :studio validates_presence_of :studio end
有很多
使用has_many
关联,声明父项有零个或多个存储在单独集合中的子项:
class Band include Mongoid::Document has_many :members end
与使用 has_one
一样,子模型必须使用 belongs_to
来声明与父模型的关联:
class Member include Mongoid::Document belongs_to :band end
与 has_one
一样,子文档也包含对各自父文档的引用:
band = Band.create!(members: [Member.new]) # => #<Band _id: 6001166d4896684910b8d1c5, > band.members # => [#<Member _id: 6001166d4896684910b8d1c6, band_id: BSON::ObjectId('6001166d4896684910b8d1c5')>]
使用验证,要求至少有一个子文档存在:
class Band include Mongoid::Document has_many :members validates_presence_of :members end
查询
any?
在关联上使用 any?
方法,可有效确定关联是否包含任何文档,而无需从数据库中检索整个文档集:
band = Band.first band.members.any?
any?
还实现了 Enumerable#any? API ,允许使用区块进行过滤:
band = Band.first band.members.any? { |member| member.instrument == 'piano' }
……或通过对多态关联非常有用的类名称:
class Drummer < Member end band = Band.first band.members.any?(Drummer)
如果已加载关联,则 any?
将检查加载的文档并且不查询数据库:
band = Band.first # Queries the database band.members.any? band.members.to_a # Does not query the database band.members.any?
请注意,仅调用 any?
不会加载关联(因为 any?
仅检索第一个匹配文档的 _id 字段)。
exists?
关联上的 exists?
方法可确定关联中是否有任何持久保存的文档。与 any?
方法不同:
exists?
总是查询数据库,即使关联已经加载。exists?
不考虑非持久性文档。exists?
不允许像any?
那样在应用程序中进行筛选,并且不接受任何参数。
以下示例说明了 exists?
和 any?
之间的区别:
band = Band.create! # Member is not persisted. band.members.build band.members.any? # => true band.members.exists? # => false # Persist the member. band.members.map(&:save!) band.members.any? # => true band.members.exists? # => true
属于关联
使用 belongs_to
宏将子文档与存储在单独集合中的父文档相关联。父文档(如果有关联的父文档)的 _id
会存储在子文档中。
默认情况下,如果在模型上定义了 belongs_to
关联,则必须为其赋值才能保存模型实例。使用 optional: true`
选项可使实例持久化,而无需指定父项:
class Band include Mongoid::Document has_one :studio end class Studio include Mongoid::Document belongs_to :band, optional: true end studio = Studio.create! # => #<Studio _id: 600118184896684987aa884f, band_id: nil>
要更改belongs_to
关联的默认行为,使其在全局范围内不要求各自的父项关联,设立belongs_to_required_by_default
配置选项设置为false
。
尽管 has_one
和 has_many
关联要求对子项定义相应的 belongs_to
关联,但也可以在没有相应 has_one
或 has_many
宏的情况下使用 belongs_to
。在这种情况下,您无法从父项访问子项,但可以从子项访问父项:
class Band include Mongoid::Document end class Studio include Mongoid::Document belongs_to :band end
出于明确下考虑,如果父级未定义关联,可以添加 inverse_of: nil
选项:
class Band include Mongoid::Document end class Studio include Mongoid::Document belongs_to :band, inverse_of: nil end
拥有并且属于许多
使用 has_and_belongs_to_many
宏,声明多对多关联:
class Band include Mongoid::Document has_and_belongs_to_many :tags end class Tag include Mongoid::Document has_and_belongs_to_many :bands end
两个模型实例都会存储相关模型的 ID 列表(如果有的话):
band = Band.create!(tags: [Tag.create!]) # => #<Band _id: 60011d554896684b8b910a2a, tag_ids: [BSON::ObjectId('60011d554896684b8b910a29')]> band.tags # => [#<Tag _id: 60011d554896684b8b910a29, band_ids: [BSON::ObjectId('60011d554896684b8b910a2a')]>]
可以使用 inverse_of: nil
选项创建单侧 has_and_belongs_to_many
关联,以便仅将 ID 存储在一份文档中:
class Band include Mongoid::Document has_and_belongs_to_many :tags, inverse_of: nil end class Tag include Mongoid::Document end band = Band.create!(tags: [Tag.create!]) # => #<Band _id: 60011dbc4896684bbbaa9255, tag_ids: [BSON::ObjectId('60011dbc4896684bbbaa9254')]> band.tags # => [#<Tag _id: 60011dbc4896684bbbaa9254, >]
当然,单侧 has_and_belongs_to_many
关联只能在定义它的模型中使用。
注意
给定两个模型 A 和 B,其中 A has_and_belongs_to_many
B,如果将类型 B 的文档添加到类型 A 文档上的 HABTM 关联时,Mongoid 不会更新类型 A 文档的 updated_at
字段,但会更新类型 B 文档的 updated_at
字段。
查询引用关联
在大多数情况下,跨引用关联(通常涉及数据或条件或多个集合)的高效查询是使用聚合管道执行的。聚合管道部分描述了用于构造聚合管道查询的 Mongoid 辅助方法。
对于简单的查询,可以避免使用 aggregation pipeline,并直接查询关联。在直接查询关联时,所有条件必须仅位于该关联的集合上(这通常指相关关联以及嵌入其中的任何关联)。
例如,给定以下模型:
class Band include Mongoid::Document has_many :tours has_many :awards field :name, type: String end class Tour include Mongoid::Document belongs_to :band field :year, type: Integer end class Award include Mongoid::Document belongs_to :band field :name, type: String end
可以按以下方式检索自 2000 年以来巡回演出过的所有乐队:
band_ids = Tour.where(year: {'$gte' => 2000}).pluck(:band_id) bands = Band.find(band_ids)
Tour
上的条件可以具有任意复杂度,但必须都是针对同一个 Tour
文档(或嵌入在 Tour
中的文档)。
要为自 2000 年以来巡回演出的乐队查找奖项:
band_ids = Tour.where(year: {'$gte' => 2000}).pluck(:band_id) awards = Award.where(band_id: {'$in' => band_ids})
嵌入式关联
得益于 MongoDB 的文档模型,Mongoid 还提供嵌入式关联,此类关联允许不同类型的文档分层存储在同一集合中。嵌入式关联是使用 embeds_one
、embeds_many
和 embedded_in
宏以及用于递归嵌入的 recursively_embeds_one
和 recursively_embeds_many
定义的。
嵌入一个
子文档嵌入到父文档中的一对一关联是使用 Mongoid 的 embeds_one
和 embedded_in
宏定义的。
定义
关联的父文档应使用 embeds_one
宏来指示其具有一个嵌入的子文档,其中嵌入的文档使用 embedded_in
。为了使其正常工作,关联的双方都需要定义。
class Band include Mongoid::Document embeds_one :label end class Label include Mongoid::Document field :name, type: String embedded_in :band end
存储
使用 embeds_one
宏嵌入的文档会以哈希的形式存储在父文档的数据库集合中。
{ "_id" : ObjectId("4d3ed089fb60ab534684b7e9"), "label" : { "_id" : ObjectId("4d3ed089fb60ab534684b7e0"), "name" : "Mute", } }
可以选择通过提供 :store_as
选项来告诉 Mongoid 将嵌入式文档存储在除名称外的其他属性中。
class Band include Mongoid::Document embeds_one :label, store_as: "lab" end
嵌入许多
子文档嵌入到父文档中的一对多关系是使用 Mongoid 的embeds_many
和 embedded_in
宏定义的。
定义
关联的父文档应使用 embeds_many
宏来指示它具有许多嵌入的子文档,这些嵌入的文档使用 embedded_in
。为了使其正常工作,关联双方都需要定义。
class Band include Mongoid::Document embeds_many :albums end class Album include Mongoid::Document field :name, type: String embedded_in :band end
存储
使用 embeds_many
宏嵌入的文档作为哈希数组存储在父级的数据库集合中。
{ "_id" : ObjectId("4d3ed089fb60ab534684b7e9"), "albums" : [ { "_id" : ObjectId("4d3ed089fb60ab534684b7e0"), "name" : "Violator", } ] }
可以选择通过提供 :store_as
选项来告诉 Mongoid 将嵌入式文档存储在除名称外的其他属性中。
class Band include Mongoid::Document embeds_many :albums, store_as: "albs" end
递归嵌入
文档可以使用 recursively_embeds_one
或 recursively_embeds_many
递归嵌入自身,并可通过 parent_
和 child_
方法为父文档和子文档提供访问器。
class Tag include Mongoid::Document field :name, type: String recursively_embeds_many end root = Tag.new(name: "programming") child_one = root.child_tags.build child_two = root.child_tags.build root.child_tags # [ child_one, child_two ] child_one.parent_tag # [ root ] child_two.parent_tag # [ root ] class Node include Mongoid::Document recursively_embeds_one end root = Node.new child = Node.new root.child_node = child root.child_node # child child.parent_node # root
引用与嵌入
虽然对引用与嵌入进行完整讨论超出本教程的范围,但下面给出在二者中进行选择的一些高层次考虑因素。
嵌入关联时,父文档和子文档都存储在同一集合中。这样就能在使用/需要时高效地持久保存和检索。例如,如果网站导航栏显示的用户属性存储在文档中,那么通常建议使用嵌入式关联。
使用嵌入式关联允许使用聚合管道等 MongoDB 工具以强大的方式查询这些文档。
由于嵌入文档作为其父级顶层文档的一部分存储,因此无法单独持久保存嵌入文档,也无法直接检索嵌入文档。不过借助 MongoDB 的投影操作,仍然可以有效查询和检索嵌入文档:
class Band include Mongoid::Document field :started_on, type: Date embeds_one :label end class Label include Mongoid::Document field :name, type: String embedded_in :band end # Retrieve labels for bands started in the last year. # # Sends a find query like this: # {"find"=>"bands", # "filter"=>{"started_on"=>{"$gt"=>2018-07-01 00:00:00 UTC}}, # "projection"=>{"_id"=>1, "label"=>1}} Band.where(started_on: {'$gt' => Time.now - 1.year}).only(:label).map(&:label).compact.uniq
在引用关联上设置过期值
为引用的关联设置过时值有时会导致 nil
值保留到数据库中。以以下案例为例:
class Post include Mongoid::Document has_one :comment, inverse_of: :post end class Comment include Mongoid::Document belongs_to :post, inverse_of: :comment, optional: true end post.comment = comment1 post.reload
此时, post.comment
设置为 comment1
,但由于发生了重新加载,post.comment
并不引用与 comment1
相同的对象。这意味着,更新一个对象不会隐式更新其他对象。这对下一个操作很重要:
post.comment = comment2 post.reload
现在,post.comment
设置为comment2
,旧评论中的post_id
设置为nil
。 但是,分配给post.comment
的值并未引用与comment1
相同的对象,因此,虽然post.comment
的旧值已更新为具有nil
post_id
,但comment1
仍然具有post_id
集。
post.comment = comment1 post.reload
最后,最后一个赋值尝试在 comment1
上设置 post_id
,此时该 ID 应为nil
,但已设置为旧的 post_id
。在此操作期间,将从 comment2
中清除 post_id
,并在 comment1
上设置新的 post_id
。但是,由于已在comment1
上设置了 post_id
,因此,不会保留任何内容,最终,两条注释都具有 nil
post_id
。此时,运行 post.comment
将返回 nil
。
查询嵌入式关联
查询顶级文档时,可以使用点符号在嵌入式关联中的文档上指定条件。例如,给定以下模型:
class Band include Mongoid::Document embeds_many :tours embeds_many :awards field :name, type: String end class Tour include Mongoid::Document embedded_in :band field :year, type: Integer end class Award include Mongoid::Document embedded_in :band field :name, type: String end
如需根据巡演属性检索乐队,请使用以下点符号:
# Get all bands that have toured since 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 中实现——查询不会发送到服务器。
支持以下操作符:
例如,使用刚刚给出的模型定义,我们可以查询某支已记载乐队的巡回演出:
band = Band.where(name: 'Astral Projection').first tours = band.tours.where(year: {'$gte' => 2000})
嵌入式匹配与服务器行为
Mongoid 的嵌入式匹配旨在支持与最新 MongoDB 服务器版本上的原生查询相同的功能和语义。 请注意以下已知限制:
文本搜索、地理空间查询操作符、执行JavaScript代码的操作符 ( $where ) 以及通过其他服务器功能(例如$expr和$jsonSchema)实现的操作符未实现嵌入式匹配。
Mongoid DSL 将
Range
参数扩展为带有$gte
和$lte
条件的哈希值。 在某些情况下 这会创建虚假查询。在这些情况下,嵌入式匹配器会引发InvalidQuery
异常。 已知受影响的操作符包括$elemMatch
、$eq
、$gt
、$gte
、$lt
、$lte
和$ne
。使用
$regex
执行嵌入匹配时,目前还无法将正则表达式对象指定为模式并同时提供选项。MongoDB Server 4.0及更早版本的服务器不会严格验证
$type
参数(例如,允许0等无效参数)。 这在客户端经过更严格的验证。
省略_id
字段
默认情况下,Mongoid 会为每个嵌入式文档添加 _id
字段。这样可以轻松引用嵌入的文档并对其进行操作。
这些 _id
字段可以省略,以节省存储空间。为此,请覆盖子文档中的 _id 字段定义并删除默认值:
class Order include Mongoid::Document embeds_many :line_items end class LineItem include Mongoid::Document embedded_in :order field :_id, type: Object end
在当前版本的 Mongoid 中,需要字段定义,但是,如果没有指定默认值,则不会在数据库中存储任何值。Mongoid 的未来版本可能允许删除以前定义的字段。
注意
删除 _id
字段意味着在查询、更新和删除过程中,必须通过内容属性值来识别嵌入文档。
删除
Mongoid 提供了三种从 embeds_many
关联中删除子项的方法:clear
、destroy_all
和 delete_all
。
clear
clear
方法使用$unset操作符从托管文档中删除整个关联。 它不会对要删除的文档运行销毁回调,在这方面其作用类似于delete_all
:
band = Band.find(...) band.tours.clear
如果对未保存的主机文档中的关联调用 clear
,它仍会尝试根据主机文档的 _id
从数据库中删除该关联:
band = Band.find(...) band.tours << Tour.new(...) unsaved_band = Band.new(id: band.id, tours: [Tour.new]) # Removes all tours from the persisted band due to _id match. unsaved_band.tours.clear band.tours # => []
delete_all
delete_all
方法使用$pullAll 操作符删除关联中的文档。 与clear
不同, delete_all
:
加载关联(如果尚未加载);
仅删除应用程序中存在的文档。
delete_all
不对要删除的文档运行销毁回调。
示例:
band = Band.find(...) band.tours.delete_all
destroy_all
delete_all
方法在运行销毁回调时,使用$pullAll操作符删除关联中的文档。 与delete_all
一样, destroy_all
会加载整个关联(如果尚未加载),并且仅删除应用程序中存在的文档:
band = Band.find(...) band.tours.destroy_all
哈希分配
嵌入关联允许用户为关联指定 Hash
而不是文档。赋值时,这个哈希会被强制赋值到关联类的文档中。示例如下:
class Band include Mongoid::Document embeds_many :albums end class Album include Mongoid::Document field :name, type: String embedded_in :band end band = Band.create! band.albums = [ { name: "Narrow Stairs" }, { name: "Transatlanticism" } ] p band.albums # => [ #<Album _id: 633c71e93282a4357bb608e5, name: "Narrow Stairs">, #<Album _id: 633c71e93282a4357bb608e6, name: "Transatlanticism"> ]
这适用于 embeds_one
、embeds_many
和 embedded_in
关联。请注意,您不能将哈希值分配给引用的关联。
常见行为
扩展
所有关联都可以带有扩展,这提供了一种向关联添加特定于应用程序的功能的方法。它们是通过为关联定义提供区块来定义的。
class Person include Mongoid::Document embeds_many :addresses do def find_by_country(country) where(country: country).first end def chinese _target.select { |address| address.country == "China" } end end end person.addresses.find_by_country("Mongolia") # returns address person.addresses.chinese # returns [ address ]
自定义关联名称
您可以随意为关联命名,但如果 Mongoid 和对方均无法从名称中推断出类,则您需要为宏提供一些附加选项来告诉 Mongoid 如何将它们连接起来。
class Car include Mongoid::Document embeds_one :engine, class_name: "Motor", inverse_of: :machine end class Motor include Mongoid::Document embedded_in :machine, class_name: "Car", inverse_of: :engine end
自定义主键和外键
可以显式指定查找关联时使用的字段。默认在“父”关联上使用 id
,在“子”关联上使用 #{association_name}_id
,例如使用 has_many/belongs_to:
class Company include Mongoid::Document has_many :emails end class Email include Mongoid::Document belongs_to :company end company = Company.find(id) # looks up emails where emails.company_id == company.id company.emails
指定不同的primary_key
,以更改“父项”关联上的字段名称,并foreign_key
更改“子项”关联上的字段名称:
class Company include Mongoid::Document field :c, type: String has_many :emails, foreign_key: 'c_ref', primary_key: 'c' end class Email include Mongoid::Document # This definition of c_ref is automatically generated by Mongoid: # field :c_ref, type: Object # But the type can also be specified: field :c_ref, type: String belongs_to :company, foreign_key: 'c_ref', primary_key: 'c' end company = Company.find(id) # looks up emails where emails.c_ref == company.c company.emails
对于 has_and_belongs_to_many 关联,由于数据存储在关联的两侧,因此,定义关联时有 4 个可配置字段:
:primary_key
是远程模型上的字段,其中包含用于查找远程模型的值。:foreign_key
是本地模型上存储:primary_key
值的字段。:inverse_primary_key
是本地模型上的字段,远程模型使用该字段来查找本地模型文档。:inverse_foreign_key
是远程模型上存储:inverse_primary_key
中的值的字段。
举个例子可以更清楚地说明这一点:
class Company include Mongoid::Document field :c_id, type: Integer field :e_ids, type: Array has_and_belongs_to_many :employees, primary_key: :e_id, foreign_key: :e_ids, inverse_primary_key: :c_id, inverse_foreign_key: :c_ids end class Employee include Mongoid::Document field :e_id, type: Integer field :c_ids, type: Array has_and_belongs_to_many :companies, primary_key: :c_id, foreign_key: :c_ids, inverse_primary_key: :e_id, inverse_foreign_key: :e_ids end company = Company.create!(c_id: 123) # => #<Company _id: 5c565ece026d7c461d8a9d4e, c_id: 123, e_ids: nil> employee = Employee.create!(e_id: 456) # => #<Employee _id: 5c565ee8026d7c461d8a9d4f, e_id: 456, c_ids: nil> company.employees << employee company # => #<Company _id: 5c565ece026d7c461d8a9d4e, c_id: 123, e_ids: [456]> employee # => #<Employee _id: 5c5883ce026d7c4b9e244c0c, e_id: 456, c_ids: [123]>
请注意,与默认的#{association_name}_id
字段一样,Mongoid 会自动将自定义外键c_ref
的字段添加到模型中。然而,由于 Mongoid 不知道该字段中应该允许什么类型的数据,因此该字段是使用 Object 类型创建的。最好使用适当类型显式定义字段。
自定义范围
您可以使用 :scope
参数设置关联的特定作用域。作用域是额外的筛选器,限制哪些对象被视为关联的一部分,作用域关联将只返回满足作用域条件的文档。作用域可以是:
Proc
,元数为零,或者一个引用关联模型上命名范围的
Symbol
。
class Trainer has_many :pets, scope: -> { where(species: 'dog') } has_many :toys, scope: :rubber end class Pet belongs_to :trainer end class Toy scope :rubber, where(material: 'rubber') belongs_to :trainer end
注意
可以将不满足关联范围的文档添加到该关联中。在这种情况下,此类文档将在内存中出现关联,并保存到数据库中,但以后查询关联时不会出现。例如:
trainer = Trainer.create! dog = Pet.create!(trainer: trainer, species: 'dog') cat = Pet.create!(trainer: trainer, species: 'cat') trainer.pets #=> [dog, cat] trainer.reload.pets #=> [dog]
注意
Mongoid 的作用域关联语法与 ActiveRecord 的不同。Mongoid 使用 :scope
关键字参数是为了与其他关联选项保持一致,而在 ActiveRecord 中,作用域是位置参数。
验证
请务必注意,默认情况下,Mongoid 将验证通过 validates_associated
加载到内存中的任何关联的子项。这适用于以下关联:
embeds_many
embeds_one
has_many
has_one
has_and_belongs_to_many
如果不希望执行此行为,可以在定义关联时将其关闭。
class Person include Mongoid::Document embeds_many :addresses, validate: false has_many :posts, validate: false end
多态性
一对一关联和一对多关联支持多态性,即具有单个关联可能包含不同类的对象。 例如,我们可以对一个组织进行建模,其中部门和团队都有经理,如下所示:
class Department include Mongoid::Document has_one :manager, as: :unit end class Team include Mongoid::Document has_one :manager, as: :unit end class Manager include Mongoid::Document belongs_to :unit, polymorphic: true end dept = Department.create! team = Team.create! alice = Manager.create!(unit: dept) alice.unit == dept # => true dept.manager == alice # => true
再举个例子,假设我们要跟踪产品和捆绑产品的历史价格。可以通过嵌入一对多多态关联来实现这一操作:
class Product include Mongoid::Document field :name, type: String has_and_belongs_to_many :bundles embeds_many :prices, as: :item end class Bundle include Mongoid::Document field :name, type: String has_and_belongs_to_many :products embeds_many :prices, as: :item end class Price include Mongoid::Document embedded_in :item, polymorphic: true end pants = Product.create!(name: 'Pants', prices: [Price.new, Price.new]) costume = Bundle.create!(name: 'Costume', products: [pants], prices: [Price.new, Price.new])
如需定义多态关联,请在子关联上指定 polymorphic: true
选项,在父关联上添加 as: :association_name
选项。
请注意,Mongoid 目前仅支持一个方向的多态性 - 从子项到父项。例如,多态性不能用于指定捆绑包可能包含其他捆绑包或产品:
class Bundle include Mongoid::Document # Does not work: has_many :items, polymorphic: true end
has_and_belongs_to_many
关联不支持多态关联。
级联回调
如果希望在对嵌入式文档的父项执行持久性操作时触发该嵌入式文档的回调,则需要为关联提供级联回调选项。
class Band include Mongoid::Document embeds_many :albums, cascade_callbacks: true embeds_one :label, cascade_callbacks: true end band.save # Fires all save callbacks on the band, albums, and label.
相关行为
可以为引用的关联提供依赖选项,以指示 Mongoid 如何处理删除或尝试删除关联一侧的情况。选项如下:
:delete_all
:删除子文档而不运行任何模型 callback。:destroy
:销毁子文档,然后运行所有模型 callback。:nullify
:将子文档的外键字段设置为零。如果子文档通常只通过父文档引用,则可能成为孤立文档。:restrict_with_exception
:如果子文档不为空,则raise
出错。:restrict_with_error
:如果子项不为空,则取消操作并返回 false。
如果没有提供 :dependent
选项,删除父文档后,不会修改子文档(换句话说,子文档将继续通过外键字段引用现已删除的父文档)。如果子文档通常只通过父文档引用,则可能成为孤立文档。
class Band include Mongoid::Document has_many :albums, dependent: :delete_all belongs_to :label, dependent: :nullify end class Album include Mongoid::Document belongs_to :band end class Label include Mongoid::Document has_many :bands, dependent: :restrict_with_exception end label = Label.first label.bands.push(Band.first) label.delete # Raises an error since bands is not empty. Band.first.destroy # Will delete all associated albums.
自动保存
Mongoid 和 ActiveRecord 之间的一个核心区别是,出于性能原因,当保存父级文档时,Mongoid 不会自动保存引用(即非嵌入式)关联的关联文档。
如果不使用自动保存,则可以通过关联创建对不存在的文档的悬空引用:
class Band include Mongoid::Document has_many :albums end class Album include Mongoid::Document belongs_to :band end band = Band.new album = Album.create!(band: band) # The band is not persisted at this point. album.reload album.band_id # => BSON::ObjectId('6257699753aefe153121a3d5') # Band does not exist. album.band # => nil
要在保存父文档时自动保存引用的关联,请在关联中添加 :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. album.reload album.band_id # => BSON::ObjectId('62576b4b53aefe178b65b8e3') album.band # => #<Band _id: 62576b4b53aefe178b65b8e3, >
使用 accepts_nested_attributes_for
时,自动保存功能会被自动添加到关联中,以便应用程序在处理表单提交时无需跟踪哪些关联被修改。
嵌入式关联始终自动保存,因为它们作为父文档的一部分存储。
无论是否启用自动保存,对关联的某些操作始终会对父文档和子文档进行保存。这些操作的非详尽列表如下:
给关联赋值:
# Saves the band and the album. band.albums = [Album.new] push
,<<
:band.albums << Album.new band.albums.push(Album.new)
存在谓词
所有关联都具有采用 name?
和 has_name?
形式的存在谓词,以检查关联是否为空。
class Band include Mongoid::Document embeds_one :label embeds_many :albums end band.label? band.has_label? band.albums? band.has_albums?
自动构建
一对一关联(embeds_one
、has_one
)有一个自动构建选项,该选项告知 Mongoid 在访问关联时实例化一个新文档,且该文档为 nil
。
class Band include Mongoid::Document embeds_one :label, autobuild: true has_one :producer, autobuild: true end band = Band.new band.label # Returns a new empty label. band.producer # Returns a new empty producer.
触摸
任何 belongs_to
关联都可以采用可选的 :touch
选项,这会导致每次子文档更新时都触发更新父文档:
class Band include Mongoid::Document field :name belongs_to :label, touch: true end band = Band.first band.name = "The Rolling Stones" band.save! # Calls touch on the parent label. band.touch # Calls touch on the parent label.
:touch
除了使用 update_at 外,还可以采用字符串或符号参数来指定关联的父文档上要触发更新的字段:
class Label include Mongoid::Document include Mongoid::Timestamps field :bands_updated_at, type: Time has_many :bands end class Band include Mongoid::Document belongs_to :label, touch: :bands_updated_at end label = Label.create! band = Band.create!(label: label) band.touch # Updates updated_at and bands_updated_at on the label.
保存或删除嵌入文档时,会通过组合根递归保存或删除其父文档(因为保存嵌入文档时必然会保存所有父文档)。因此在 embedded_in
关联中不需要 :touch
属性。
Mongoid 目前不支持在 embedded_in 关联中指定要使用的附加字段。
:touch
不应就 embedded_in
关联设为 false
,因为组合层次结构始终会在触发更新嵌入式文档时进行更新。此项规定目前尚未实行,但计划在今后实行。
counter_cache 选项
与 ActiveRecord 一样,您可以对关联使用 :counter_cache
选项,以便更高效地查找所属对象的数量。与 ActiveRecord 也类似,您必须考虑到关联模型上会有一个额外属性。这意味着使用 Mongoid 时,您需要在关联模型上加上 Mongoid::Attributes::Dynamic
。例如:
class Order include Mongoid::Document belongs_to :customer, counter_cache: true end class Customer include Mongoid::Document include Mongoid::Attributes::Dynamic has_many :orders end
关联代理
关联对目标对象使用透明代理。在某些情况下,这可能会导致令人惊讶的行为。
当访问关联目标上的方法时,根据关联情况,可能会丧失方法可见性:
class Order include Mongoid::Document belongs_to :customer private def internal_status 'new' end end class Customer include Mongoid::Document has_many :orders private def internal_id 42 end end order = Order.new customer = Customer.create!(orders: [order]) # has_many does not permit calling private methods on the target customer.orders.first.internal_status # NoMethodError (private method `internal_status' called for #<Order:0x000055af2ec46c50>) # belongs_to permits calling private methods on the target order.customer.internal_id # => 42
关联元数据
Mongoid 中的所有关联都包含元数据,其中保存相关关联的信息,并且是第三方开发者用来扩展 Mongoid 的宝贵工具。
您可以通过几种不同的方式访问关联的关联元数据。
# Get the metadata for a named association from the class or document. Model.reflect_on_association(:association_name) model.reflect_on_association(:association_name) # Get the metadata with a specific association itself on a specific # document. model.associations[:association_name]
属性
所有关联都包含一个 _target
(一个或多个代理文档)、一个 _base
(关联挂起的文档)和 _association
(提供有关关联的信息)。
class Person include Mongoid::Document embeds_many :addresses end person.addresses = [ address ] person.addresses._target # returns [ address ] person.addresses._base # returns person person.addresses._association # returns the association metadata
关联对象
关联对象本身包含的信息比人们可能知道的要多,对于 Mongoid 扩展的开发者很有用。
方法 | 说明 |
---|---|
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 | 返回该外键字段设置器的名称。 |
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 | 返回反向的多态类型字段设置器的名称。 |
Association#key | 返回属性哈希中用于获取关联的字段的名称。 |
Association#klass | 返回关联中代理文档的类。 |
Association#name | 返回关联名称。 |
Association#options | 返回 self,以实现与 ActiveRecord 的 API 兼容性。 |
Association#order | 返回该关联上的自定义排序选项。 |
Association#polymorphic? | 返回关联是否为多态。 |
Association#setter | 返回要设置关联的字段名称。 |
Association#store_as | 返回要在其中存储嵌入式关联的属性的名称。 |
Association#touchable? | 返回关联是否具有触发更新选项。 |
Association#type | 返回要获取多态类型的字段名称。 |
Association#type_setter | 返回要设置多态类型的字段名称。 |
Association#validate? | 返回该关联是否有相关的验证。 |