文档菜单
文档首页
/ / /
Mongoid
/

CRUD 操作

本页内容

  • 保存文档
  • 标准
  • 原子操作
  • 重新加载
  • 重新加载未保存的文档
  • 访问字段值
  • 获取器 & 设置器
  • 自定义获取器 & 设置器
  • read_attribute & write_attribute
  • 哈希访问
  • 批量属性写入
  • 脏数据跟踪
  • 查看变更
  • 重置变更
  • 持久化
  • 查看之前变更
  • 更新容器字段
  • 只读文档
  • 重写 readonly?

Mongoid 支持所有预期的 CRUD 操作,对于那些熟悉其他 Ruby 映射器(如 Active Record 或 Data Mapper)的人来说。Mongoid 与其他 MongoDB 映射器的区别在于,通用持久化操作仅对已更改的字段执行原子更新,而不是每次都写入整个文档到数据库。

持久化部分将提供执行文档中记录的命令时执行的数据库操作的示例。

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

操作
示例

Model#attributes

以字符串键和以 Mongoized 形式(即它们在数据库中存储的方式)的值返回文档的属性作为“Hash”。

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

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 的文档。

如果遇到任何验证错误,则相应的文档不会被插入,而是与已插入的文档一起返回。使用 persisted? new_record? 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 关联上深度设置值。如果在更新之前此类关联的文档为 nil,则将创建一个。

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 开始提供了 多文档事务,它提供了跨多个文档的原子持久化。

使用 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。这既适用于读取也适用于写入。

推荐的做法是使用为每个声明字段生成的获取器和设置器方法。

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

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

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

注意

自定义设置器在分配嵌套属性时被调用,但是它们在设置关联之前被调用。因此,在这些方法中,关联可能不一定总是可用,并且在引用它们时鼓励进行检查。也可以使用回调来执行某些事件的操作,而在回调执行期间,关联已经设置并可用。

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"

注意

在文档上设置关联不会导致更改已更改的属性散列被修改。这适用于所有关联,无论是引用的还是内嵌的。请注意,更改引用关联的_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? 方法

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

返回

与数据交互

© . All rights reserved.