增删改查操作
在此页面上
保存文档
对于熟悉 Active Record 或 Data Mapper 等其他 Ruby 映射器的用户来说,Mongoid 支持所有预期的 CRUD 操作。Mongoid 与 MongoDB 的其他映射器的区别在于,一般的持久性操作仅对已更改的字段执行原子更新,而不是每次将整个文档写入数据库。
关于持久性的各节将提供示例来说明在执行文档中的命令时会执行哪些数据库操作。
标准分析器
Mongoid 的标准持久性方法采用其他映射框架中常见方法的形式。下表显示了所有标准操作及其示例。
操作 | 例子 | |||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
以带有string键的 ``Hash`` 形式返回文档的属性,并以 Mongoized 形式返回其值(即它们在数据库中的存储方式)。 属性哈希还包含所有嵌入式文档的属性,以及它们的嵌入式文档等。如果嵌入关联为空,则其键将不会显示在返回的哈希中。 |
| |||||||||||||||||||||||
将一个或多个文档插入数据库,如果发生验证或服务器错误,则会引发错误。 传递属性哈希值以创建具有指定属性的一个文档,或传递一组哈希值以创建多个文档。如果已传递单个哈希值,则会返回相应的文档。如果已传递哈希数组,则返回与哈希对应的文档数组。 如果将区块赋给 如果保存任何文档时出现问题(例如验证错误或服务器错误),则会引发异常,因此不会返回任何文档。但是,如果传递了哈希数组并成功保存了先前的文档,则这些文档将保留在数据库中。 |
| |||||||||||||||||||||||
将一个或多个文档实例化,如果验证通过,则将它们插入到数据库中。
如果遇到任何验证错误,则不会插入相应的文档,而是与已插入的文档一起返回。请使用 |
| |||||||||||||||||||||||
自动将更改的属性保存到数据库,如果是新的,则插入文档。如果验证失败或服务器出错,则引发异常。 如果已保存更改的属性,则返回 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 开始提供多文档事务,服务器版本 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 提供了多种访问字段值的方法。
注意
Getter 方法和 Setter 方法
推荐的方法是使用为每个声明的字段生成的 getter 和 setter 方法:
class Person include Mongoid::Document field :first_name end person = Person.new person.first_name = "Artem" person.first_name # => "Artem"
要使用此机制,必须显示声明每个字段,或者模型类必须启用动态字段。
自定义 getter 和 setter
可以显式定义 getter 和 setter 方法,以在读取或写入字段时提供自定义行为,例如值转换或在不同字段名称下存储值。在这种情况下,可以使用 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"
注意
在文档上设置关联不会导致 changes
或 changed_attributes
哈希值被修改。无论是引用式关联还是嵌入式关联,所有关联都是如此。请注意,更改引用式关联上的 _id 字段确实会导致更改显示在 changes
和 changed_attributes
哈希中。
重置更改
您可以通过调用 reset 方法,将经过更改的字段值重置为之前的值。
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