Docs 菜单
Docs 主页
/ / /
Mongoid
/

关联

在此页面上

  • 引用的关联
  • 有一个
  • 有很多
  • 属于关联
  • 拥有并且属于许多
  • 查询引用关联
  • 嵌入式关联
  • 嵌入一个
  • 嵌入许多
  • 递归嵌入
  • 引用与嵌入
  • 查询嵌入式关联
  • 省略 _id 字段
  • 删除
  • 哈希分配
  • 常见行为
  • 扩展
  • 自定义关联名称
  • 自定义主键和外键
  • 自定义范围
  • 验证
  • 多态性
  • 级联回调
  • 相关行为
  • 自动保存
  • 存在谓词
  • 自动构建
  • 触摸
  • counter_cache 选项
  • 关联代理
  • 关联元数据
  • 属性
  • 关联对象

Mongoid 支持 ActiveRecord 用户熟悉的 has_onehas_manybelongs_tohas_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? 方法,可有效确定关联是否包含任何文档,而无需从数据库中检索整个文档集:

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? 方法可确定关联中是否有任何持久保存的文档。与 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_onehas_many 关联要求对子项定义相应的 belongs_to 关联,但也可以在没有相应 has_onehas_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_oneembeds_manyembedded_in 宏以及用于递归嵌入的 recursively_embeds_onerecursively_embeds_many 定义的。

子文档嵌入到父文档中的一对一关联是使用 Mongoid 的 embeds_oneembedded_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_manyembedded_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_onerecursively_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等无效参数)。 这在客户端经过更严格的验证。

默认情况下,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 关联中删除子项的方法:cleardestroy_alldelete_all

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方法使用$pullAll 操作符删除关联中的文档。 与clear不同, delete_all

  • 加载关联(如果尚未加载);

  • 仅删除应用程序中存在的文档。

delete_all 不对要删除的文档运行销毁回调。

示例:

band = Band.find(...)
band.tours.delete_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_oneembeds_manyembedded_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_onehas_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,因为组合层次结构始终会在触发更新嵌入式文档时进行更新。此项规定目前尚未实行,但计划在今后实行。

与 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?
返回该关联是否有相关的验证。

后退

继承