mongodb/mongoid

View on GitHub
docs/reference/fields.txt

Summary

Maintainability
Test Coverage
.. _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 <https://mongodb.com/docs/manual/reference/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 <field-type-big-decimal>`
- ``Mongoid::Boolean``, which may be specified simply as ``Boolean`` in the
  scope of a class which included ``Mongoid::Document``.
- :ref:`Date <field-type-date>`
- :ref:`DateTime <field-type-date-time>`
- ``Float``
- :ref:`Hash <field-type-hash>`
- ``Integer``
- :ref:`Object <untyped-fields>`
- ``BSON::ObjectId``
- ``Range``
- :ref:`Regexp <field-type-regexp>`
- ``Set``
- ``String``
- :ref:`Mongoid::StringifiedSymbol <field-type-stringified-symbol>`,
  which may be specified simply as ``StringifiedSymbol`` in the scope of a
  class which included ``Mongoid::Document``.
- :ref:`Symbol <field-type-stringified-symbol>`
- :ref:`Time <field-type-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 <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 <field-type-symbol>`.
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 <field-type-stringified-symbol>`
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 <https://www.mongodb.com/docs/manual/reference/limits/#naming-restrictions>`_,
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 <time-zones>`.

``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)
  # => #<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:

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 <time-zones>` (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 <time-zones>`.

.. code-block:: ruby

   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):

.. 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 <time-zones>` 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) <http://pcre.org/>`_
and Ruby uses `Onigmo <https://github.com/k-takata/Onigmo>`_, which is a
fork of `Oniguruma regular expression engine <https://github.com/kkos/oniguruma>`_.
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
  # => #<BSON::Regexp::Raw:0x0000555f505e4a20 @pattern="hello.world", @options="ms">

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')
  # => #<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":

.. 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 _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.

.. 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')
  # => #<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``
:ref:`configuration option <configuration-options>` 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: , >
  band.id
  => nil
  band.reload
  # raises Mongoid::Errors::DocumentNotFound
  Band.last
  => #<Band _id: 5fc681c22c97a6791f324b99, >

Omitting ``_id`` fields is more common in :ref:`embedded documents <omit-id>`.

Mongoid also defines the ``id`` field aliased to ``_id``. The ``id``
alias can :ref:`be removed <unalias-id>` 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" ])
  # => #<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:

.. 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
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:

.. 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 <https://jira.mongodb.org/browse/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 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.

.. 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 <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 <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,
specifying 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:

Localized Fields
================

Mongoid supports localized fields via the `I18n gem <https://github.com/ruby-i18n/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 <https://github.com/ruby-i18n/i18n/wiki/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 <https://github.com/ruby-i18n/i18n/pull/415>`_
  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")
  # => #<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.

.. 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
<https://www.mongodb.com/docs/manual/core/dot-dollar-considerations/#std-label-crud-concepts-dot-dollar-considerations>`_
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 </reference/operator/aggregation/getField/>`,
:manual:`setField </reference/operator/aggregation/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 <https://www.mongodb.com/docs/manual/core/dot-dollar-considerations/>`_,
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