关联
在本页
引用关联
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
关联,但 belongs_to
也可以在没有相应 has_one
或 has_many
宏的情况下使用。在这种情况下,子对象无法从父对象访问,但父对象可以从子对象访问
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,其中Ahas_and_belongs_to_many
B,当向类型为A的文档上添加类型为B的文档到HABTM关联时,Mongoid不会更新类型为A的文档的updated_at
字段,但会更新类型为B的文档的updated_at
字段。
查询引用关联
在大多数情况下,使用聚合管道进行跨引用关联的查询(以及一般涉及数据或条件或多个集合的查询)是高效的。关于构建聚合管道查询的Mongoid辅助函数在聚合管道部分中描述。
对于简单查询,可以避免使用聚合管道,并直接查询关联。当直接查询关联时,所有条件必须仅在该关联的集合上(这通常意味着相关的关联及其嵌入的任何关联)。
例如,给定以下模型
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", } }
您可以选择性地告诉Mongoid将嵌入的文档存储在除名称之外的其他属性中,通过提供:store_as
选项。
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", } ] }
您可以选择性地告诉Mongoid将嵌入的文档存储在除名称之外的其他属性中,通过提供:store_as
选项。
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
设置为 nil
,此时应该为 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.)实现的运算符。
蒙古鸟 DSL 将
Range
参数扩展为具有$gte
和$lte
条件的哈希表。在某些情况下,这会创建无效的查询。嵌套匹配器在这些情况下引发InvalidQuery
异常。已知受影响的操作符包括$elemMatch
、$eq
、$gt
、$gte
、$lt
、$lte
和$ne
。当使用
$regex
进行嵌套匹配时,目前无法 将正则表达式对象指定为模式,并提供选项。MongoDB 服务器 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 操作符 从主文档中删除整个关联。它不会在正在删除的文档上运行 destroy 回调,在这方面类似于 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
哈希赋值
嵌入式关联允许用户将 哈希
而不是文档赋值给关联。在赋值时,此哈希被强制转换为要分配给关联的类的文档。以下是一个示例
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 如何将它们连接起来。
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
:删除子文档而不运行任何模型回调。:destroy
:销毁子文档并运行所有模型回调。:nullify
:将子文档的外键字段设置为 nil。如果子文档通常仅通过父文档引用,则子文档可能成为孤儿。:restrict_with_exception
:如果子文档不为空,则抛出错误。: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.
自动保存
与ActiveRecord相比,Mongoid的一个核心区别是,出于性能考虑,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
还可以接受一个字符串或符号参数,指定在父关联中需要修改的字段,除了 updated_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 关联上指定要修改的额外字段。
在 embedded_in
关联上不应将 :touch
设置为 false
,因为嵌入文档的修改会导致组合层次结构始终被更新。这目前尚未实施,但未来将实施这项措施。
计数缓存选项
与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? | 返回关联是否从父类向下级联回调。 |
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? | 返回关联是否有touch选项。 |
Association#type | 返回获取多态类型的字段名称。 |
Association#type_setter | 返回设置多态类型的字段名称。 |
Association#validate? | 返回关联是否有关联的验证。 |