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

字段定义

本页内容

  • 字段类型
  • 未指定类型字段
  • 字段类型:StringifiedSymbol
  • 字段类型:Symbol
  • 字段类型:Hash
  • 字段类型:Time
  • 字段类型:Date
  • 字段类型:DateTime
  • 字段类型:Regexp
  • BigDecimal 字段
  • 使用符号或字符串代替类
  • 指定字段默认值
  • 指定存储字段名称
  • 字段别名
  • 保留名称
  • 字段重定义
  • 自定义 ID
  • 不可转换的值
  • 自定义字段行为
  • 自定义获取器和设置器
  • 自定义字段类型
  • 自定义字段选项
  • 动态字段
  • 字段名称中的特殊字符
  • 本地化字段
  • 本地化:present 字段选项
  • 回退
  • 查询
  • 索引
  • 只读属性
  • 时间戳字段
  • 包含点/句点和美元符号的字段名称

MongoDB 使用BSON 类型 存储底层文档数据,Mongoid 在应用运行时将 BSON 类型转换为 Ruby 类型。例如,使用type: :float 定义的字段将在内存中使用 Ruby 的 Float 类,并在数据库中持久化为 BSON 的 double 类型。

字段类型定义了 Mongoid 在构建查询以及从/向数据库检索/写入字段时的行为。具体来说

  1. 在运行时为字段赋值时,值将转换为指定的类型。

  2. 在持久化数据到 MongoDB 时,数据将以适当的类型发送,允许在 MongoDB 或其他工具中进行更丰富的数据操作。

  3. 在查询文档时,在将查询参数发送到 MongoDB 之前,查询参数将被转换为指定的类型。

  4. 在从数据库检索文档时,字段值将被转换为指定的类型。

在模型类中更改字段定义不会更改已存储在 MongoDB 中的数据。要更新现有文档的字段类型或内容,必须将字段重新保存到数据库。请注意,由于 Mongoid 跟踪模型上的哪些属性已更改,并且仅保存更改的属性,因此在更改现有字段的类型而不更改存储值时,可能需要显式写入字段值。

考虑一个简单的类,用于在应用程序中建模一个人。一个人可能有姓名、出生日期和体重。我们可以使用 field 宏在这些属性上定义人。

class Person
include Mongoid::Document
field :name, type: String
field :date_of_birth, type: Date
field :weight, type: Float
end

字段的合法类型如下

Mongoid 也将字符串 "Boolean" 识别为 Mongoid::Boolean 类的别名。

要定义自定义字段类型,请参阅下面的 自定义字段类型

注意

不支持将 BSON::Int64BSON::Int32 类型用作字段类型。将这些类型保存到数据库将按预期工作,但是查询它们将返回本机的 Ruby Integer 类型。在 BSON <=4 中查询 BSON::Decimal128 类型的字段将返回 BSON::Decimal128 类型的值,而在 BSON 5+ 中将返回 BigDecimal 类型的值。

未指定字段类型与指定 Object 类型相同。此类字段为未指定类型

class Product
include Mongoid::Document
field :properties
# Equivalent to:
field :properties, type: Object
end

未指定类型的字段可以存储任何可以直接序列化为 BSON 的值。当字段可能包含不同类型的值(即它是一个变体类型字段)或值类型事先未知时,这非常有用

product = Product.new(properties: "color=white,size=large")
product.properties
# => "color=white,size=large"
product = Product.new(properties: {color: "white", size: "large"})
product.properties
# => {:color=>"white", :size=>"large"}

当将值分配给字段时,Mongoid 仍然执行 mongoization,但使用值的类而不是字段类型进行 mongoization 逻辑。

product = Product.new(properties: 0..10)
product.properties
# The range 0..10, mongoized:
# => {"min"=>0, "max"=>10}

在从数据库读取数据时,Mongoid 不会对未指定类型的字段执行任何类型转换。因此,尽管可以将任何可序列化为 BSON 的值写入未指定类型的字段,但通常需要在数据库读取端进行特殊处理的值在未指定类型的字段中可能无法正确工作。在 Mongoid 支持的字段类型中,以下类型的值不应存储在未指定类型的字段中

  • Date(值将返回为 Time

  • DateTime(值将返回为 Time

  • Range(值将返回为 Hash

《StringifiedSymbol》字段类型是推荐用于存储应向Ruby应用程序公开的值的字段类型。当使用《Symbol》字段类型时,Mongoid默认将值存储为BSON符号。有关BSON符号类型的更多信息,请参阅此处。然而,BSON符号类型已被弃用,在缺乏本地符号类型的编程语言中难以处理,因此《StringifiedSymbol》类型允许使用符号,同时确保与其他驱动程序的互操作性。《StringifiedSymbol》类型将所有数据以字符串形式存储在数据库中,同时将值作为符号公开到应用程序中。

以下是一个示例用法

class Post
include Mongoid::Document
field :status, type: StringifiedSymbol
end
post = Post.new(status: :hello)
# status is stored as "hello" on the database, but returned as a Symbol
post.status
# => :hello
# String values can be assigned also:
post = Post.new(status: "hello")
# status is stored as "hello" on the database, but returned as a Symbol
post.status
# => :hello

所有非字符串值在发送到数据库(通过to_s)时将被转换为字符串,并且所有值在返回到应用程序时都将转换为符号。无法直接转换为符号的值,例如整数和数组,将首先转换为字符串,然后转换为符号后再返回到应用程序。

例如,设置一个整数作为status

post = Post.new(status: 42)
post.status
# => :"42"

如果将《StringifiedSymbol》类型应用于包含BSON符号的字段,则在下次保存时,值将作为字符串而不是BSON符号存储。这允许在数据库中当前存储字符串或BSON符号的字段透明地延迟迁移到《StringifiedSymbol》字段类型。

新应用程序应使用StringifiedSymbol字段类型将Ruby符号存储在数据库中。《StringifiedSymbol》字段类型与其他应用程序和编程语言的兼容性最佳,并且在所有情况下都具有相同的行为。

Mongoid还提供了已弃用的《Symbol》字段类型,用于将Ruby符号序列化为BSON符号。由于BSON规范已弃用BSON符号类型,当单独使用时,`bson` gem将Ruby符号序列化为BSON字符串。然而,为了与较旧的数据库保持向后兼容性,`mongo` gem覆盖了此行为,将Ruby符号序列化为BSON符号。这是为了能够指定包含BSON符号作为字段的文档的查询。

要覆盖默认行为并配置`mongo` gem(从而也配置Mongoid),以将符号值编码为字符串,请在您的项目中包含以下代码片段

class Symbol
def bson_type
BSON::String::BSON_TYPE
end
end

当使用类型为哈希的字段时,请注意遵守mongoDB的合法键名,否则值将无法正确存储。

class Person
include Mongoid::Document
field :first_name
field :url, type: Hash
# will update the fields properly and save the values
def set_vals
self.first_name = 'Daniel'
self.url = {'home_page' => 'http://www.homepage.com'}
save
end
# all data will fail to save due to the illegal hash key
def set_vals_fail
self.first_name = 'Daniel'
self.url = {'home.page' => 'http://www.homepage.com'}
save
end
end

时间字段将值存储为配置时区中的时间实例。配置时区.

日期DateTime实例分配给时间字段时,将转换为时间实例

class Voter
include Mongoid::Document
field :registered_at, type: Time
end
Voter.new(registered_at: Date.today)
# => #<Voter _id: 5fdd80392c97a618f07ba344, registered_at: 2020-12-18 05:00:00 UTC>

在上面的示例中,该值被解释为当地时间的今天开始,因为应用程序未配置为使用UTC时间。

注意

当数据库包含时间字段的字符串值时,Mongoid使用Time.parse解析字符串值,该解析器将没有时区的值视为本地时间。

Mongoid允许将多种类型的值分配给日期字段

  • 日期 - 提供的日期将按原样存储。

  • 时间DateTimeActiveSupport::TimeWithZone - 从值的时区中提取日期组件。

  • 字符串 - 使用字符串中指定的日期。

  • 整数浮点数 - 值被视为UTC时间戳,并将其转换为配置的时间区域(请注意,Mongoid.use_utc 对此转换没有影响),然后从结果时间中取日期。

换句话说,如果值中指定了日期,则直接使用该日期,而无需首先将值转换为配置的时间区域。

由于日期与日期时间转换是有损的(它丢弃了时间组件),特别是如果应用程序在不同的时区中使用时间,建议在将值赋给类型为Date的字段之前,显式地将StringTimeDateTime对象转换为Date对象。

注意

当数据库包含一个Date字段的字符串值时,Mongoid使用Time.parse解析字符串值,丢弃结果Time对象的时间部分并使用日期部分。Time.parse将没有时区的值视为本地时间。

MongoDB以UTC时间戳存储所有时间。在将值分配给DateTime字段或查询DateTime字段时,Mongoid在将其发送到MongoDB服务器之前将传入的值转换为UTC Time

TimeActiveSupport::TimeWithZoneDateTime对象包含时区信息,而持久化的值是UTC中指定的时刻。当检索值时,返回的时间区域由配置的时区设置定义。

class Ticket
include Mongoid::Document
field :opened_at, type: DateTime
end
Time.zone = 'Berlin'
ticket = Ticket.create!(opened_at: '2018-02-18 07:00:08 -0500')
ticket.opened_at
# => Sun, 18 Feb 2018 13:00:08 +0100
ticket
# => #<Ticket _id: 5c13d4b9026d7c4e7870bb2f, opened_at: 2018-02-18 12:00:08 UTC>
Time.zone = 'America/New_York'
ticket.opened_at
# => Sun, 18 Feb 2018 07:00:08 -0500
Mongoid.use_utc = true
ticket.opened_at
# => Sun, 18 Feb 2018 12:00:08 +0000

Mongoid还支持将整数和浮点数转换为DateTime。在这样做的时候,假设整数/浮点数是Unix时间戳(UTC)。

ticket.opened_at = 1544803974
ticket.opened_at
# => Fri, 14 Dec 2018 16:12:54 +0000

如果将字符串用作DateTime字段值,则行为取决于字符串是否包含时区。如果没有指定时区,则使用默认Mongoid时区

Time.zone = 'America/New_York'
ticket.opened_at = 'Mar 4, 2018 10:00:00'
ticket.opened_at
# => Sun, 04 Mar 2018 15:00:00 +0000

如果指定了时区,则将其保留。

ticket.opened_at = 'Mar 4, 2018 10:00:00 +01:00'
ticket.opened_at
# => Sun, 04 Mar 2018 09:00:00 +0000

注意

当数据库包含一个 DateTime 字段的字符串值时,Mongoid 会使用 Time.parse 来解析字符串值,将没有时区的值视为本地时间。

MongoDB 支持在文档中存储正则表达式,并使用正则表达式进行查询。请注意,MongoDB 使用 Perl 兼容的正则表达式 (PCRE),而 Ruby 使用 Onigmo,它是 Oniguruma 正则表达式引擎 的分支。这两个正则表达式实现通常提供等效的功能,但存在几个重要的语法差异。

当字段声明为类型Regexp时,Mongoid会将Ruby正则表达式转换为BSON正则表达式,并将结果存储在MongoDB中。从数据库中检索字段会产生一个BSON::Regexp::Raw实例

class Token
include Mongoid::Document
field :pattern, type: Regexp
end
token = Token.create!(pattern: /hello.world/m)
token.pattern
# => /hello.world/m
token.reload
token.pattern
# => #<BSON::Regexp::Raw:0x0000555f505e4a20 @pattern="hello.world", @options="ms">

使用#compile方法在BSON::Regexp::Raw上,可以获取回Ruby正则表达式

token.pattern.compile
# => /hello.world/m

请注意,如果原始的正则表达式不是Ruby的,对它调用#compile可能会产生不同的正则表达式。例如,以下是一个PCRE匹配以"hello"结尾的字符串

BSON::Regexp::Raw.new('hello$', 's')
# => #<BSON::Regexp::Raw:0x0000555f51441640 @pattern="hello$", @options="s">

编译这个正则表达式会产生一个Ruby正则表达式,它除了匹配以"hello"结尾的字符串外,还匹配在新行之前包含"hello"的字符串

BSON::Regexp::Raw.new('hello$', 's').compile =~ "hello\nworld"
# => 0

这是因为PCRE和Ruby正则表达式之间$的含义不同。

BigDecimal字段类型用于存储精度更高的数字。

BigDecimal字段类型根据全局配置选项Mongoid.map_big_decimal_to_decimal128的值,在数据库中以两种不同的方式存储其值。如果此标志设置为false(默认值),则BigDecimal字段将作为字符串存储,否则将作为BSON::Decimal128存储。

BigDecimal字段类型在转换为BSON::Decimal128和从BSON::Decimal128转换时有某些限制

  • BSON::Decimal128的范围和精度有限,而BigDecimal在范围和精度方面没有限制。BSON::Decimal128的最大值约为10^6145,最小值约为-10^6145,并且有最多34位精度。当尝试存储不适合BSON::Decimal128的值时,建议将它们存储为字符串而不是BSON::Decimal128。可以通过将Mongoid.map_big_decimal_to_decimal128设置为false来实现。如果尝试将不适合BSON::Decimal128的值存储为BSON::Decimal128,则会引发错误。

  • BSON::Decimal128 能够接受有符号的 NaN 值,而 BigDecimal 则不行。当使用 BigDecimal 字段类型从数据库检索有符号的 NaN 值时,NaN 将是无符号的。

  • BSON::Decimal128 在数据库中存储时保留尾部零。然而,BigDecimal 则不保留尾部零,因此使用 BigDecimal 字段类型检索 BSON::Decimal128 值可能会导致精度损失。

当在无类型字段(即动态类型字段)中存储 BigDecimalMongoid.map_big_decimal_to_decimal128false 时,存在一个额外的注意事项。在这种情况下,BigDecimal 被存储为字符串,并且由于使用了动态字段,使用 BigDecimal 查询该字段将找不到该 BigDecimal 的字符串,因为查询正在寻找 BigDecimal。为了查询该字符串,必须首先使用 to_sBigDecimal 转换为字符串。请注意,当字段类型为 BigDecimal 时,这不是问题。

如果您希望完全不使用 BigDecimal,可以将字段类型设置为 BSON::Decimal128。这将允许您跟踪尾部零和有符号的 NaN 值。

在 Mongoid 的下一个主要版本中,全局配置选项 Mongoid.map_big_decimal_to_decimal128 将默认为 true。当此标志开启时,查询中的 BigDecimal 值将不会与数据库中已存储的字符串匹配;它们仅会匹配数据库中的 decimal128 值。如果您有一个由字符串支持的 BigDecimal 字段,您有三个选择

  1. 可以将全局配置选项 Mongoid.map_big_decimal_to_decimal128 设置为 false,并继续将您的 BigDecimal 值作为字符串存储。请注意,您将放弃将 BigDecimal 值存储为 decimal128 的优势,例如根据字段数值进行查询和聚合。

  2. 可以将全局配置选项 Mongoid.map_big_decimal_to_decimal128 设置为 true,并将该字段的所有值从字符串转换为数据库中的 decimal128 值。您应该在将全局配置选项设置为 true 之前执行此转换。以下是一个示例查询来完成此操作

    db.bands.updateMany({
    "field": { "$exists": true }
    }, [
    {
    "$set": {
    "field": { "$toDecimal": "$field" }
    }
    }
    ])

    此查询将更新所有包含给定字段的文档,将该字段设置为对应的 decimal128 值。请注意,此查询只能在 MongoDB 4.2+ 中工作。

  3. 可以将全局配置选项 Mongoid.map_big_decimal_to_decimal128 设置为 true,并且该字段可以同时具有字符串和 decimal128 值。这样,数据库中只插入和更新 decimal128 值。请注意,您仍然无法完全利用仅使用 decimal128 值的优势,但您的数据集正在缓慢迁移到所有 decimal128 值,因为旧字符串值被更新为 decimal128,而新的 decimal128 值被添加。使用此设置,您仍然可以按如下方式查询 BigDecimal

    Mongoid.map_big_decimal_to_decimal128 = true
    big_decimal = BigDecimal('2E9')
    Band.in(sales: [big_decimal, big_decimal.to_s]).to_a

    此查询将找到所有既为 decimal128 值又为匹配该值的字符串的值。

Mongoid 允许使用符号或字符串代替类来指定字段的类型,例如

class Order
include Mongoid::Document
field :state, type: :integer
# Equivalent to:
field :state, type: "integer"
# Equivalent to:
field :state, type: Integer
end

以下列出的标准字段类型可以使用符号或字符串以这种方式指定。Mongoid 识别以下扩展

  • :array => Array

  • :big_decimal => BigDecimal

  • :binary => BSON::Binary

  • :boolean => Mongoid::Boolean

  • :date => Date

  • :date_time => DateTime

  • :float => Float

  • :hash => Hash

  • :integer => Integer

  • :object_id => BSON::ObjectId

  • :range => Range

  • :regexp => Regexp

  • :set => Set

  • :string => String

  • :stringified_symbol => StringifiedSymbol

  • :symbol => Symbol

  • :time => Time

字段可以配置为具有默认值。默认值可以是固定的,如下例所示

class Order
include Mongoid::Document
field :state, type: String, default: 'created'
end

默认值也可以指定为 Proc

class Order
include Mongoid::Document
field :fulfill_by, type: Time, default: ->{ Time.now + 3.days }
end

注意

不是 Proc 实例的默认值在类加载时评估,这意味着以下两个定义是不等效的

field :submitted_at, type: Time, default: Time.now
field :submitted_at, type: Time, default: ->{ Time.now }

第二个定义可能是期望的,它将提交时间设置为文档实例创建时的当前时间。

要设置依赖于文档状态的默认值,请在 Proc 实例中使用 self,这将评估为正在操作的文档实例

field :fulfill_by, type: Time, default: ->{
# Order should be fulfilled in 2 business hours.
if (7..8).include?(self.submitted_at.hour)
self.submitted_at + 4.hours
elsif (9..3).include?(self.submitted_at.hour)
self.submitted_at + 2.hours
else
(self.submitted_at + 1.day).change(hour: 11)
end
}

当将默认值定义为 Proc 时,Mongoid 将在所有其他属性设置和关联初始化之后应用默认值。要使默认值在其他属性设置之前应用,请使用 pre_processed: true 字段选项

field :fulfill_by, type: Time, default: ->{ Time.now + 3.days },
pre_processed: true

当通过 Proc 指定 _id 字段的自定义默认值时,也需要使用 pre_processed: true 选项,以确保通过关联正确设置 _id

field :_id, type: String, default: -> { 'hello' }, pre_processed: true

无模式数据库的一个缺点是MongoDB必须将所有字段信息与每个文档一起存储,这意味着它会在RAM和磁盘上占用大量的存储空间。限制这种做法的常见模式是将字段别名设置为少量字符,同时保持应用域的表达性。Mongoid允许您这样做,并通过getter、setter和准则中的长名称引用领域中的字段,同时为您执行转换。

class Band
include Mongoid::Document
field :n, as: :name, type: String
end
band = Band.new(name: "Placebo")
band.attributes # { "n" => "Placebo" }
criteria = Band.where(name: "Placebo")
criteria.selector # { "n" => "Placebo" }

可以定义字段别名。值将存储在目标字段中,但可以从目标字段或别名字段中访问。

class Band
include Mongoid::Document
field :name, type: String
alias_attribute :n, :name
end
band = Band.new(n: 'Astral Projection')
# => #<Band _id: 5fc1c1ee2c97a64accbeb5e1, name: "Astral Projection">
band.attributes
# => {"_id"=>BSON::ObjectId('5fc1c1ee2c97a64accbeb5e1'), "name"=>"Astral Projection"}
band.n
# => "Astral Projection"

可以使用unalias_attribute方法从模型类中删除别名。

class Band
unalias_attribute :n
end

可以使用unalias_attribute移除预定义的id别名。这对于在id_id字段中存储不同的值非常有用。

class Band
include Mongoid::Document
unalias_attribute :id
field :id, type: String
end
Band.new(id: '42')
# => #<Band _id: 5fc1c3f42c97a6590684046c, id: "42">

尝试在Mongoid文档上定义一个与保留方法名称冲突的字段将引发错误。可以通过调用Mongoid.destructive_fields方法来获取保留名称列表。

默认情况下,Mongoid允许在模型上重定义字段。要当字段重定义时引发错误,请将duplicate_fields_exception配置选项设置为true

将选项设置为true时,以下示例将引发错误

class Person
include Mongoid::Document
field :name
field :name, type: String
end

要定义字段,请使用overwrite: true选项

class Person
include Mongoid::Document
field :name
field :name, type: String, overwrite: true
end

默认情况下,Mongoid在文档上定义的_id字段包含由Mongoid自动生成的BSON::ObjectId值。

可以替换_id字段定义以更改_id值的类型或使用不同的默认值

class Band
include Mongoid::Document
field :name, type: String
field :_id, type: String, default: ->{ name }
end

也可以完全省略默认值

class Band
include Mongoid::Document
field :_id, type: String
end

如果省略了默认的 _id,并且您的应用程序没有提供 _id 值,Mongoid 将不包含 _id 值来持久化文档。在这种情况下,如果文档是顶级文档,服务器将分配一个 _id 值;如果文档是嵌入文档,则不会分配 _id 值。Mongoid 不会在文档持久化时自动检索此值(如果已分配),您必须使用其他方法获取持久化值(以及完整的持久化文档)。

band = Band.create!
=> #<Band _id: , >
band.id
=> nil
band.reload
# raises Mongoid::Errors::DocumentNotFound
Band.last
=> #<Band _id: 5fc681c22c97a6791f324b99, >

在嵌入文档中省略 _id 字段更为常见。

Mongoid 还定义了与 _id 同义的 id 字段。如果需要(例如,与使用 id 字段存储与 _id 不同的值的系统集成),可以删除 id 别名。

在 Mongoid 8 中,Mongoid 标准化了“不可转换”值的赋值和读取处理。当值不能被强制转换为字段的类型时,它被认为是“不可转换”的。例如,数组对于整数字段就是“不可转换”的值。

不可转换值的赋值已被标准化为默认赋值 nil。考虑以下示例

class User
include Mongoid::Document
field :name, type: Integer
end
User.new(name: [ "hello" ])

将数组赋值给整数类型的字段不会起作用,因为数组不能被强制转换为整数。将不可转换的值赋给字段将导致写入 nil

user = User.new(name: [ "Mike", "Trout" ])
# => #<User _id: 62b222d43282a47bf73e3264, name: nil>

注意,原始的不可转换的值将存储在带有其字段名称的 attributes_before_type_cast 哈希中

user.attributes_before_type_cast["name"]
# => ["Mike", "Trout"]

注意

注意,对于数值字段,任何定义了 to_i 用于整数字段,to_f 用于浮点数,以及 to_d 用于大数的类都是可转换的。字符串是例外,并且只有在字符串是数字时才会调用相应的 to_* 方法。如果一个类只定义了 to_i 而没有 to_f 并且被分配给浮点字段,这是不可转换的,Mongoid 不会执行两步转换(即 to_i 然后是 to_f)。

当数据库中的文档包含与 Mongoid 中表示不同的类型值时,如果 Mongoid 无法将它们强制转换为正确的类型,它将用 nil 替换该值。考虑以下模型和数据库中的文档

class User
include Mongoid::Document
field :name, type: Integer
end
{ _id: ..., name: [ "Mike", "Trout" ] }

从数据库中读取此文档将导致模型的名称字段包含 nil

User.first.name
# => nil

类型为数组的数据库值不能存储在属性中,因为数组不能被强制转换为整数。注意,原始不可转换的值将存储在带有其字段名称的 attributes_before_type_cast 哈希中

user.attributes_before_type_cast["name"]
# => ["Mike", "Trout"]

注意

容器对象(例如 Hash、Array)上的 demongoize 方法尚未更改,以允许自动持久化容器属性的变更。有关此主题的更深入讨论,请参阅 MONGOID-2951

Mongoid 提供了多种自定义字段行为的方式。

您可以通过覆盖获取器和设置器来修改字段在访问或写入时的值。获取器和设置器使用与字段相同的名称。在获取器和设置器内部使用 read_attributewrite_attribute 方法来操作原始属性值。

例如,Mongoid 提供了 :default 字段选项来将默认值写入字段。如果您想在应用程序中设置字段的默认值但又不想持久化它,可以按如下方式覆盖获取器

class DistanceMeasurement
include Mongoid::Document
field :value, type: Float
field :unit, type: String
def unit
read_attribute(:unit) || "m"
end
def to_s
"#{value} #{unit}"
end
end
measurement = DistanceMeasurement.new(value: 2)
measurement.to_s
# => "2.0 m"
measurement.attributes
# => {"_id"=>BSON::ObjectId('613fa0b0a15d5d61502f3447'), "value"=>2.0}

再举一个例子,一个将空字符串转换为 nil 值的字段可以按以下方式实现

class DistanceMeasurement
include Mongoid::Document
field :value, type: Float
field :unit, type: String
def unit=(value)
if value.blank?
value = nil
end
write_attribute(:unit, value)
end
end
measurement = DistanceMeasurement.new(value: 2, unit: "")
measurement.attributes
# => {"_id"=>BSON::ObjectId('613fa15aa15d5d617216104c'), "value"=>2.0, "unit"=>nil}

您可以在 Mongoid 中定义自定义类型,并确定它们的序列化和反序列化方式。在这个例子中,我们定义了一个新的字段类型 Point,我们可以在模型类中如下使用它

class Profile
include Mongoid::Document
field :location, type: Point
end

然后创建一个 Ruby 类来表示这个类型。这个类必须定义用于 MongoDB 序列化和反序列化的方法,如下所示

class Point
attr_reader :x, :y
def initialize(x, y)
@x, @y = x, y
end
# Converts an object of this instance into a database friendly value.
# In this example, we store the values in the database as array.
def mongoize
[ x, y ]
end
class << self
# Takes any possible object and converts it to how it would be
# stored in the database.
def mongoize(object)
case object
when Point then object.mongoize
when Hash then Point.new(object[:x], object[:y]).mongoize
else object
end
end
# Get the object as it was stored in the database, and instantiate
# this custom class from it.
def demongoize(object)
Point.new(object[0], object[1])
end
# Converts the object that was supplied to a criteria and converts it
# into a query-friendly form.
def evolve(object)
case object
when Point then object.mongoize
else object
end
end
end
end

实例方法 mongoize 接收您自定义类型对象的实例,并将其转换为数据库中将存储的表示形式,即传递给 MongoDB Ruby 驱动程序。在我们上面的例子中,我们希望将我们的 Point 对象存储为一个数组 [ x, y ]

类方法 mongoize 与实例方法类似,但它必须处理所有可能的输入类型。当调用自定义类型字段的设置方法时使用 mongoize 方法。

point = Point.new(12, 24)
venue = Venue.new(location: point) # This uses the Point#mongoize instance method.
venue = Venue.new(location: [ 12, 24 ]) # This uses the Point.mongoize class method.

类方法 demongoizemongoize 的逆操作。它接收来自 MongoDB Ruby 驱动程序的原始对象并将其转换为您的自定义类型实例。在这种情况下,数据库驱动程序返回一个数组,并从它中实例化一个 Point。当调用自定义类型字段的获取器时使用 demongoize 方法。请注意,在上面的例子中,由于 demongoize 调用了 Point.new,每次调用获取器都会生成一个 Point 的新实例。

Mongoid 总是会在从数据库检索的值上调用 demongoize 方法,但应用程序理论上可以在 demongoize 方法中使用任意输入。建议应用程序在其 demongoize 方法中添加对任意输入的处理。我们可以将 Pointdemongoize 方法重写如下

def demongoize(object)
if object.is_a?(Array) && object.length == 2
Point.new(object[0], object[1])
end
end

请注意,demongoize 只会在给定的数组长度为 2 时创建一个新的 Point,否则将返回 nilmongoizedemongoize 方法都应该准备好接受任意输入,并且在无法将值转换为自定义类型时返回 nil。有关详细信息,请参阅无法转换的值部分。

最后,类方法 evolvemongoize 类似,但它用于在 Mongoid 查询条件中使用对象进行转换。

point = Point.new(12, 24)
Venue.where(location: point) # This uses Point.evolve

evolve 方法也应准备好接受任意输入,但是与 mongoizedemongoize 方法不同,它在无法将值转换为自定义类型时应返回输入的值。有关详细信息,请参阅无法转换的值部分。

自定义字段类型可能会在用户可见的属性值与声明的字段类型不同时,将用户可见的属性值转换为存储在数据库中的值。例如,这可以用于实现一个枚举到另一个枚举的映射,以便在应用程序中具有更描述性的值,而在数据库中存储更紧凑的值。

class ColorMapping
MAPPING = {
'black' => 0,
'white' => 1,
}.freeze
INVERSE_MAPPING = MAPPING.invert.freeze
class << self
# Takes application-scope value and converts it to how it would be
# stored in the database. Converts invalid values to nil.
def mongoize(object)
MAPPING[object]
end
# Get the value as it was stored in the database, and convert to
# application-scope value. Converts invalid values to nil.
def demongoize(object)
INVERSE_MAPPING[object]
end
# Converts the object that was supplied to a criteria and converts it
# into a query-friendly form. Returns invalid values as is.
def evolve(object)
MAPPING.fetch(object, object)
end
end
end
class Profile
include Mongoid::Document
field :color, type: ColorMapping
end
profile = Profile.new(color: 'white')
profile.color
# => "white"
# Writes 0 to color field
profile.save!

您可以在模型类加载时定义用于扩展其行为的 field 宏函数的自定义选项。

例如,我们将定义一个 :max_length 选项,它将为字段添加一个长度验证器。首先,在一个初始化器中声明新的字段选项,指定其处理函数为一个代码块

# in /config/initializers/mongoid_custom_fields.rb
Mongoid::Fields.option :max_length do |model, field, value|
model.validates_length_of field.name, maximum: value
end

然后在您的模型类中使用它

class Person
include Mongoid::Document
field :name, type: String, max_length: 10
end

请注意,每当在字段定义中使用选项时,处理函数将被调用,即使选项的值为false或nil。

默认情况下,Mongoid要求显式使用field声明定义所有可能在文档上设置的字段。Mongoid还支持从任意散列或数据库中存储的文档动态创建字段。当模型使用未显式定义的字段时,这些字段被称为动态字段

要启用动态字段,请在模型中包含Mongoid::Attributes::Dynamic模块。

class Person
include Mongoid::Document
include Mongoid::Attributes::Dynamic
end
bob = Person.new(name: 'Bob', age: 42)
bob.name
# => "Bob"

可以在同一模型类中使用field声明和动态字段。有field声明的属性将根据field声明进行处理,其余属性将被视为动态字段。

动态字段中的属性值最初必须通过将属性哈希传递给构造函数、通过attributes=进行批量赋值、通过[]=进行批量赋值、使用write_attribute或它们必须已经在数据库中存在来设置。

# OK
bob = Person.new(name: 'Bob')
# OK
bob = Person.new
bob.attributes = {age: 42}
# OK
bob = Person.new
bob['age'] = 42
# Raises NoMethodError: undefined method age=
bob = Person.new
bob.age = 42
# OK
bob = Person.new
# OK - string access
bob.write_attribute('age', 42)
# OK - symbol access
bob.write_attribute(:name, 'Bob')
# OK, initializes attributes from whatever is in the database
bob = Person.find('123')

如果某个属性在特定模型实例的属性哈希中不存在,则对应字段的读取器和写入器均未定义,调用它们将引发NoMethodError

bob = Person.new
bob.attributes = {age: 42}
bob.age
# => 42
# raises NoMethodError
bob.name
# raises NoMethodError
bob.name = 'Bob'
# OK
bob['name'] = 'Bob'
bob.name
# => "Bob"

可以使用批量属性访问或read_attribute(这也适用于不使用动态字段的模型)来始终读取属性。

bob = Person.new(age: 42)
# OK - string access
bob['name']
# => nil
# OK - symbol access
bob[:name]
# => nil
# OK - string access
bob['age']
# => 42
# OK - symbol access
bob[:age]
# => 42
# OK
bob.attributes['name']
# => nil
# OK
bob.attributes['age']
# => 42
# Returns nil - keys are always strings
bob.attributes[:age]
# => nil
# OK
bob.read_attribute('name')
# => nil
# OK
bob.read_attribute(:name)
# => nil
# OK - string access
bob.read_attribute('age')
# => 42
# OK - symbol access
bob.read_attribute(:age)
# => 42

注意

read_attribute方法返回的值以及存储在attributes哈希中的值是mongoized值。

Mongoid 允许字段名称动态包含空格和标点符号

bob = Person.new('hello world' => 'MDB')
bob.send('hello world')
# => "MDB"
bob.write_attribute("hello%world", 'MDB')
bob[:"hello%world"]
# => "MDB"

Mongoid 支持通过 I18n 钥匙 本地化字段。

class Product
include Mongoid::Document
field :description, type: String, localize: true
end

通过告诉字段 localize,Mongoid 会将字段作为本地/值对的哈希表存储,但正常的访问方式将像字符串一样。

I18n.default_locale = :en
product = Product.new
product.description = "Marvelous!"
I18n.locale = :de
product.description = "Fantastisch!"
product.attributes
# { "description" => { "en" => "Marvelous!", "de" => "Fantastisch!" }

您可以使用相应的 _translations 方法一次性获取和设置所有翻译。

product.description_translations
# { "en" => "Marvelous!", "de" => "Fantastisch!" }
product.description_translations =
{ "en" => "Marvelous!", "de" => "Wunderbar!" }

本地化字段可以与任何字段类型一起使用。例如,它们可以与浮点字段一起使用,以处理货币的差异。

class Product
include Mongoid::Document
field :price, type: Float, localize: true
field :currency, type: String, localize: true
end

通过这种方式创建模型,我们可以将价格与货币类型分开,这允许您在查询或聚合该字段时使用所有与数字相关的功能(前提是您索引到存储的翻译哈希表)。我们可以按如下方式创建该模型的实例

product = Product.new
I18n.locale = :en
product.price = 1.00
product.currency = "$"
I18n.locale = :he
product.price = 3.24
product.currency = "₪"
product.attributes
# => { "price" => { "en" => 1.0, "he" => 3.24 }, "currency" => { "en" => "$", "he" => "₪" } }

Mongoid 支持在创建本地化字段时使用 :present 选项

class Product
include Mongoid::Document
field :description, localize: :present
end

此选项会自动从 _translations 哈希中移除空白值(即那些返回 blank? 方法的 true 的值)

I18n.default_locale = :en
product = Product.new
product.description = "Marvelous!"
I18n.locale = :de
product.description = "Fantastisch!"
product.description_translations
# { "en" => "Marvelous!", "de" => "Fantastisch!" }
product.description = ""
product.description_translations
# { "en" => "Marvelous!" }

当为 :de 区域设置写入空字符串时,将移除 "de" 键从 _translations 哈希中,而不是写入空字符串。

Mongoid 与 i18n 回退 集成。要使用回退,必须显式启用相应的功能。

在 Rails 应用程序中,将环境中的 config.i18n.fallbacks 配置设置设置为 true,并指定后备语言。

config.i18n.fallbacks = true
config.after_initialize do
I18n.fallbacks[:de] = [ :en, :es ]
end

在非 Rails 应用程序中,将后备模块包含到您使用的 I18n 后端中,并指定后备语言。

require "i18n/backend/fallbacks"
I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
I18n.fallbacks[:de] = [ :en, :es ]

启用后备功能后,如果活动语言中没有翻译,则会从后备语言中查找翻译。

product = Product.new
I18n.locale = :en
product.description = "Marvelous!"
I18n.locale = :de
product.description # "Marvelous!"

Mongoid 还在字段上定义了 :fallbacks 选项,可用于在特定字段上禁用后备功能。

class Product
include Mongoid::Document
field :description, type: String, localize: true, fallbacks: false
end
product = Product.new
I18n.locale = :en
product.description = "Marvelous!"
I18n.locale = :de
product.description # nil

请注意,此选项默认为 true

注意

在 i18n 1.1 中,后备的行为已更改,[链接](https://github.com/ruby-i18n/i18n/pull/415) 要求始终显式列出后备区域设置,而不是在没有提供后备区域设置时回退到默认区域设置。

当使用 Mongoid 的查询 API 查询本地化字段时,Mongoid 会自动修改条件以匹配当前区域设置。

# Match all products with Marvelous as the description. Locale is en.
Product.where(description: "Marvelous!")
# The resulting MongoDB query filter: { "description.en" : "Marvelous!" }

如果您计划在本地化字段上进行大量查询,您应该对计划搜索的每个区域进行索引。

class Product
include Mongoid::Document
field :description, localize: true
index "description.de" => 1
index "description.en" => 1
end

您可以使用Mongoid将某些属性指定为只读。这将允许使用这些属性创建文档,但在使用如update_attributes之类的批量更新方法时,将忽略对这些属性的变化。

class Band
include Mongoid::Document
field :name, type: String
field :origin, type: String
attr_readonly :name, :origin
end
band = Band.create(name: "Placebo")
band.update_attributes(name: "Tool") # Filters out the name change.

如果您尝试单独更新或删除只读属性,将引发ReadonlyAttribute异常。

band.update_attribute(:name, "Tool") # Raises the error.
band.remove_attribute(:name) # Raises the error.

使用setter对只读属性进行赋值将被忽略。

b = Band.create!(name: "The Rolling Stones")
# => #<Band _id: 6287a3d5d1327a5292535383, name: "The Rolling Stones", origin: nil>
b.name = "The Smashing Pumpkins"
# => "The Smashing Pumpkins"
b.name
# => "The Rolling Stones"

对原子持久性运算符的调用,如bitinc,将持久化只读字段的变化。

Mongoid在Mongoid::Timestamps中提供了一个时间戳模块,可以将其包含在内以获取对created_atupdated_at字段的基本行为。

class Person
include Mongoid::Document
include Mongoid::Timestamps
end

您还可以选择只为创建或修改保留特定的时间戳。

class Person
include Mongoid::Document
include Mongoid::Timestamps::Created
end
class Post
include Mongoid::Document
include Mongoid::Timestamps::Updated
end

如果您想关闭特定调用的时间戳,请使用无时间方法。

person.timeless.save
Person.timeless.create!

如果您想使用带有别名的更短时间戳字段来节省空间,您可以包含模块的简短版本。

class Band
include Mongoid::Document
include Mongoid::Timestamps::Short # For c_at and u_at.
end
class Band
include Mongoid::Document
include Mongoid::Timestamps::Created::Short # For c_at only.
end
class Band
include Mongoid::Document
include Mongoid::Timestamps::Updated::Short # For u_at only.
end

在字段名中使用点/句点(.)以及以美元符号($)开头不建议,因为Mongoid对这些字段中存储的文档的检索和操作提供有限的支持。

Mongoid和MongoDB查询语言(MQL)通常使用点/句点字符(.)来分隔字段路径中的字段名,并使用以美元符号($)开头的单词作为运算符。MongoDB为使用包含点和以美元符号开头的字段名提供有限支持,以与其他软件兼容,然而,由于这种支持仅限于特定运算符(例如getFieldsetField)并且需要使用聚合管道进行查询和更新,因此,应用程序应尽量避免在字段名中使用点,并尽可能避免以美元符号开头。

从版本8开始,Mongoid允许用户访问以美元符号开头且包含点/句点的字段。它们可以通过以下方式使用send方法访问

class User
include Mongoid::Document
field :"first.last", type: String
field :"$_amount", type: Integer
end
user = User.first
user.send(:"first.last")
# => Mike.Trout
user.send(:"$_amount")
# => 42650000

也可以使用read_attribute来访问这些字段

user.read_attribute("first.last")
# => Mike.Trout

由于服务器限制,更新和替换包含点和美元符号的字段需要使用特殊运算符。因此,在这些字段上调用设置器是禁止的,并将引发错误

class User
include Mongoid::Document
field :"first.last", type: String
field :"$_amount", type: Integer
end
user = User.new
user.send(:"first.last=", "Shohei.Ohtani")
# raises a InvalidDotDollarAssignment error
user.send(:"$_amount=", 8500000)
# raises a InvalidDotDollarAssignment error

返回

模式配置