字段定义
本页内容
字段类型
MongoDB 使用BSON 类型 存储底层文档数据,Mongoid 在应用运行时将 BSON 类型转换为 Ruby 类型。例如,使用type: :float
定义的字段将在内存中使用 Ruby 的 Float
类,并在数据库中持久化为 BSON 的 double
类型。
字段类型定义了 Mongoid 在构建查询以及从/向数据库检索/写入字段时的行为。具体来说
在运行时为字段赋值时,值将转换为指定的类型。
在持久化数据到 MongoDB 时,数据将以适当的类型发送,允许在 MongoDB 或其他工具中进行更丰富的数据操作。
在查询文档时,在将查询参数发送到 MongoDB 之前,查询参数将被转换为指定的类型。
在从数据库检索文档时,字段值将被转换为指定的类型。
在模型类中更改字段定义不会更改已存储在 MongoDB 中的数据。要更新现有文档的字段类型或内容,必须将字段重新保存到数据库。请注意,由于 Mongoid 跟踪模型上的哪些属性已更改,并且仅保存更改的属性,因此在更改现有字段的类型而不更改存储值时,可能需要显式写入字段值。
考虑一个简单的类,用于在应用程序中建模一个人。一个人可能有姓名、出生日期和体重。我们可以使用 field
宏在这些属性上定义人。
class Person include Mongoid::Document field :name, type: String field :date_of_birth, type: Date field :weight, type: Float end
字段的合法类型如下
数组
BSON::Binary
Mongoid::Boolean
,在包含Mongoid::Document
的类的作用域内可以简单地指定为Boolean
。Float
整数
BSON::ObjectId
范围
集合
字符串
Mongoid::StringifiedSymbol,在包含
Mongoid::Document
的类的作用域内可以简单地指定为StringifiedSymbol
。ActiveSupport::TimeWithZone
Mongoid 也将字符串 "Boolean"
识别为 Mongoid::Boolean
类的别名。
要定义自定义字段类型,请参阅下面的 自定义字段类型。
注意
不支持将 BSON::Int64
和 BSON::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允许将多种类型的值分配给日期
字段
日期
- 提供的日期将按原样存储。时间
、DateTime
、ActiveSupport::TimeWithZone
- 从值的时区中提取日期组件。字符串
- 使用字符串中指定的日期。整数
、浮点数
- 值被视为UTC时间戳,并将其转换为配置的时间区域(请注意,Mongoid.use_utc
对此转换没有影响),然后从结果时间中取日期。
换句话说,如果值中指定了日期,则直接使用该日期,而无需首先将值转换为配置的时间区域。
由于日期与日期时间转换是有损的(它丢弃了时间组件),特别是如果应用程序在不同的时区中使用时间,建议在将值赋给类型为Date
的字段之前,显式地将String
、Time
和DateTime
对象转换为Date
对象。
注意
当数据库包含一个Date
字段的字符串值时,Mongoid使用Time.parse
解析字符串值,丢弃结果Time
对象的时间部分并使用日期部分。Time.parse
将没有时区的值视为本地时间。
字段类型:日期时间
MongoDB以UTC时间戳存储所有时间。在将值分配给DateTime
字段或查询DateTime
字段时,Mongoid在将其发送到MongoDB服务器之前将传入的值转换为UTC Time
。
Time
、ActiveSupport::TimeWithZone
和DateTime
对象包含时区信息,而持久化的值是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字段类型用于存储精度更高的数字。
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
值可能会导致精度损失。
当在无类型字段(即动态类型字段)中存储 BigDecimal
且 Mongoid.map_big_decimal_to_decimal128
为 false
时,存在一个额外的注意事项。在这种情况下,BigDecimal
被存储为字符串,并且由于使用了动态字段,使用 BigDecimal
查询该字段将找不到该 BigDecimal
的字符串,因为查询正在寻找 BigDecimal
。为了查询该字符串,必须首先使用 to_s
将 BigDecimal
转换为字符串。请注意,当字段类型为 BigDecimal
时,这不是问题。
如果您希望完全不使用 BigDecimal
,可以将字段类型设置为 BSON::Decimal128
。这将允许您跟踪尾部零和有符号的 NaN
值。
迁移到基于 decimal128
的 BigDecimal
字段
在 Mongoid 的下一个主要版本中,全局配置选项 Mongoid.map_big_decimal_to_decimal128
将默认为 true
。当此标志开启时,查询中的 BigDecimal
值将不会与数据库中已存储的字符串匹配;它们仅会匹配数据库中的 decimal128
值。如果您有一个由字符串支持的 BigDecimal
字段,您有三个选择
可以将全局配置选项
Mongoid.map_big_decimal_to_decimal128
设置为false
,并继续将您的BigDecimal
值作为字符串存储。请注意,您将放弃将BigDecimal
值存储为decimal128
的优势,例如根据字段数值进行查询和聚合。可以将全局配置选项
Mongoid.map_big_decimal_to_decimal128
设置为true
,并将该字段的所有值从字符串转换为数据库中的decimal128
值。您应该在将全局配置选项设置为 true 之前执行此转换。以下是一个示例查询来完成此操作db.bands.updateMany({ "field": { "$exists": true } }, [ { "$set": { "field": { "$toDecimal": "$field" } } } ]) 此查询将更新所有包含给定字段的文档,将该字段设置为对应的
decimal128
值。请注意,此查询只能在 MongoDB 4.2+ 中工作。可以将全局配置选项
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
取消别名id
可以使用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
自定义ID
默认情况下,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_attribute
和 write_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.
类方法 demongoize
是 mongoize
的逆操作。它接收来自 MongoDB Ruby 驱动程序的原始对象并将其转换为您的自定义类型实例。在这种情况下,数据库驱动程序返回一个数组,并从它中实例化一个 Point
。当调用自定义类型字段的获取器时使用 demongoize
方法。请注意,在上面的例子中,由于 demongoize
调用了 Point.new
,每次调用获取器都会生成一个 Point
的新实例。
Mongoid 总是会在从数据库检索的值上调用 demongoize
方法,但应用程序理论上可以在 demongoize
方法中使用任意输入。建议应用程序在其 demongoize
方法中添加对任意输入的处理。我们可以将 Point
的 demongoize
方法重写如下
def demongoize(object) if object.is_a?(Array) && object.length == 2 Point.new(object[0], object[1]) end end
请注意,demongoize
只会在给定的数组长度为 2 时创建一个新的 Point
,否则将返回 nil
。mongoize
和 demongoize
方法都应该准备好接受任意输入,并且在无法将值转换为自定义类型时返回 nil
。有关详细信息,请参阅无法转换的值部分。
最后,类方法 evolve
与 mongoize
类似,但它用于在 Mongoid 查询条件中使用对象进行转换。
point = Point.new(12, 24) Venue.where(location: point) # This uses Point.evolve
evolve
方法也应准备好接受任意输入,但是与 mongoize
和 demongoize
方法不同,它在无法将值转换为自定义类型时应返回输入的值。有关详细信息,请参阅无法转换的值部分。
虚幻自定义字段类型
自定义字段类型可能会在用户可见的属性值与声明的字段类型不同时,将用户可见的属性值转换为存储在数据库中的值。例如,这可以用于实现一个枚举到另一个枚举的映射,以便在应用程序中具有更描述性的值,而在数据库中存储更紧凑的值。
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" => "₪" } }
本地化 :present
字段选项
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"
对原子持久性运算符的调用,如bit
和inc
,将持久化只读字段的变化。
时间戳字段
Mongoid在Mongoid::Timestamps
中提供了一个时间戳模块,可以将其包含在内以获取对created_at
和updated_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为使用包含点和以美元符号开头的字段名提供有限支持,以与其他软件兼容,然而,由于这种支持仅限于特定运算符(例如getField,setField)并且需要使用聚合管道进行查询和更新,因此,应用程序应尽量避免在字段名中使用点,并尽可能避免以美元符号开头。
从版本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