Docs 菜单
Docs 主页
/ / /
Mongoid
/

增删改查操作

在此页面上

  • 保存文档
  • 标准分析器
  • 原子性
  • 重新加载
  • 重新加载未保存的文档
  • 访问字段值
  • Getter 方法和 Setter 方法
  • 自定义 getter 和 setter
  • read_attributewrite_attribute
  • 哈希访问
  • 批量属性写入
  • 脏跟踪
  • 查看更改
  • 重置更改
  • 持久性
  • 查看以前的更改
  • 更新容器字段
  • 只读文档
  • 覆盖 readonly?

对于熟悉 Active Record 或 Data Mapper 等其他 Ruby 映射器的用户来说,Mongoid 支持所有预期的 CRUD 操作。Mongoid 与 MongoDB 的其他映射器的区别在于,一般的持久性操作仅对已更改的字段执行原子更新,而不是每次将整个文档写入数据库。

关于持久性的各节将提供示例来说明在执行文档中的命令时会执行哪些数据库操作。

Mongoid 的标准持久性方法采用其他映射框架中常见方法的形式。下表显示了所有标准操作及其示例。

操作
例子

Model#attributes

以带有string键的 ``Hash`` 形式返回文档的属性,并以 Mongoized 形式返回其值(即它们在数据库中的存储方式)。

属性哈希还包含所有嵌入式文档的属性,以及它们的嵌入式文档等。如果嵌入关联为空,则其键将不会显示在返回的哈希中。

person = Person.new(first_name: "Heinrich", last_name: "Heine")
person.attributes
# => { "_id" => BSON::ObjectId('633467d03282a43784c2d56e'), "first_name" => "Heinrich", "last_name" => "Heine" }

Model.create!

将一个或多个文档插入数据库,如果发生验证或服务器错误,则会引发错误。

传递属性哈希值以创建具有指定属性的一个文档,或传递一组哈希值以创建多个文档。如果已传递单个哈希值,则会返回相应的文档。如果已传递哈希数组,则返回与哈希对应的文档数组。

如果将区块赋给 create! ,在尝试保存该文档之前,将依次以每个文档作为参数来调用它。

如果保存任何文档时出现问题(例如验证错误或服务器错误),则会引发异常,因此不会返回任何文档。但是,如果传递了哈希数组并成功保存了先前的文档,则这些文档将保留在数据库中。

Person.create!(
first_name: "Heinrich",
last_name: "Heine"
) # => Person instance
Person.create!([
{ first_name: "Heinrich", last_name: "Heine" },
{ first_name: "Willy", last_name: "Brandt" }
]) # => Array of two Person instances
Person.create!(first_name: "Heinrich") do |doc|
doc.last_name = "Heine"
end # => Person instance

Model.create

将一个或多个文档实例化,如果验证通过,则将它们插入到数据库中。

create 类似于 create! 但不会在验证错误时引发异常。它仍然会因服务器错误而引发错误,例如尝试插入带有以下内容的文档 _id 集合中已经存在的 _id。

如果遇到任何验证错误,则不会插入相应的文档,而是与已插入的文档一起返回。请使用 persisted? , new_record? or errors 方法来检查哪些返回的文档已插入数据库。

Person.create(
first_name: "Heinrich",
last_name: "Heine"
) # => Person instance
Person.create([
{ first_name: "Heinrich", last_name: "Heine" },
{ first_name: "Willy", last_name: "Brandt" }
]) # => Array of two Person instances
Person.create(first_name: "Heinrich") do |doc|
doc.last_name = "Heine"
end # => Person instance
class Post
include Mongoid::Document
validates_uniqueness_of :title
end
posts = Post.create([{title: "test"}, {title: "test"}])
# => array of two Post instances
posts.map { |post| post.persisted? } # => [true, false]

Model#save!

自动将更改的属性保存到数据库,如果是新的,则插入文档。如果验证失败或服务器出错,则引发异常。

如果已保存更改的属性,则返回 true,否则会引发异常。

person = Person.new(
first_name: "Heinrich",
last_name: "Heine"
)
person.save!
person.first_name = "Christian Johan"
person.save!

Model#save

以原子方式将更改的属性保存到数据库,如果有新属性,则插入文档。

如果已保存更改的属性,则返回 true。如果存在任何验证错误,则返回 false。如果文档通过了验证,但在保存过程中出现服务器错误,则引发异常。

通过 validate: false 绕过验证的选项。

通过 touch: false 选项,忽略对 updated_at 字段的更新。如果之前未保存要保存的文档,则忽略此选项,并且将使用当前时间更新 created_at 和 updated_at 字段。

person = Person.new(
first_name: "Heinrich",
last_name: "Heine"
)
person.save
person.save(validate: false)
person.save(touch: false)
person.first_name = "Christian Johan"
person.save

Model#update_attributes

更新数据库中的文档属性。如果验证通过,则返回 true,如果未通过,则返回 false。

person.update_attributes(
first_name: "Jean",
last_name: "Zorg"
)

Model#update_attributes!

更新数据库中的文档属性,如果验证失败,则会引发错误。

person.update_attributes!(
first_name: "Leo",
last_name: "Tolstoy"
)

Model#update_attribute

绕过验证,更新单个属性。

person.update_attribute(:first_name, "Jean")

Model#upsert

执行 MongoDB 利用更新或插入来替换文档的操作。如果文档存在于数据库中,并且 :replace 选项设立为 true,则它将被应用程序中的当前文档覆盖(数据库中存在但不存在于应用程序的文档实例中的任何属性都将丢失)。 如果 :replace 选项为 false(默认值)时,将更新文档,并将保留不在应用程序文档中的任何属性。 如果数据库中不存在该文档,则会将其插入。 请注意,这仅运行 {before|after|around}_upsert 回调。

person = Person.new(
first_name: "Heinrich",
last_name: "Heine"
)
person.upsert
person.upsert(replace: true)

Model#touch

更新文档的 updated_at 时间戳,可以选择使用一个额外提供的时间字段。这会将接触级联到所有 belongs_to 文档与选项集的关联。此操作会跳过验证和回调。

尝试接触已损坏的文档,将引发 FrozenError与尝试更新已销毁文档的属性时相同

person.touch
person.touch(:audited_at)

Model#delete

从数据库中删除文档而不运行回调。

如果文档没有持久化,则 Mongoid 将尝试从数据库中删除文档,条件为此类文档具有相同的 _id.

person.delete
person = Person.create!(...)
unsaved_person = Person.new(id: person.id)
unsaved_person.delete
person.reload
# raises Mongoid::Errors::DocumentNotFound because the person was deleted

Model#destroy

在运行销毁回调时,从数据库中删除文档。

如果文档没有持久化,则 Mongoid 将尝试从数据库中删除文档,条件为此类文档具有相同的 _id.

person.destroy
person = Person.create!(...)
unsaved_person = Person.new(id: person.id)
unsaved_person.destroy
person.reload
# raises Mongoid::Errors::DocumentNotFound because the person was deleted

Model.delete_all

不运行任何回调的情况下,从数据库中删除所有文档。

Person.delete_all

Model.destroy_all

在运行回调时,从数据库中删除所有文档,这一操作可能代价高昂,因为所有文档都会加载到内存中。

Person.destroy_all

Mongoid 提供了以下与持久性相关的属性:

属性
例子

Model#new_record?

返回: true 如果模型实例尚未保存到数据库中。相对于 persisted?

person = Person.new(
first_name: "Heinrich",
last_name: "Heine"
)
person.new_record? # => true
person.save!
person.new_record? # => false

Model#persisted?

返回: true 条件是模型实例已保存到数据库。相对于 new_record?

person = Person.new(
first_name: "Heinrich",
last_name: "Heine"
)
person.persisted? # => false
person.save!
person.persisted? # => true

Mongoid 将 MongoDB 更新操作符作为 Mongoid 文档上的方法公开。 使用这些方法时,不会调用回调,也不会执行验证。 支持的更新操作符包括:

操作
例子

Model#add_to_set

在字段上执行原子 $addToSet。

person.add_to_set(aliases: "Bond")

Model#bit

对字段执行原子 $bit。

person.bit(age: { and: 10, or: 12 })

Model#inc

在字段上执行原子 $inc。

person.inc(age: 1)

Model#pop

在字段上执行原子 $pop。

person.pop(aliases: 1)

Model#pull

在字段上执行原子 $pull。

person.pull(aliases: "Bond")

Model#pull_all

对字段执行原子 $pullAll。

person.pull_all(aliases: [ "Bond", "James" ])

Model#push

对字段执行原子 $push。

person.push(aliases: ["007","008"])

Model#rename

在字段上执行原子 $rename。

person.rename(bday: :dob)

Model#set

更新模型实例上的属性,如果该实例已持久存在,则在字段上执行原子 $set,绕过验证。

set 也可以对哈希字段值进行深度设置。

set 还可以深度设置 embeds_one 关联。如果此类关联的文档为零,则会在更新之前创建一个文档。

set 不应用于 has_one 关联,因为它在情况下无法正常工作。

person = Person.create!(name: "Ricky Bobby")
person.set(name: "Tyler Durden") # updates name in the database
person = Person.new
person.set(name: "Tyler Durden") # does not write to database
person.name # => "Tyler Durden"
person.persisted? # => true
class Post
include Mongoid::Document
field :metadata, type: Hash
end
post = Post.create!
post.set('metadata.published_at' => Time.now)
post.metadata['published_at'] # => Time instance
post.set('metadata.approved.today' => true)
post.metadata['approved'] # => {'today' => true}
class Flight
include Mongoid::Document
embeds_one :plan
end
class Plan
include Mongoid::Document
embedded_in :flight
field :route, type: String
end
flight = Flight.create!
flight.plan # => nil
flight.set('plan.route', 'test route')
flight.plan # => Plan instance
flight.plan.route # => "test route"

Model#unset

在字段上执行原子 $unset。

person.unset(:name)

请注意,由于这些方法会跳过验证,因此有可能将无效文档保存到数据库中,并最终在应用程序中产生无效文档(由于验证失败,随后无法通过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 提供了多种访问字段值的方法。

注意

当所访问的字段被投影出来时,要么是因为不包含在 only 中,要么是因为包含在 without 中,以下所述的所有访问方法都会引发 Mongoid::Errors::AttributeNotLoaded。这适用于读取和写入。

推荐的方法是使用为每个声明的字段生成的 getter 和 setter 方法:

class Person
include Mongoid::Document
field :first_name
end
person = Person.new
person.first_name = "Artem"
person.first_name
# => "Artem"

要使用此机制,必须显示声明每个字段,或者模型类必须启用动态字段

可以显式定义 getter 和 setter 方法,以在读取或写入字段时提供自定义行为,例如值转换或在不同字段名称下存储值。在这种情况下,可以使用 read_attributewrite_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"}

注意

自定义 setter 是在分配嵌套属性期间调用的,但它们是在建立关联之前调用的。因此,关联在这些方法中可能并不始终可用,我们鼓励在引用关联时检查它们是否存在。回调还可用于对某些事件执行操作,并且已设置好关联,并且其在执行期间可用。

也可以显式使用 read_attributewrite_attribute 方法。请注意,如果字段指定了其存储字段名称,则 read_attributewrite_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_attributewrite_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"

注意

在文档上设置关联不会导致 changeschanged_attributes 哈希值被修改。无论是引用式关联还是嵌入式关联,所有关联都是如此。请注意,更改引用式关联上的 _id 字段确实会导致更改显示在 changeschanged_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?列表:

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

后退

处理数据