.. _fields:
****************
Field Definition
****************
.. default-domain:: mongodb
.. contents:: On this page
:local:
:backlinks: none
:depth: 2
:class: singlecol
.. _field-types:
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:
1. When assigning values to fields at runtime, the values are converted to the
specified type.
2. When persisting data to MongoDB, the data is sent in an appropriate
type, permitting richer data manipulation within MongoDB or by other
tools.
3. When querying documents, query parameters are converted to the specified
type before being sent to MongoDB.
4. 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.
.. code-block:: ruby
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``
- :ref:`BigDecimal `
- ``Mongoid::Boolean``, which may be specified simply as ``Boolean`` in the
scope of a class which included ``Mongoid::Document``.
- :ref:`Date `
- :ref:`DateTime `
- ``Float``
- :ref:`Hash `
- ``Integer``
- :ref:`Object `
- ``BSON::ObjectId``
- ``Range``
- :ref:`Regexp `
- ``Set``
- ``String``
- :ref:`Mongoid::StringifiedSymbol `,
which may be specified simply as ``StringifiedSymbol`` in the scope of a
class which included ``Mongoid::Document``.
- :ref:`Symbol `
- :ref:`Time `
- ``ActiveSupport::TimeWithZone``
Mongoid also recognizes the string ``"Boolean"`` as an alias for the
``Mongoid::Boolean`` class.
To define custom field types, refer to :ref:`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:
Untyped Fields
--------------
Not specifying a type for a field is the same as specifying the ``Object``
type. Such fields are untyped:
.. code-block:: ruby
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:
.. code-block:: ruby
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.
.. code-block:: ruby
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 as ``Time``)
- ``DateTime`` (values will be returned as ``Time``)
- ``Range`` (values will be returned as ``Hash``)
.. _field-type-stringified-symbol:
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 :ref:`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:
.. code-block:: ruby
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``:
.. code-block:: ruby
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:
Field Type: Symbol
------------------
New applications should use the :ref:`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:
.. code-block:: ruby
class Symbol
def bson_type
BSON::String::BSON_TYPE
end
end
.. _field-type-hash:
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.
.. code-block:: ruby
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:
Field Type: Time
----------------
``Time`` fields store values as ``Time`` instances in the :ref:`configured
time zone `.
``Date`` and ``DateTime`` instances are converted to ``Time`` instances upon
assignment to a ``Time`` field:
.. code-block:: ruby
class Voter
include Mongoid::Document
field :registered_at, type: Time
end
Voter.new(registered_at: Date.today)
# => #
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:
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 :ref:`configured time zone ` (note that
``Mongoid.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-date-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 :ref:`configured time zone settings `.
.. code-block:: ruby
class Ticket
include Mongoid::Document
field :opened_at, type: DateTime
end
Mongoid.use_activesupport_time_zone = true
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
# => #
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):
.. code-block:: ruby
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 :ref:`default Mongoid time zone ` is used:
.. code-block:: ruby
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:
.. code-block:: ruby
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:
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:
.. code-block:: ruby
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
# => #
Use ``#compile`` method on ``BSON::Regexp::Raw`` to get back the Ruby regular
expression:
.. code-block:: ruby
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":
.. code-block:: ruby
BSON::Regexp::Raw.new('hello$', 's')
# => #
Compiling this regular expression produces a Ruby regular expression that
matches strings containing "hello" before a newline, besides strings ending in
"hello":
.. code-block:: ruby
BSON::Regexp::Raw.new('hello$', 's').compile =~ "hello\nworld"
# => 0
This is because the meaning of ``$`` is different between PCRE and Ruby
regular expressions.
.. _field-type-big-decimal:
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, while ``BigDecimal``
has no restrictions in terms of range and precision. ``BSON::Decimal128`` has
a max value of approximately ``10^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 a ``BSON::Decimal128``, it is recommended to
have them stored as a string instead of a ``BSON::Decimal128``. You can do
that by setting ``Mongoid.map_big_decimal_to_decimal128`` to ``false``. If a
value that does not fit in a ``BSON::Decimal128`` is attempted to be stored
as one, an error will be raised.
- ``BSON::Decimal128`` is able to accept signed ``NaN`` values, while
``BigDecimal`` is not. When retrieving signed ``NaN`` values from
the database using the ``BigDecimal`` field type, the ``NaN`` will be
unsigned.
- ``BSON::Decimal128`` maintains trailing zeroes when stored in the database.
``BigDecimal``, however, does not maintain trailing zeroes, and therefore
retrieving ``BSON::Decimal128`` values using the ``BigDecimal`` 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:
1. The ``Mongoid.map_big_decimal_to_decimal128`` global config option can be
set to ``false``, and you can continue storing your ``BigDecimal`` values as
strings. Note that you are surrendering the advantages of storing ``BigDecimal``
values as a ``decimal128``, like being able to do queries and aggregations
based on the numerical value of the field.
2. The ``Mongoid.map_big_decimal_to_decimal128`` global config option can be
set to ``true``, and you can convert all values for that field from strings to
``decimal128`` 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:
.. code-block:: javascript
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+.
3. The ``Mongoid.map_big_decimal_to_decimal128`` global config option can be
set to ``true``, and you can have both strings and ``decimal128`` values for
that field. This way, only ``decimal128`` values will be inserted into and
updated to the database going forward. Note that you still don't get the
full advantages of using only ``decimal128`` values, but your dataset is
slowly migrating to all ``decimal128`` values, as old string values are
updated to ``decimal128`` and new ``decimal128`` values are added. With this
setup, you can still query for ``BigDecimal`` values as follows:
.. code-block:: ruby
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:
.. code-block:: ruby
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``
.. _field-default-values:
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:
.. code-block:: ruby
class Order
include Mongoid::Document
field :state, type: String, default: 'created'
end
The default value can also be specified as a ``Proc``:
.. code-block:: ruby
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:
.. code-block:: ruby
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:
.. code-block:: ruby
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:
.. code-block:: ruby
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:
.. code-block:: ruby
field :_id, type: String, default: -> { 'hello' }, pre_processed: true
.. _storage-field-names:
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.
.. code-block:: ruby
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:
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:
.. code-block:: ruby
class Band
include Mongoid::Document
field :name, type: String
alias_attribute :n, :name
end
band = Band.new(n: '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.
.. code-block:: ruby
class Band
unalias_attribute :n
end
.. _unalias-id:
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:
.. code-block:: ruby
class Band
include Mongoid::Document
unalias_attribute :id
field :id, type: String
end
Band.new(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``
:ref:`configuration option ` to ``true``.
With the option set to true, the following example will raise an error:
.. code-block:: ruby
class Person
include Mongoid::Document
field :name
field :name, type: String
end
To define the field anyway, use the ``overwrite: true`` option:
.. code-block:: ruby
class Person
include Mongoid::Document
field :name
field :name, type: String, overwrite: true
end
.. _custom-id:
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:
.. code-block:: ruby
class Band
include Mongoid::Document
field :name, type: String
field :_id, type: String, default: ->{ name }
end
It is possible to omit the default entirely:
.. code-block:: ruby
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:
.. code-block:: ruby
band = Band.create!
=> #
band.id
=> nil
band.reload
# raises Mongoid::Errors::DocumentNotFound
Band.last
=> #
Omitting ``_id`` fields is more common in :ref:`embedded documents `.
Mongoid also defines the ``id`` field aliased to ``_id``. The ``id``
alias can :ref:`be removed ` if desired (such as to integrate
with systems that use the ``id`` field to store value different from ``_id``.
.. _uncastable-values:
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:
.. code::
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:
.. code::
user = User.new(name: [ "Mike", "Trout" ])
# => #
Note that the original uncastable values will be stored in the
``attributes_before_type_cast`` hash with their field names:
.. code::
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
represenations 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:
.. code::
class User
include Mongoid::Document
field :name, type: Integer
end
.. code::
{ _id: ..., name: [ "Mike", "Trout" ] }
Reading this document from the database will result in the model's name field
containing ``nil``:
.. code::
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:
.. code::
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:
Customizing Field Behavior
==========================
Mongoid offers several ways to customize the behavior of fields.
.. _custom-getters-and-setters:
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:
.. code-block:: ruby
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:
.. code-block:: ruby
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:
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:
.. code-block:: ruby
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:
.. code-block:: ruby
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 represenation 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.
.. code-block:: ruby
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:
.. code:: ruby
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 :ref:`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.
.. code-block:: ruby
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 :ref:`Uncastable Values ` for more details.
.. _phantom-custom-field-types:
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:
.. code-block:: ruby
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:
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,
specifiying its handler function as a block:
.. code-block:: ruby
# 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:
.. code-block:: ruby
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:
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:
.. code-block:: ruby
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.
.. code-block:: ruby
# 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``:
.. code-block:: ruby
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):
.. code-block:: ruby
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:
.. code-block:: ruby
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 `i18n `_.
.. code-block:: ruby
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.
.. code-block:: ruby
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.
.. code-block:: ruby
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:
.. code:: ruby
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:
.. code:: ruby
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-fields:
Localize ``:present`` Field Option
----------------------------------
Mongoid supports the ``:present`` option when creating a localized field:
.. code-block:: ruby
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:
.. code-block:: ruby
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:
.. code-block:: ruby
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:
.. code-block:: ruby
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:
.. code-block:: ruby
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:
.. code:: ruby
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.
.. code-block:: ruby
# 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.
.. code-block:: ruby
class Product
include Mongoid::Document
field :description, localize: true
index "description.de" => 1
index "description.en" => 1
end
.. _read-only:
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``:
.. code-block:: ruby
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:
.. code-block:: ruby
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:
.. code-block:: ruby
b = Band.create!(name: "The Rolling Stones")
# => #
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 readonly 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.
.. code-block:: ruby
class Person
include Mongoid::Document
include Mongoid::Timestamps
end
You may also choose to only have specific timestamps for creation or
modification.
.. code-block:: ruby
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:
.. code-block:: ruby
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.
.. code-block:: ruby
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-periods-and-dollar-signs:
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. :manual:`getField `,
:manual:`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:
.. code::
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:
.. code::
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:
.. code::
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