CRUD 操作
本页内容
保存文档
Mongoid 支持所有预期的 CRUD 操作,对于那些熟悉其他 Ruby 映射器(如 Active Record 或 Data Mapper)的人来说。Mongoid 与其他 MongoDB 映射器的区别在于,通用持久化操作仅对已更改的字段执行原子更新,而不是每次都写入整个文档到数据库。
持久化部分将提供执行文档中记录的命令时执行的数据库操作的示例。
标准
Mongoid 的标准持久化方法以其他映射框架中常见的通用方法的形式出现。以下表格显示了所有标准操作及其示例。
操作 | 示例 | |||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
以字符串键和以 Mongoized 形式(即它们在数据库中存储的方式)的值返回文档的属性作为“Hash”。 属性哈希还包括所有嵌入文档的属性,以及它们的嵌入文档等。如果嵌入关联为空,则其键不会出现在返回的哈希中。 |
| |||||||||||||||||||||||
将文档或多个文档插入数据库,如果发生验证或服务器错误,则引发错误。 传递一个属性哈希以创建具有指定属性的单个文档,或传递一个哈希数组以创建多个文档。如果传递单个哈希,则返回相应的文档。如果传递哈希数组,则返回与哈希相对应的文档数组。 如果向 如果在保存任何文档时出现问题,例如验证错误或服务器错误,则会引发异常,并且不会返回任何文档。但是,如果传递了一个散列数组并且先前文档已成功保存,则这些文档将保留在数据库中。 |
| |||||||||||||||||||||||
实例化一个文档或多个文档,如果验证通过,则将它们插入数据库。
如果遇到任何验证错误,则相应的文档不会被插入,而是与已插入的文档一起返回。使用 |
| |||||||||||||||||||||||
以原子方式将更改的属性保存到数据库中,或如果为新文档则插入文档。如果验证失败或存在服务器错误,则引发异常。 如果更改的属性已保存,则返回 true,否则引发异常。 |
| |||||||||||||||||||||||
以原子方式将更改的属性保存到数据库中,或如果为新文档则插入文档。 如果更改的属性已保存,则返回 true。如果有任何验证错误,则返回 false。如果在验证通过但保存过程中发生服务器错误,则引发异常。 传递 传递 |
| |||||||||||||||||||||||
在数据库中更新文档属性。如果验证通过,则返回 true,如果未通过则返回 false。 |
| |||||||||||||||||||||||
在数据库中更新文档属性,如果验证失败则引发错误。 |
| |||||||||||||||||||||||
更新单个属性,跳过验证。 |
| |||||||||||||||||||||||
在文档上执行 MongoDB 替换并更新。如果文档存在于数据库中并且 |
| |||||||||||||||||||||||
更新文档的 updated_at 时间戳,可选地提供额外的时间字段。这将触及其他 尝试触摸一个已销毁的文档将引发 |
| |||||||||||||||||||||||
不运行回调,从数据库中删除文档。 如果文档尚未持久化,Mongoid 将尝试删除具有相同 |
| |||||||||||||||||||||||
在运行销毁回调的同时从数据库中删除文档。 如果文档尚未持久化,Mongoid 将尝试删除具有相同 |
| |||||||||||||||||||||||
不运行任何回调,从数据库中删除所有文档。 |
| |||||||||||||||||||||||
在运行回调的同时从数据库中删除所有文档。这是一个可能昂贵的操作,因为所有文档都将被加载到内存中。 |
|
Mongoid 提供以下与持久性相关的属性
属性 | 示例 | |||||||
---|---|---|---|---|---|---|---|---|
如果模型实例尚未保存到数据库,则返回 |
| |||||||
如果模型实例已保存到数据库,则返回 |
|
原子
Mongoid 暴露MongoDB 更新运算符 作为 Mongoid 文档上的方法。当使用这些方法时,不会调用回调,也不会执行验证。支持更新运算符包括
操作 | 示例 | |||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
在字段上执行原子的 $addToSet。 |
| |||||||||||||||||||||||||||||||||||||||||||
在字段上执行原子的 $bit。 |
| |||||||||||||||||||||||||||||||||||||||||||
在字段上执行原子的 $inc。 |
| |||||||||||||||||||||||||||||||||||||||||||
在字段上执行原子的 $pop。 |
| |||||||||||||||||||||||||||||||||||||||||||
在字段上执行原子的 $pull。 |
| |||||||||||||||||||||||||||||||||||||||||||
在字段上执行原子的 $pullAll。 |
| |||||||||||||||||||||||||||||||||||||||||||
在字段上执行原子的 $push。 |
| |||||||||||||||||||||||||||||||||||||||||||
在字段上执行原子的 $rename。 |
| |||||||||||||||||||||||||||||||||||||||||||
更新模型实例上的属性,如果实例已经持久化,则对字段执行原子的 $set,绕过验证。
|
| |||||||||||||||||||||||||||||||||||||||||||
在字段上执行原子的 $unset。 |
|
请注意,由于这些方法跳过了验证,因此有可能将无效的文档保存到数据库中,并在应用程序中留下无效的文档(由于失败的验证,这些文档在通过 save
调用时将无法保存)。
原子操作分组
可以使用文档上的 #atomically
方法将原子操作组合在一起。发送到 #atomically
给定的块中的所有操作都作为单个原子命令发送到集群。例如
person.atomically do person.inc(age: 1) person.set(name: 'Jake') end
#atomically
块可以嵌套。默认行为是在每个块结束时立即写入块执行的改变
person.atomically do person.atomically do person.inc(age: 1) person.set(name: 'Jake') end raise 'An exception' # name and age changes are still persisted end
可以通过指定 join_context: true
选项到 #atomically
来改变此行为,或全局设置 join_contexts
配置选项 为 true
。当启用上下文连接时,嵌套的 #atomically
块将与外部块连接,并且只有最外层的块(或第一个 join_contexts
为 false 的块)实际上会将更改写入集群。例如
person.atomically do person.atomically(join_context: true) do person.inc(age: 1) person.set(name: 'Jake') end raise 'An exception' # name and age changes are not persisted end
可以通过在 Mongoid 配置中设置 join_context
选项来全局默认启用上下文连接行为。在这种情况下,可以在 #atomically
块上指定 join_context: false
以获得独立的持久化上下文行为。
如果在尚未将更改持久化到集群的 #atomically
块中引发异常,Mongoid 模型上的任何挂起的属性更改都将被撤销。例如
person = Person.new(name: 'Tom') begin person.atomically do person.inc(age: 1) person.set(name: 'Jake') person.name # => 'Jake' raise 'An exception' end rescue Exception person.name # => 'Tom' end
本节中描述的原子操作一次应用于一个文档,因此嵌套在多个文档上调用的 #atomically
块不会使不同文档的更改原子性地一起持久化。然而,MongoDB 从服务器版本 4.0 开始提供了 多文档事务,它提供了跨多个文档的原子持久化。
重新加载
使用 reload
方法从数据库中获取文档的最新版本。文档的任何未保存的修改都将丢失
band = Band.create!(name: 'foo') # => #<Band _id: 6206d06de1b8324561f179c9, name: "foo", description: nil, likes: nil> band.name = 'bar' band # => #<Band _id: 6206d06de1b8324561f179c9, name: "bar", description: nil, likes: nil> band.reload # => #<Band _id: 6206d06de1b8324561f179c9, name: "foo", description: nil, likes: nil>
当文档重新加载时,其所有嵌入式关联也会在相同的查询中重新加载(因为嵌入式文档存储在服务器上的父文档中)。如果一个文档有引用关联,则加载的关联不会重新加载,但其值将被清除,以便这些关联将在下一次访问时从数据库中加载。
注意
某些关联操作,例如赋值,会持久化新文档。在这些情况下,通过重新加载可能不会有未保存的修改可以撤销。在以下示例中,将空数组赋值给关联会立即持久化,重新加载不会对文档进行任何更改
# Assuming band has many tours, which could be referenced: band = Band.create!(tours: [Tour.create!]) # ... or embedded: band = Band.create!(tours: [Tour.new]) # This writes the empty tour list into the database. band.tours = [] # There are no unsaved modifications in band at this point to be reverted. band.reload # Returns the empty array since this is what is in the database. band.tours # => []
如果模型定义了分片键,则分片键值将包含在重新加载查询中。
如果数据库不包含匹配的文档,Mongoid通常引发Mongoid::Errors::DocumentNotFound
。但是,如果配置选项raise_not_found_error
设置为false
,并且数据库不包含匹配的文档,Mongoid将用设置为默认值的属性的新创建的文档替换当前文档。重要的是,这通常会导致文档的_id
发生变化,如下例所示
band = Band.create! # => #<Band _id: 6206d00de1b8324561f179c7, name: "foo", description: nil, likes: nil> Mongoid.raise_not_found_error = false band.destroy band.reload # => #<Band _id: 6206d031e1b8324561f179c8, name: nil, description: nil, likes: nil>
因此,不建议在将raise_not_found_error
设置为false
时使用reload
。
重新加载未保存的文档
reload
函数可以在文档尚未持久化时调用。在这种情况下,reload
使用文档中指定的 id
值(以及如果定义了分片键,则还包括分片键值)执行一个 find
查询。
existing = Band.create!(name: 'Photek') # Unsaved document band = Band.new(id: existing.id) band.reload band.name # => "Photek"
访问字段值
Mongoid 提供了多种访问字段值的方法。
注意
获取器和设置器
推荐的做法是使用为每个声明字段生成的获取器和设置器方法。
class Person include Mongoid::Document field :first_name end person = Person.new person.first_name = "Artem" person.first_name # => "Artem"
要使用此机制,每个字段必须显式声明,或者模型类必须启用 动态字段。
自定义获取器和设置器
可以显式定义获取器和设置器方法,以便在读取或写入字段时提供自定义行为,例如值转换或在不同字段名称下存储值。在这种情况下,可以使用 read_attribute
和 write_attribute
方法直接将值写入属性哈希中。
class Person include Mongoid::Document def first_name read_attribute(:fn) end def first_name=(value) write_attribute(:fn, value) end end person = Person.new person.first_name = "Artem" person.first_name # => "Artem" person.attributes # => {"_id"=>BSON::ObjectId('606477dc2c97a628cf47075b'), "fn"=>"Artem"}
read_attribute
& write_attribute
read_attribute
和 write_attribute
方法也可以显式使用。请注意,如果字段指定了其存储字段名称,则 read_attribute
和 write_attribute
都接受声明的字段名称或存储字段名称进行操作。
class Person include Mongoid::Document field :first_name, as: :fn field :last_name, as: :ln end person = Person.new(first_name: "Artem") # => #<Person _id: 60647a522c97a6292c195b4b, first_name(fn): "Artem", last_name(ln): nil> person.read_attribute(:first_name) # => "Artem" person.read_attribute(:fn) # => "Artem" person.write_attribute(:last_name, "Pushkin") person # => #<Person _id: 60647a522c97a6292c195b4b, first_name(fn): "Artem", last_name(ln): "Pushkin"> person.write_attribute(:ln, "Medvedev") person # => #<Person _id: 60647a522c97a6292c195b4b, first_name(fn): "Artem", last_name(ln): "Medvedev">
read_attribute
和 write_attribute
不要求使用该名称的字段已定义,但使用 write_attribute
写入字段值也不会导致相应的字段被定义。
person.write_attribute(:undefined, "Hello") person # => #<Person _id: 60647b212c97a6292c195b4c, first_name(fn): "Artem", last_name(ln): "Medvedev"> person.attributes # => {"_id"=>BSON::ObjectId('60647b212c97a6292c195b4c'), "first_name"=>"Artem", "last_name"=>"Medvedev", "undefined"=>"Hello"} person.read_attribute(:undefined) # => "Hello" person.undefined # raises NoMethodError
当使用 read_attribute
访问缺失的字段时,它返回 nil
。
哈希访问
Mongoid模型实例定义了[]
和[]=
方法来提供类似Hash
风格的属性访问。[]
是read_attribute
的别名,而[]=
是write_attribute
的别名;有关它们行为的详细描述,请参阅read_attribute和write_attribute部分。
class Person include Mongoid::Document field :first_name, as: :fn field :last_name, as: :ln end person = Person.new(first_name: "Artem") person["fn"] # => "Artem" person[:first_name] # => "Artem" person[:ln] = "Medvedev" person # => #<Person _id: 606483742c97a629bdde5cfc, first_name(fn): "Artem", last_name(ln): "Medvedev"> person["last_name"] = "Pushkin" person # => #<Person _id: 606483742c97a629bdde5cfc, first_name(fn): "Artem", last_name(ln): "Pushkin">
批量属性写入
当您想一次性设置多个字段值时,也有几种不同的方法可以实现。
# Get the field values as a hash. person.attributes # Set the field values in the document. Person.new(first_name: "Jean-Baptiste", middle_name: "Emmanuel") person.attributes = { first_name: "Jean-Baptiste", middle_name: "Emmanuel" } person.write_attributes( first_name: "Jean-Baptiste", middle_name: "Emmanuel", )
脏属性跟踪
Mongoid支持使用与Active Model类似的API跟踪已更改或“脏”字段。如果定义的某个字段在模型中已修改,则模型将被标记为脏,并触发一些额外的行为。
查看变更
有多种方法可以查看模型中已更改的内容。从文档实例化时开始记录更改,无论是作为新文档还是从数据库加载,直到保存为止。任何持久化操作都会清除更改。
class Person include Mongoid::Document field :name, type: String end person = Person.first person.name = "Alan Garner" # Check to see if the document has changed. person.changed? # true # Get an array of the names of the changed fields. person.changed # [ :name ] # Get a hash of the old and changed values for each field. person.changes # { "name" => [ "Alan Parsons", "Alan Garner" ] } # Check if a specific field has changed. person.name_changed? # true # Get the changes for a specific field. person.name_change # [ "Alan Parsons", "Alan Garner" ] # Get the previous value for a field. person.name_was # "Alan Parsons"
注意
在文档上设置关联不会导致更改
或已更改的属性
散列被修改。这适用于所有关联,无论是引用的还是内嵌的。请注意,更改引用关联的_id(们)字段会导致更改出现在更改
和已更改的属性
散列中。
重置更改
您可以通过调用重置方法将字段的更改重置为其以前的值。
person = Person.first person.name = "Alan Garner" # Reset the changed name back to the original person.reset_name! person.name # "Alan Parsons"
持久化
Mongoid将脏跟踪作为其持久化操作的核心。它查看文档上的更改,并且仅原子地更新已更改的内容,这与其他在每个保存时写入整个文档的框架不同。如果没有进行任何更改,Mongoid在调用Model#save
时不会访问数据库。
查看先前更改
文档已保存后,您可以通过调用 Model#previous_changes
来查看之前的更改。
person = Person.first person.name = "Alan Garner" person.save # Clears out current changes. # View the previous changes. person.previous_changes # { "name" => [ "Alan Parsons", "Alan Garner" ] }
更新容器字段
请注意,在 MONGOID-2951 被解决之前,所有字段(包括容器字段)都必须赋值,以便其值能持久化到数据库。
例如,向这样的集合添加元素不起作用
class Band include Mongoid::Document field :tours, type: Set end band = Band.new band.tours # => #<Set: {}> band.tours << 'London' # => #<Set: {"London"}> band.tours # => #<Set: {}>
相反,必须在模型外部修改字段值,并将其重新赋值给模型,如下所示
class Band include Mongoid::Document field :tours, type: Set end band = Band.new tours = band.tours # => #<Set: {}> tours << 'London' # => #<Set: {"London"}> band.tours = tours # => #<Set: {"London"}> band.tours # => #<Set: {"London"}>
只读文档
根据 Mongoid.legacy_readonly
特性标志的值,文档可以通过两种方式标记为只读。
如果此标志被关闭,当在文档上调用 #readonly!
方法时,文档将被标记为只读。当此标志关闭时,只读文档在尝试执行任何持久化操作(包括但不限于保存、更新、删除和销毁)时将引发 ReadonlyDocument
错误。注意,重新加载不会重置只读状态。
band = Band.first band.readonly? # => false band.readonly! band.readonly? # => true band.name = "The Rolling Stones" band.save # => raises ReadonlyDocument error band.reload.readonly? # => true
如果此标志被打开,当文档被投影(即使用 #only
或 #without
)时,文档将被标记为只读。当此标志打开时,只读文档将不可删除或销毁(将引发 ReadonlyDocument
错误),但可以保存和更新。重新加载文档将重置只读状态。
class Band include Mongoid::Document field :name, type: String field :genre, type: String end band = Band.only(:name).first band.readonly? # => true band.destroy # => raises ReadonlyDocument error band.reload.readonly? # => false
重写 readonly?
使文档只读的另一种方式是重写 readonly? 方法
class Band include Mongoid::Document field :name, type: String field :genre, type: String def readonly? true end end band = Band.first band.readonly? # => true band.destroy # => raises ReadonlyDocument error