Field Definition
On this page
- Field Types
- Untyped Fields
- Field Type: StringifiedSymbol
- Field Type: Symbol
- Field Type: Hash
- Field Type: Time
- Field Type: Date
- Field Type: DateTime
- Field Type: Regexp
- BigDecimal Fields
- Using Symbols Or Strings Instead Of Classes
- Specifying Field Default Values
- Specifying Storage Field Names
- Field Aliases
- Reserved Names
- Field Redefinition
- Custom IDs
- Uncastable Values
- Customizing Field Behavior
- Custom Getters And Setters
- Custom Field Types
- Custom Field Options
- Dynamic Fields
- Special Characters in Field Names
- Localized Fields
- Localize
:present
Field Option - Fallbacks
- Querying
- Indexing
- Read-Only Attributes
- Timestamp Fields
- Field Names with Dots/Periods (
.
) and Dollar Signs ($
)
Field Types
MongoDB stores underlying document data using
BSON types, and
Mongoid converts BSON types to Ruby types at runtime in your application.
For example, a field defined with type: :float
will use the Ruby Float
class in-memory and will persist in the database as the the BSON double
type.
Field type definitions determine how Mongoid behaves when constructing queries and retrieving/writing fields from/to the database. Specifically:
When assigning values to fields at runtime, the values are converted to the specified type.
When persisting data to MongoDB, the data is sent in an appropriate type, permitting richer data manipulation within MongoDB or by other tools.
When querying documents, query parameters are converted to the specified type before being sent to MongoDB.
When retrieving documents from the database, field values are converted to the specified type.
Changing the field definitions in a model class does not alter data already stored in MongoDB. To update type or contents of fields of existing documents, the field must be re-saved to the database. Note that, due to Mongoid tracking which attributes on a model change and only saving the changed ones, it may be necessary to explicitly write a field value when changing the type of an existing field without changing the stored values.
Consider a simple class for modeling a person in an application. A person may
have a name, date_of_birth, and weight. We can define these attributes
on a person by using the field
macro.
class Person include Mongoid::Document field :name, type: String field :date_of_birth, type: Date field :weight, type: Float end
The valid types for fields are as follows:
Array
BSON::Binary
Mongoid::Boolean
, which may be specified simply asBoolean
in the scope of a class which includedMongoid::Document
.Float
Integer
BSON::ObjectId
Range
Set
String
Mongoid::StringifiedSymbol, which may be specified simply as
StringifiedSymbol
in the scope of a class which includedMongoid::Document
.ActiveSupport::TimeWithZone
Mongoid also recognizes the string "Boolean"
as an alias for the
Mongoid::Boolean
class.
To define custom field types, refer to Custom Field Types below.
Note
Using the BSON::Int64
and BSON::Int32
types as field types is unsupported.
Saving these types to the database will work as expected, however, querying them
will return the native Ruby Integer
type. Querying fields of type
BSON::Decimal128
will return values of type BSON::Decimal128
in
BSON <=4 and values of type BigDecimal
in BSON 5+.
Untyped Fields
Not specifying a type for a field is the same as specifying the Object
type. Such fields are untyped:
class Product include Mongoid::Document field :properties # Equivalent to: field :properties, type: Object end
An untyped field can store values of any type which is directly serializable to BSON. This is useful when a field may contain values of different types (i.e. it is a variant type field), or when the type of values is not known ahead of time:
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"}
When values are assigned to the field, Mongoid still performs mongoization but uses the class of the value rather than the field type for mongoization logic.
product = Product.new(properties: 0..10) product.properties # The range 0..10, mongoized: # => {"min"=>0, "max"=>10}
When reading data from the database, Mongoid does not perform any type conversions on untyped fields. For this reason, even though it is possible to write any BSON-serializable value into an untyped fields, values which require special handling on the database reading side will generally not work correctly in an untyped field. Among field types supported by Mongoid, values of the following types should not be stored in untyped fields:
Date
(values will be returned asTime
)DateTime
(values will be returned asTime
)Range
(values will be returned asHash
)
Field Type: StringifiedSymbol
The StringifiedSymbol
field type is the recommended field type for storing
values that should be exposed as symbols to Ruby applications. When using the Symbol
field type,
Mongoid defaults to storing values as BSON symbols. For more information on the
BSON symbol type, see here.
However, the BSON symbol type is deprecated and is difficult to work with in programming languages
without native symbol types, so the StringifiedSymbol
type allows the use of symbols
while ensuring interoperability with other drivers. The StringifiedSymbol
type stores all data
on the database as strings, while exposing values to the application as symbols.
An example usage is shown below:
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
All non-string values will be stringified upon being sent to the database (via to_s
), and
all values will be converted to symbols when returned to the application. Values that cannot be
converted directly to symbols, such as integers and arrays, will first be converted to strings and
then symbols before being returned to the application.
For example, setting an integer as status
:
post = Post.new(status: 42) post.status # => :"42"
If the StringifiedSymbol
type is applied to a field that contains BSON symbols, the values
will be stored as strings instead of BSON symbols on the next save. This permits transparent lazy
migration from fields that currently store either strings or BSON symbols in the database to the
StringifiedSymbol
field type.
Field Type: Symbol
New applications should use the StringifiedSymbol field type
to store Ruby symbols in the database. The StringifiedSymbol
field type
provides maximum compatibility with other applications and programming languages
and has the same behavior in all circumstances.
Mongoid also provides the deprecated Symbol
field type for serializing
Ruby symbols to BSON symbols. Because the BSON specification deprecated the
BSON symbol type, the bson
gem will serialize Ruby symbols into BSON strings
when used on its own. However, in order to maintain backwards compatibility
with older datasets, the mongo
gem overrides this behavior to serialize Ruby
symbols as BSON symbols. This is necessary to be able to specify queries for
documents which contain BSON symbols as fields.
To override the default behavior and configure the mongo
gem (and thereby
Mongoid as well) to encode symbol values as strings, include the following code
snippet in your project:
class Symbol def bson_type BSON::String::BSON_TYPE end end
Field Type: Hash
When using a field of type Hash, be wary of adhering to the legal key names for mongoDB, or else the values will not store properly.
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
Field Type: Time
Time
fields store values as Time
instances in the configured
time zone.
Date
and DateTime
instances are converted to Time
instances upon
assignment to a Time
field:
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>
In the above example, the value was interpreted as the beginning of today in local time, because the application was not configured to use UTC times.
Note
When the database contains a string value for a Time
field, Mongoid
parses the string value using Time.parse
which considers values without
time zones to be in local time.
Field Type: Date
Mongoid allows assignment of values of several types to Date
fields:
Date
- the provided date is stored as is.Time
,DateTime
,ActiveSupport::TimeWithZone
- the date component of the value is taken in the value's time zone.String
- the date specified in the string is used.Integer
,Float
- the value is taken to be a UTC timestamp which is converted to the configured time zone (note thatMongoid.use_utc
has no effect on this conversion), then the date is taken from the resulting time.
In other words, if a date is specified in the value, that date is used without first converting the value to the configured time zone.
As a date & time to date conversion is lossy (it discards the time component),
especially if an application operates with times in different time zones it is
recommended to explicitly convert String
, Time
and DateTime
objects to Date
objects before assigning the values to fields of type
Date
.
Note
When the database contains a string value for a Date
field, Mongoid
parses the string value using Time.parse
, discards the time portion of
the resulting Time
object and uses the date portion. Time.parse
considers values without time zones to be in local time.
Field Type: DateTime
MongoDB stores all times as UTC timestamps. When assigning a value to a
DateTime
field, or when querying a DateTime
field, Mongoid
converts the passed in value to a UTC Time
before sending it to the
MongoDB server.
Time
, ActiveSupport::TimeWithZone
and DateTime
objects embed
time zone information, and the value persisted is the specified moment in
time, in UTC. When the value is retrieved, the time zone in which it is
returned is defined by the configured time zone settings.
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 also supports casting integers and floats to DateTime
. When
doing so, the integers/floats are assumed to be Unix timestamps (in UTC):
ticket.opened_at = 1544803974 ticket.opened_at # => Fri, 14 Dec 2018 16:12:54 +0000
If a string is used as a DateTime
field value, the behavior depends on
whether the string includes a time zone. If no time zone is specified,
the default Mongoid time zone is used:
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
If a time zone is specified, it is respected:
ticket.opened_at = 'Mar 4, 2018 10:00:00 +01:00' ticket.opened_at # => Sun, 04 Mar 2018 09:00:00 +0000
Note
When the database contains a string value for a DateTime
field, Mongoid
parses the string value using Time.parse
which considers values without
time zones to be in local time.
Field Type: Regexp
MongoDB supports storing regular expressions in documents, and querying using regular expressions. Note that MongoDB uses Perl-compatible regular expressions (PCRE) and Ruby uses Onigmo, which is a fork of Oniguruma regular expression engine. The two regular expression implementations generally provide equivalent functionality but have several important syntax differences.
When a field is declared to be of type Regexp, Mongoid converts Ruby regular
expressions to BSON regular expressions and stores the result in MongoDB.
Retrieving the field from the database produces a BSON::Regexp::Raw
instance:
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">
Use #compile
method on BSON::Regexp::Raw
to get back the Ruby regular
expression:
token.pattern.compile # => /hello.world/m
Note that, if the regular expression was not originally a Ruby one, calling
#compile
on it may produce a different regular expression. For example,
the following is a PCRE matching a string that ends in "hello":
BSON::Regexp::Raw.new('hello$', 's') # => #<BSON::Regexp::Raw:0x0000555f51441640 @pattern="hello$", @options="s">
Compiling this regular expression produces a Ruby regular expression that matches strings containing "hello" before a newline, besides strings ending in "hello":
BSON::Regexp::Raw.new('hello$', 's').compile =~ "hello\nworld" # => 0
This is because the meaning of $
is different between PCRE and Ruby
regular expressions.
BigDecimal Fields
The BigDecimal
field type is used to store numbers with increased precision.
The BigDecimal
field type stores its values in two different ways in the
database, depending on the value of the Mongoid.map_big_decimal_to_decimal128
global config option. If this flag is set to false (which is the default),
the BigDecimal
field will be stored as a string, otherwise it will be stored
as a BSON::Decimal128
.
The BigDecimal
field type has some limitations when converting to and from
a BSON::Decimal128
:
BSON::Decimal128
has a limited range and precision, whileBigDecimal
has no restrictions in terms of range and precision.BSON::Decimal128
has a max value of approximately10^6145
and a min value of approximately-10^6145
, and has a maximum of 34 bits of precision. When attempting to store values that don't fit into aBSON::Decimal128
, it is recommended to have them stored as a string instead of aBSON::Decimal128
. You can do that by settingMongoid.map_big_decimal_to_decimal128
tofalse
. If a value that does not fit in aBSON::Decimal128
is attempted to be stored as one, an error will be raised.BSON::Decimal128
is able to accept signedNaN
values, whileBigDecimal
is not. When retrieving signedNaN
values from the database using theBigDecimal
field type, theNaN
will be unsigned.BSON::Decimal128
maintains trailing zeroes when stored in the database.BigDecimal
, however, does not maintain trailing zeroes, and therefore retrievingBSON::Decimal128
values using theBigDecimal
field type may result in a loss of precision.
There is an additional caveat when storing a BigDecimal
in a field with no
type (i.e. a dynamically typed field) and Mongoid.map_big_decimal_to_decimal128
is false
. In this case, the BigDecimal
is stored as a string, and since a
dynamic field is being used, querying for that field with a BigDecimal
will
not find the string for that BigDecimal
, since the query is looking for a
BigDecimal
. In order to query for that string, the BigDecimal
must
first be converted to a string with to_s
. Note that this is not a problem
when the field has type BigDecimal
.
If you wish to avoid using BigDecimal
altogether, you can set the field
type to BSON::Decimal128
. This will allow you to keep track of trailing
zeroes and signed NaN
values.
Migration to decimal128
-backed BigDecimal
Field
In a future major version of Mongoid, the Mongoid.map_big_decimal_to_decimal128
global config option will be defaulted to true
. When this flag is turned on,
BigDecimal
values in queries will not match to the strings that are already
stored in the database; they will only match to decimal128
values that are
in the database. If you have a BigDecimal
field that is backed by strings,
you have three options:
The
Mongoid.map_big_decimal_to_decimal128
global config option can be set tofalse
, and you can continue storing yourBigDecimal
values as strings. Note that you are surrendering the advantages of storingBigDecimal
values as adecimal128
, like being able to do queries and aggregations based on the numerical value of the field.The
Mongoid.map_big_decimal_to_decimal128
global config option can be set totrue
, and you can convert all values for that field from strings todecimal128
values in the database. You should do this conversion before setting the global config option to true. An example query to accomplish this is as follows:db.bands.updateMany({ "field": { "$exists": true } }, [ { "$set": { "field": { "$toDecimal": "$field" } } } ]) This query updates all documents that have the given field, setting that field to its corresponding
decimal128
value. Note that this query only works in MongoDB 4.2+.The
Mongoid.map_big_decimal_to_decimal128
global config option can be set totrue
, and you can have both strings anddecimal128
values for that field. This way, onlydecimal128
values will be inserted into and updated to the database going forward. Note that you still don't get the full advantages of using onlydecimal128
values, but your dataset is slowly migrating to alldecimal128
values, as old string values are updated todecimal128
and newdecimal128
values are added. With this setup, you can still query forBigDecimal
values as follows:Mongoid.map_big_decimal_to_decimal128 = true big_decimal = BigDecimal('2E9') Band.in(sales: [big_decimal, big_decimal.to_s]).to_a This query will find all values that are either a
decimal128
value or a string that match that value.
Using Symbols Or Strings Instead Of Classes
Mongoid permits using symbols or strings instead of classes to specify the type of fields, for example:
class Order include Mongoid::Document field :state, type: :integer # Equivalent to: field :state, type: "integer" # Equivalent to: field :state, type: Integer end
Only standard field types as listed below can be specified using symbols or strings in this manner. Mongoid recognizes the following expansions:
: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
Specifying Field Default Values
A field can be configured to have a default value. The default value can be fixed, as in the following example:
class Order include Mongoid::Document field :state, type: String, default: 'created' end
The default value can also be specified as a Proc
:
class Order include Mongoid::Document field :fulfill_by, type: Time, default: ->{ Time.now + 3.days } end
Note
Default values that are not Proc
instances are evaluated at class load
time, meaning the following two definitions are not equivalent:
field :submitted_at, type: Time, default: Time.now field :submitted_at, type: Time, default: ->{ Time.now }
The second definition is most likely the desired one, which causes the time of submission to be set to the current time at the moment of document instantiation.
To set a default which depends on the document's state, use self
inside the Proc
instance which would evaluate to the document instance
being operated on:
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 }
When defining a default value as a Proc
, Mongoid will apply the default
after all other attributes are set and associations are initialized.
To have the default be applied before the other attributes are set,
use the pre_processed: true
field option:
field :fulfill_by, type: Time, default: ->{ Time.now + 3.days }, pre_processed: true
The pre_processed: true
option is also necessary when specifying a custom
default value via a Proc
for the _id
field, to ensure the _id
is set correctly via associations:
field :_id, type: String, default: -> { 'hello' }, pre_processed: true
Specifying Storage Field Names
One of the drawbacks of having a schemaless database is that MongoDB must store all field information along with every document, meaning that it takes up a lot of storage space in RAM and on disk. A common pattern to limit this is to alias fields to a small number of characters, while keeping the domain in the application expressive. Mongoid allows you to do this and reference the fields in the domain via their long names in getters, setters, and criteria while performing the conversion for you.
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" }
Field Aliases
It is possible to define field aliases. The value will be stored in the destination field but can be accessed from either the destination field or from the aliased field:
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"
Aliases can be removed from model classes using the unalias_attribute
method.
class Band unalias_attribute :n end
Unaliasing id
unalias_attribute
can be used to remove the predefined id
alias.
This is useful for storing different values in id
and _id
fields:
class Band include Mongoid::Document unalias_attribute :id field :id, type: String end Band.new(id: '42') # => #<Band _id: 5fc1c3f42c97a6590684046c, id: "42">
Reserved Names
Attempting to define a field on a document that conflicts with a reserved
method name in Mongoid will raise an error. The list of reserved names can
be obtained by invoking the Mongoid.destructive_fields
method.
Field Redefinition
By default Mongoid allows redefining fields on a model. To raise an error
when a field is redefined, set the duplicate_fields_exception
configuration option to true
.
With the option set to true, the following example will raise an error:
class Person include Mongoid::Document field :name field :name, type: String end
To define the field anyway, use the overwrite: true
option:
class Person include Mongoid::Document field :name field :name, type: String, overwrite: true end
Custom IDs
By default, Mongoid defines the _id
field on documents to contain a
BSON::ObjectId
value which is automatically generated by Mongoid.
It is possible to replace the _id
field definition to change the type
of the _id
values or have different default values:
class Band include Mongoid::Document field :name, type: String field :_id, type: String, default: ->{ name } end
It is possible to omit the default entirely:
class Band include Mongoid::Document field :_id, type: String end
If the default on _id
is omitted, and no _id
value is provided by
your application, Mongoid will persist the document without the _id
value. In this case, if the document is a top-level document, an _id
value will be assigned by the server; if the document is an embedded document,
no _id
value will be assigned. Mongoid will not automatically retrieve
this value, if assigned, when the document is persisted - you
must obtain the persisted value (and the complete persisted document) using
other means:
band = Band.create! => #<Band _id: , > band.id => nil band.reload # raises Mongoid::Errors::DocumentNotFound Band.last => #<Band _id: 5fc681c22c97a6791f324b99, >
Omitting _id
fields is more common in embedded documents.
Mongoid also defines the id
field aliased to _id
. The id
alias can be removed if desired (such as to integrate
with systems that use the id
field to store value different from _id
.
Uncastable Values
In Mongoid 8, Mongoid has standardized the treatment of the assignment and reading of "uncastable" values. A value is considered "uncastable" when it cannot be coerced to the type of its field. For example, an array would be an "uncastable" value to an Integer field.
Assigning Uncastable Values
The assignment of uncastable values has been standardized to assign nil
by
default. Consider the following example:
class User include Mongoid::Document field :name, type: Integer end User.new(name: [ "hello" ])
Assigning an array to a field of type Integer doesn't work since an array can't
be coerced to an Integer. The assignment of uncastable values to a field will
cause a nil
to be written:
user = User.new(name: [ "Mike", "Trout" ]) # => #<User _id: 62b222d43282a47bf73e3264, name: nil>
Note that the original uncastable values will be stored in the
attributes_before_type_cast
hash with their field names:
user.attributes_before_type_cast["name"] # => ["Mike", "Trout"]
Note
Note that for numeric fields, any class that defines to_i
for Integer
fields, to_f
for Floats, and to_d
for BigDecimals, is castable.
Strings are the exception and will only call the corresponding to_*
method if the string is numeric. If a class only defines to_i
and not
to_f
and is being assigned to a Float field, this is uncastable, and Mongoid
will not perform a two-step conversion (i.e. to_i
and then to_f
).
Reading Uncastable Values
When documents in the database contain values of different types than their
representations in Mongoid, if Mongoid cannot coerce them into the correct type,
it will replace the value with nil
. Consider the following model and document in the
database:
class User include Mongoid::Document field :name, type: Integer end
{ _id: ..., name: [ "Mike", "Trout" ] }
Reading this document from the database will result in the model's name field
containing nil
:
User.first.name # => nil
The database value of type array cannot be stored in the attribute, since the
array can't be coerced to an Integer. Note that the original uncastable values
will be stored in the attributes_before_type_cast
hash with their field
names:
user.attributes_before_type_cast["name"] # => ["Mike", "Trout"]
Note
The demongoize
methods on container objects (i.e. Hash, Array) have not
been changed to permit automatic persistence of mutated container attributes.
See MONGOID-2951 for a
longer discussion of this topic.
Customizing Field Behavior
Mongoid offers several ways to customize the behavior of fields.
Custom Getters And Setters
You may override getters and setters for fields to modify the values
when they are being accessed or written. The getters and setters use the
same name as the field. Use read_attribute
and write_attribute
methods inside the getters and setters to operate on the raw attribute
values.
For example, Mongoid provides the :default
field option to write a
default value into the field. If you wish to have a field default value
in your application but do not wish to persist it, you can override the
getter as follows:
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}
To give another example, a field which converts empty strings to nil values may be implemented as follows:
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}
Custom Field Types
You can define custom types in Mongoid and determine how they are serialized
and deserialized. In this example, we define a new field type Point
, which we
can use in our model class as follows:
class Profile include Mongoid::Document field :location, type: Point end
Then make a Ruby class to represent the type. This class must define methods used for MongoDB serialization and deserialization as follows:
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
The instance method mongoize
takes an instance of your custom type object, and
converts it into a representation of how it will be stored in the database, i.e. to pass
to the MongoDB Ruby driver. In our example above, we want to store our Point
object as an Array
in the form [ x, y ]
.
The class method mongoize
is similar to the instance method, however it must handle
objects of all possible types as inputs. The mongoize
method is used when calling the
setter methods for fields of your custom type.
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.
The class method demongoize
does the inverse of mongoize
. It takes the raw object
from the MongoDB Ruby driver and converts it to an instance of your custom type.
In this case, the database driver returns an Array
and we instantiate a Point
from it.
The demongoize
method is used when calling the getters of fields for your custom type.
Note that in the example above, since demongoize
calls Point.new
, a new instance of
Point
will be generated on each call to the getter.
Mongoid will always call the demongoize
method on values that were
retrieved from the database, but applications may, in theory, call
demongoize
with arbitrary input. It is recommended that applications add
handling for arbitrary input in their demongoize
methods. We can rewrite
Point
's demongoize method as follows:
def demongoize(object) if object.is_a?(Array) && object.length == 2 Point.new(object[0], object[1]) end end
Notice that demongoize
will only create a new Point
if given an array
of length 2, and will return nil
otherwise. Both the mongoize
and
demongoize
methods should be prepared to receive arbitrary input and should
return nil
on values that are uncastable to your custom type. See the
section on Uncastable Values for more details.
Lastly, the class method evolve
is similar to mongoize
, however it is used
when transforming objects for use in Mongoid query criteria.
point = Point.new(12, 24) Venue.where(location: point) # This uses Point.evolve
The evolve
method should also be prepared to receive arbitrary input,
however, unlike the mongoize
and demongoize
methods, it should return
the inputted value on values that are uncastable to your custom type. See the
section on Uncastable Values for more details.
Phantom Custom Field Types
The custom field type may perform conversions from user-visible attribute values to the values stored in the database when the user-visible attribute value type is different from the declared field type. For example, this can be used to implement a mapping from one enumeration to another, to have more descriptive values in the application and more compact values stored in the database:
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!
Custom Field Options
You may define custom options for the field
macro function
which extend its behavior at the your time model classes are loaded.
As an example, we will define a :max_length
option which will add a length
validator for the field. First, declare the new field option in an initializer,
specifying its handler function as a block:
# 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
Then, use it your model class:
class Person include Mongoid::Document field :name, type: String, max_length: 10 end
Note that the handler function will be invoked whenever the option is used in the field definition, even if the option's value is false or nil.
Dynamic Fields
By default, Mongoid requires all fields that may be set on a document to
be explicitly defined using field
declarations. Mongoid also supports
creating fields on the fly from an arbitrary hash or documents stored in
the database. When a model uses fields not explicitly defined, such fields
are called dynamic fields.
To enable dynamic fields, include Mongoid::Attributes::Dynamic
module
in the model:
class Person include Mongoid::Document include Mongoid::Attributes::Dynamic end bob = Person.new(name: 'Bob', age: 42) bob.name # => "Bob"
It is possible to use field
declarations and dynamic fields in the same
model class. Attributes for which there is a field
declaration will be
treated according to the field
declaration, with remaining attributes
being treated as dynamic fields.
Attribute values in the dynamic fields must initially be set by either
passing the attribute hash to the constructor, mass assignment via
attributes=
, mass assignment via []=
, using write_attribute
,
or they must already be present in the database.
# 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')
If an attribute is not present in a particular model instance's attributes
hash, both the reader and the writer for the corresponding field are not
defined, and invoking them raises 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"
Attributes can always be read using mass attribute access or read_attribute
(this applies to models not using dynamic fields as well):
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
Note
The values returned from the read_attribute
method, and those stored in
the attributes
hash, are the mongoized
values.
Special Characters in Field Names
Mongoid permits dynamic field names to include spaces and punctuation:
bob = Person.new('hello world' => 'MDB') bob.send('hello world') # => "MDB" bob.write_attribute("hello%world", 'MDB') bob[:"hello%world"] # => "MDB"
Localized Fields
Mongoid supports localized fields via the I18n gem.
class Product include Mongoid::Document field :description, type: String, localize: true end
By telling the field to localize
, Mongoid will under the covers store the field
as a hash of locale/value pairs, but normal access to it will behave like a string.
I18n.default_locale = :en product = Product.new product.description = "Marvelous!" I18n.locale = :de product.description = "Fantastisch!" product.attributes # { "description" => { "en" => "Marvelous!", "de" => "Fantastisch!" }
You can get and set all the translations at once by using the corresponding _translations
method.
product.description_translations # { "en" => "Marvelous!", "de" => "Fantastisch!" } product.description_translations = { "en" => "Marvelous!", "de" => "Wunderbar!" }
Localized fields can be used with any field type. For example, they can be used with float fields for differences with currency:
class Product include Mongoid::Document field :price, type: Float, localize: true field :currency, type: String, localize: true end
By creating the model in this way, we can separate the price from the currency type, which allows you to use all of the number-related functionalities on the price when querying or aggregating that field (provided that you index into the stored translations hash). We can create an instance of this model as follows:
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" => "₪" } }
Localize :present
Field Option
Mongoid supports the :present
option when creating a localized field:
class Product include Mongoid::Document field :description, localize: :present end
This option automatically removes blank
values (i.e. those that return true
for the blank?
method) from the _translations
hash:
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!" }
When the empty string is written for the :de
locale, the "de"
key is
removed from the _translations
hash instead of writing the empty string.
Fallbacks
Mongoid integrates with i18n fallbacks. To use the fallbacks, the respective functionality must be explicitly enabled.
In a Rails application, set the config.i18n.fallbacks
configuration setting
to true
in your environment and specify the fallback languages:
config.i18n.fallbacks = true config.after_initialize do I18n.fallbacks[:de] = [ :en, :es ] end
In a non-Rails application, include the fallbacks module into the I18n backend you are using and specify the fallback languages:
require "i18n/backend/fallbacks" I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks) I18n.fallbacks[:de] = [ :en, :es ]
When fallbacks are enabled, if a translation is not present in the active language, translations will be looked up in the fallback languages:
product = Product.new I18n.locale = :en product.description = "Marvelous!" I18n.locale = :de product.description # "Marvelous!"
Mongoid also defines a :fallbacks
option on fields, which can be used to
disable fallback functionality on a specific field:
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
Note that this option defaults to true
.
Note
In i18n 1.1, the behavior of fallbacks changed to always require an explicit list of fallback locales rather than falling back to the default locale when no fallback locales have been provided.
Querying
When querying for localized fields using Mongoid's criteria API, Mongoid will automatically alter the criteria to match the current locale.
# Match all products with Marvelous as the description. Locale is en. Product.where(description: "Marvelous!") # The resulting MongoDB query filter: { "description.en" : "Marvelous!" }
Indexing
If you plan to be querying extensively on localized fields, you should index each of the locales that you plan on searching on.
class Product include Mongoid::Document field :description, localize: true index "description.de" => 1 index "description.en" => 1 end
Read-Only Attributes
You can tell Mongoid that certain attributes are read-only. This will allow
documents to be created with these attributes, but changes to them will be
ignored when using mass update methods such as 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.
If you explicitly try to update or remove a read-only attribute by itself,
a ReadonlyAttribute
exception will be raised:
band.update_attribute(:name, "Tool") # Raises the error. band.remove_attribute(:name) # Raises the error.
Assignments to read-only attributes using their setters will be ignored:
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"
Calls to atomic persistence operators, like bit
and inc
, will persist
changes to read-only fields.
Timestamp Fields
Mongoid supplies a timestamping module in Mongoid::Timestamps
which
can be included to get basic behavior for created_at
and
updated_at
fields.
class Person include Mongoid::Document include Mongoid::Timestamps end
You may also choose to only have specific timestamps for creation or modification.
class Person include Mongoid::Document include Mongoid::Timestamps::Created end class Post include Mongoid::Document include Mongoid::Timestamps::Updated end
If you want to turn off timestamping for specific calls, use the timeless method:
person.timeless.save Person.timeless.create!
If you'd like shorter timestamp fields with aliases on them to save space, you can include the short versions of the modules.
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
Field Names with Dots/Periods (.
) and Dollar Signs ($
)
Using dots/periods (.
) in fields names and starting a field name with
a dollar sign ($
) is not recommended, as Mongoid provides limited support
for retrieving and operating on the documents stored in those fields.
Both Mongoid and MongoDB query language (MQL) generally use the dot/period
character (.
) to separate field names in a field path that traverses
embedded documents, and words beginning with the dollar sign ($
) as
operators. MongoDB provides limited support
for using field names containing dots and starting with the dollar sign
for interoperability with other software,
however, due to this support being confined to specific operators
(e.g. getField,
setField) and
requiring the usage of the aggregation pipeline for both queries and updates,
applications should avoid using dots in field names and starting field names
with the dollar sign if possible.
Mongoid, starting in version 8, now allows users to access fields that begin with
dollar signs and that contain dots/periods. They can be accessed using the send
method as follows:
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
It is also possible to use read_attribute
to access these fields:
user.read_attribute("first.last") # => Mike.Trout
Due to server limitations, updating and replacing fields containing dots and dollars requires using special operators. For this reason, calling setters on these fields is prohibited and will raise an error:
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