mongodb/mongoid

View on GitHub
docs/release-notes/mongoid-7.4.txt

Summary

Maintainability
Test Coverage
***********
Mongoid 7.4
***********

.. default-domain:: mongodb

.. contents:: On this page
   :local:
   :backlinks: none
   :depth: 2
   :class: singlecol

This page describes significant changes and improvements in Mongoid 7.4.
The complete list of releases is available `on GitHub
<https://github.com/mongodb/mongoid/releases>`_ and `in JIRA
<https://jira.mongodb.org/projects/MONGOID?selectedItem=com.atlassian.jira.jira-projects-plugin:release-page>`_;
please consult GitHub releases for detailed release notes and JIRA for
the complete list of issues fixed in each release, including bug fixes.

All behavior changes in Mongoid 7.4 must be explicitly requested by changing
the value of configuration options as detailed below. By default,
Mongoid 7.4 behaves the same as Mongoid 7.3.


Ruby Version Support
--------------------

As of version 7.4, Mongoid supports Ruby 2.5+.
Support for Ruby 2.4 and earlier has been dropped.


Support for MongoDB 3.4 and Earlier Servers Deprecated
------------------------------------------------------

Mongoid 7.4 deprecates support for MongoDB 3.4 and earlier.
Mongoid 8 will require MongoDB 3.6 or newer.


Feature Flags Summary
---------------------

To ensure a stable upgrade path from Mongoid 7.3, Mongoid 7.4
introduces feature flags which are further explained in the
sections below.

To enable all new behavior in Mongoid 7.4, please use the following
:ref:`configuration options <configuration-options>` in your mongoid.yml file.
We recommend newly created apps to do this as well.

.. code-block:: yaml

  development:
    ...
    options:
      # Enable all new behavior in Mongoid 7.4
      legacy_triple_equals: false
      object_id_as_json_oid: false
      compare_time_by_ms: true
      broken_aggregables: false
      broken_updates: false
      broken_and: false
      broken_scoping: false
      broken_alias_handling: false
      legacy_pluck_distinct: false


Change ``===`` Operator To Match Ruby Semantics
-----------------------------------------------

In Mongoid 7.4, the ``===`` operator on ``Mongoid::Document`` classes and
instances can be configured to behave the same way as it does in Ruby,
and is equivalent to calling ``is_a?`` on the right hand
side with the left hand side as the argument:

.. code-block:: ruby

  ModelClass === instance

  # equivalent to:
  instance.is_a?(ModelClass)

In order to get this functionality, the ``Mongoid.legacy_triple_equals``
option must be set to false. If it is set to true, which is the default for
Mongoid 7.4, the ``===`` operator will function as it did in Mongoid 7.3:
``===`` returned ``true`` for some cases when the equivalent Ruby
``===`` implementation returned false, as per the examples below.

Mongoid 7.4 with ``Mongoid.legacy_triple_equals`` set to ``false`` behavior:

.. code-block:: ruby

  class Band
    include Mongoid::Document

    has_many :members
  end

  class CoverBand < Band
  end

  class Member
    include Mongoid::Document

    belongs_to :band
  end

  band = Band.new
  cover_band = CoverBand.new

  band === Band
  # => false

  cover_band === Band
  # => false

  Band === Band
  # => false

  CoverBand === Band
  # => false

  band.members === Array
  # => false

  band.members === Mongoid::Association::Referenced::HasMany::Enumerable
  # => false

Mongoid 7.3 and 7.4 with ``Mongoid.legacy_triple_equals`` set to ``true``
behavior:

.. code-block:: ruby

  band === Band
  # => true

  cover_band === Band
  # => true

  Band === Band
  # => true

  CoverBand === Band
  # => true

  band.members === Array
  # => true

  band.members === Mongoid::Association::Referenced::HasMany::Enumerable
  # => true

The standard invocation of ``===``, that is having the class on the left and
the instance on the right, works the same in Mongoid 7.4 as it did previously
and matches the core Ruby behavior:

.. code-block:: ruby

  Band === band
  # => true

  Band === cover_band
  # => true

.. note::

  In Mongoid 8.0, the default value of the ``Mongoid.legacy_triple_equals`` option will
  change to ``false``.


Return String ``_id`` Value (Hexadecimal) from ``BSON::ObjectId#as_json``
-------------------------------------------------------------------------

Mongoid 7.4 permits configuring the ``BSON::ObjectId#as_json`` method
to return the ``_id`` value as a hexadecimal string instead of the
``{"$oid" => "..."}`` hash it has returned in Mongoid 7.3 and previous
versions.

When ``Mongoid.object_id_as_json_oid`` is set to ``false``, Mongoid will
delegate to ``bson-ruby`` implementation of ``BSON::ObjectId#as_json``.
In ``bson-ruby`` 4 the ``BSON::ObjectId#as_json`` method will continue
to return the hash ``{"$oid" => "..."}`` for backwards compatibility, but
in ``bson-ruby`` 5 the ``BSON::ObjectId#as_json`` method will return only
the hexadecimal ObjectId string.

When ``Mongoid.object_id_as_json_oid`` is set to ``true``, Mongoid will
install an implementation of ``BSON::ObjectId#as_json`` which returns
the hash ``{"$oid" => "..."}`` as it did in Mongoid 7.3 and earlier.

The behavior of ``as_json`` is summarized in the following table:

.. list-table::
   :header-rows: 1
   :stub-columns: 1
   :class: compatibility-large no-padding

   * - ``Mongoid.object_id_as_json_oid`` value
     - true
     - false

   * - ``bson-ruby`` 4
     - ``{"$oid"=>"621ed7fda15d5d231594627c"}``
     - ``{"$oid"=>"621ed7fda15d5d231594627c"}``

   * - ``bson-ruby`` 5
     - ``{"$oid"=>"621ed7fda15d5d231594627c"}``
     - ``"621ed7fda15d5d231594627c"``

.. note::

  In Mongoid 8.0, the default value of the ``Mongoid.object_id_as_json_oid`` option will
  change to ``false``.


Scoped Associations
-------------------

Associations now support the ``:scope`` argument, yielding
:ref:`scoped associations <association-scope>`.


Compare Times With Millisecond Precision When Embedded Matching
---------------------------------------------------------------

Mongoid 7.4 with the ``Mongoid.compare_time_by_ms`` option set to ``true``
will truncate the times to millisecond precision when comparing them while
performing embedded matching.

Time objects in Ruby have nanosecond precision, whereas MongoDB server
can only store times with millisecond precision. Set the
``Mongoid.compare_time_by_ms`` option to ``true`` to truncate times to
millisecond precision when performing queries on already loaded embedded
associations (this is also called "embedded matching" and is done completely
in Ruby), to obtain the same query results when performing time comparisons
regardless of which documents are being queried. Setting this option to
``false`` will produce different results for queries on embedded associations
that are already loaded into memory vs queries on unloaded associations and
top-level models.

The ``Mongoid.compare_time_by_ms`` option is set to ``false`` by default
in Mongoid 7.4 for backwards compatibility.

.. note::

  In Mongoid 8.0, the default value of the ``Mongoid.compare_time_by_ms`` option will
  change to ``true``.


``count``, ``sum``, ``avg``, ``min``, ``max`` Ignore Sort If Not Limiting/Skipping
----------------------------------------------------------------------------------

The ``count``, ``sum``, ``avg``, ``min`` and ``max`` methods now omit the
sort stage from the generated aggregation pipeline if no skip or limit
is specified, because the results aren't affected by the sort order.
Example call that will now omit the sort stage and would potentially use
an index where it wouldn't before:

.. code-block:: ruby

  Band.desc(:name).count


Return ``0`` When Aggregating Empty Result Sets
-----------------------------------------------

Mongoid 7.4 with the ``Mongoid.broken_aggregables`` option set to ``false``
will return ``0`` from the ``sum`` method over an empty result set, for example:

.. code-block:: ruby

  Product.where(impossible_condition: true).sum(:price)
  # => 0

Mongoid 7.3 and Mongoid 7.4 with the ``Mongoid.broken_aggregables`` option
set to ``true`` (the default) returns ``nil`` in this case.

.. note::

  In Mongoid 8.0, the default value of the ``Mongoid.broken_aggregables`` option will
  change to ``false``.


Correct Update Behavior When Replacing Association
--------------------------------------------------

Mongoid 7.4 with the ``Mongoid.broken_updates`` option set to ``false``
will correctly persist an ``embeds_one`` association target that is set to nil
and then to a non-nil value, for example:

.. code-block:: ruby

  class Canvas
    include Mongoid::Document

    embeds_one :palette
  end

  canvas.palette = palette
  canvas.palette = nil
  canvas.palette = palette

In Mongoid 7.3 and earlier, and in 7.4 with the ``Mongoid.broken_aggregables``
option set to ``true`` (the default), ``canvas.palette`` would be ``nil`` when
we would expect it to be ``palette``.

.. note::

  In Mongoid 8.0, the default value of the ``Mongoid.broken_updates`` option will
  change to ``false``


Correct Logical ``and`` Query Generation
----------------------------------------

Mongoid 7.4 with the ``Mongoid.broken_and`` option set to ``false``
will preserve existing conditions when using ``and`` to add new conditions
to a query when the same operator is used on the same field multiple times.
For example, in the following query:

.. code-block:: ruby

  Band.where(id: 1).and({year: {'$in' => [2020]}}, {year: {'$in' => [2021]}}).where(id: 2)

Mongoid 7.4 with the ``Mongoid.broken_and`` option set to ``false`` will
generate the following criteria:

.. code-block:: ruby

  #<Mongoid::Criteria
    selector: {"_id"=>1, "year"=>{"$in"=>[2020]}, "$and"=>[{"year"=>{"$in"=>[2021]}}, {"_id"=>2}]}
    options:  {}
    class:    Band
    embedded: false>

In Mongoid 7.3 and earlier, and in 7.4 with the ``Mongoid.broken_and``
option set to ``true`` (the default), the following criteria would be
generated instead which omit the {"$in" => [2021]} condition:

.. code-block:: ruby

  <Mongoid::Criteria
    selector: {"_id"=>1, "year"=>{"$in"=>[2020]}, "$and"=>[{"_id"=>2}]}
    options:  {}
    class:    Band
    embedded: false>

.. note::

  In Mongoid 8.0, the default value of the ``Mongoid.broken_and`` option will
  change to ``false``.


Restore Parent Scope When Exiting ``with_scope`` Block
------------------------------------------------------

Mongoid 7.4 with the ``Mongoid.broken_scoping`` option set to ``false``
will restore the parent scope when exiting a ``with_scope`` block.
For example:

.. code-block:: ruby

  Band.with_scope(year: 2020) do
    Band.with_scope(active: true) do
      # ...
    end

    # {year: 2020} condition is applied here
  end

In Mongoid 7.3 and earlier, and in 7.4 with the ``Mongoid.broken_scoping``
option set to ``true`` (the default), once any ``with_scope`` block finishes,
all scopes are cleared:

.. code-block:: ruby

  Band.with_scope(year: 2020) do
    Band.with_scope(active: true) do
      # ...
    end

    # No scope is applied here
  end

.. note::

  In Mongoid 8.0, the default value of the ``Mongoid.broken_scoping`` option will
  change to ``false``.


Changes to ``distinct`` and ``pluck``
-------------------------------------

Respect Field Aliases In Embedded Documents When Using ``distinct`` and ``pluck``
`````````````````````````````````````````````````````````````````````````````````

When ``distinct`` and ``pluck`` are used with aliased fields in embedded
documents, the aliases can be expanded if the ``Mongoid.broken_alias_handling``
option is set to ``false``. By default, for backwards compatibility, in
Mongoid 7.4 this option is set to true, yielding Mongoid 7.3 and earlier
behavior. Given the following definitions:

.. code-block:: ruby

  class Band
    include Mongoid::Document
    embeds_many :managers
  end

  class Manager
    include Mongoid::Document
    embedded_in :band

    field :name, as: :n
  end

Mongoid 7.4 behavior with ``Mongoid.broken_alias_handling`` set to ``false``:

.. code-block:: ruby

  # Expands out to "managers.n" in the query:
  Band.distinct('managers.name')
  Band.pluck('managers.name')

Mongoid 7.3 and 7.4 with ``Mongoid.broken_alias_handling`` set to ``true`` behavior:

.. code-block:: ruby

  # Sends "managers.name" without expanding the alias:
  Band.distinct('managers.name')
  Band.pluck('managers.name')

.. note::

  The alias expansion for top-level fields has already been done by Mongoid 7.3.

.. note::

  In Mongoid 8.0, the default value of the ``Mongoid.broken_alias_handling`` option will
  change to ``false``.


Demongoize Values Returned from ``pluck`` and ``distinct``
``````````````````````````````````````````````````````````

Mongoid 7.4 with the ``Mongoid.legacy_pluck_distinct`` option set to ``false``
will demongoize values returned from the ``pluck`` and ``distinct`` methods.
Given the following definitions:

.. code-block:: ruby

  class Band
    include Mongoid::Document

    field :sales, type: BigDecimal
  end

  Band.create!(sales: "1E2")
  Band.create!(sales: "2E2")

Mongoid 7.4 behavior with ``Mongoid.legacy_pluck_distinct`` set to ``false``:

.. code-block:: ruby

  Band.pluck(:sales)
  # => [0.1e3, 0.2e3]
  Band.distinct(:sales)
  # => [0.1e3, 0.2e3]


In Mongoid 7.3 and earlier, and in 7.4 with the ``Mongoid.legacy_pluck_distinct``
option set to ``true`` (the default), the value returned from the pluck and
distinct methods will not be demongoized. For example:

.. code-block:: ruby

  Band.pluck(:sales)
  # => ["1E2", "2E2"]
  Band.distinct(:sales)
  # => ["1E2", "2E2"]

.. note::

  In Mongoid 8.0, the default value of the ``Mongoid.legacy_pluck_distinct`` option will
  change to ``false``.


Localized Fields with ``pluck`` and ``distinct``
````````````````````````````````````````````````
Mongoid 7.4 with the ``Mongoid.legacy_pluck_distinct`` option set to ``false``
changes the behavior of using ``pluck`` and ``distinct`` with localized fields.
Now, when retrieving a localized field using these methods, the translation for
the current locale will be returned. To get the full translations hash the
``_translations`` field can be used. Given the following definitions:

.. code-block:: ruby

  class Band
    include Mongoid::Document

    field :name, localize: true
  end

  I18n.locale = :en
  band = Band.create!(name: 'english-name')
  I18n.locale = :de
  band.name = 'deutsch-name'
  band.save!

Mongoid 7.4 behavior with ``Mongoid.legacy_pluck_distinct`` set to ``false``:

.. code-block:: ruby

  Band.pluck(:name)
  # => ["deutsch-name"]
  Band.pluck(:name_translations)
  # => [{"en"=>"english-name", "de"=>"deutsch-name"}, {"en"=>"english-name", "de"=>"deutsch-name"}]

In Mongoid 7.3 and earlier, and in 7.4 with the ``Mongoid.legacy_pluck_distinct``
option set to ``true`` (the default), inputting a localized field returns the
full translations hash. Inputting the ``_translations`` field will return ``nil``.
For example:

.. code-block:: ruby

  Band.pluck(:name)
  # => [{"en"=>"english-name", "de"=>"deutsch-name"}, {"en"=>"english-name", "de"=>"deutsch-name"}]
  Band.pluck(:name_translations)
  # => [nil, nil]

.. note::

  In Mongoid 8.0, the default value of the ``Mongoid.legacy_pluck_distinct`` option will
  change to ``false``.


Embedded Fields with ``pluck``
``````````````````````````````
Mongoid 7.4 with the ``Mongoid.legacy_pluck_distinct`` option set to ``false``
returns the embedded values themselves, i.e. not inside a hash. Given the
following definitions:

.. code-block:: ruby

  class Band
    include Mongoid::Document

    embeds_one :label
  end

  class Label
    include Mongoid::Document

    embedded_in :band
    field :sales, type: BigDecimal
  end

Mongoid 7.4 behavior with ``Mongoid.legacy_pluck_distinct`` set to ``false``:

.. code-block:: ruby

  Band.pluck("label.sales")
  # => [0.1e3]

In Mongoid 7.3 and earlier, and in 7.4 with the ``Mongoid.legacy_pluck_distinct``
option set to ``true`` (the default), plucking embedded attributes returns them
inside a hash. For example:

.. code-block:: ruby

  Band.pluck("label.sales")
  # => [{"sales"=>"1E2"}]

.. note::

  In Mongoid 8.0, the default value of the ``Mongoid.legacy_pluck_distinct`` option will
  change to ``false``.


``update_one`` Warnings in ``upsert``
-------------------------------------

Mongoid 7.4.1 fixes incorrect usage of the driver's ``update_one`` method from
Mongoid's ``upsert`` method. Mongoid's ``upsert`` actually performs a
replacing upsert, and Mongoid 7.4.1 and later correctly call ``replace_one``.