mongodb/mongoid

View on GitHub
docs/reference/inheritance.txt

Summary

Maintainability
Test Coverage
.. _inheritance:

***********
Inheritance
***********

.. default-domain:: mongodb

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


.. _inheritance-overview:

Overview
========

Mongoid supports inheritance in both top level and embedded documents. When
a child document inherits from a parent document, the parent document's
fields, associations, validations and scopes are copied to the child document.

.. code-block:: ruby

   class Canvas
     include Mongoid::Document
     field :name, type: String
     embeds_many :shapes
   end

   class Browser < Canvas
     field :version, type: Integer
     scope :recent, ->{ where(:version.gt => 3) }
   end

   class Firefox < Browser
   end

   class Shape
     include Mongoid::Document
     field :x, type: Integer
     field :y, type: Integer
     embedded_in :canvas
   end

   class Circle < Shape
     field :radius, type: Float
   end

   class Rectangle < Shape
     field :width, type: Float
     field :height, type: Float
   end

In the above example, ``Canvas``, ``Browser`` and ``Firefox`` will all save in the canvases
collection. An additional attribute ``_type`` is stored in order to make sure when loaded
from the database the correct document is returned. This also holds true for the embedded
documents ``Circle``, ``Rectangle``, and ``Shape``.

.. note::

    When searching for a ``Circle``, the query will only return documents in the shape collection
    where the ``_type`` (or whatever the discriminator key was set to) field has the value ``Circle`` (or
    whatever the discriminator value was set to), all other discriminator values will be considered an object
    of the Shape class.

    Similarly, when querying by parent classes (``Canvas`` in this example), any documents in the collection
    that do not have a discriminator value, or whose discriminator value does not map to either the parent
    or any of its descendants, will be returned as instances of the parent class.


.. _discriminator-key:

Changing the Discriminator Key
==============================

Mongoid supports changing the discriminator key from the default ``_type``. There are a few
cases where one might want to do this:

1. For optimization: The user might want to use a shorter key like ``_t``.

2. When trying to work with an existing system: It's possible the user is working with an existing system or dataset that has predefined keys.


There are two ways to change the discriminator key, on the class level and on the global level.
To change the discriminator key on the class level the user can set it directly on the parent class using
the ``discriminator_key=`` method.
Take the above example:

.. code-block:: ruby

   class Shape
     include Mongoid::Document
     field :x, type: Integer
     field :y, type: Integer
     embedded_in :canvas

     self.discriminator_key = "shape_type"
   end

   class Circle < Shape
     field :radius, type: Float
   end

   class Rectangle < Shape
     field :width, type: Float
     field :height, type: Float
   end

Here a call to the ``discriminator_key=`` setter was added to the parent class. Now, on
creation of a Rectangle or Circle, a ``shape_type`` field will be added.

Note that the discriminator key can only be modified in the parent class, and an error
will be raised if trying to set it on the child class.

If the discriminator key is changed after the child class is created, a new field is
added with the new discriminator key value, and the old field will remain unchanged.
For example:

.. code-block:: ruby

   class Shape
     include Mongoid::Document
     field :x, type: Integer
     field :y, type: Integer
     embedded_in :canvas
   end

   class Circle < Shape
     field :radius, type: Float
   end

   class Rectangle < Shape
     field :width, type: Float
     field :height, type: Float
   end

   Shape.discriminator_key = "shape_type"

In this case, on creation of a Rectangle or Circle, there will be both a ``shape_type``
and a ``_type`` field that both default to ``Rectangle`` or ``Circle`` respectively.


The discriminator key can also be set on the global level. Meaning, all classes will
use the globally set discriminator key instead of ``_type``. Take the above example:

.. code-block:: ruby

   Mongoid.discriminator_key = "_the_type"

   class Shape
     include Mongoid::Document
     field :x, type: Integer
     field :y, type: Integer
     embedded_in :canvas
   end

   class Circle < Shape
     field :radius, type: Float
   end

   class Rectangle < Shape
     field :width, type: Float
     field :height, type: Float
   end

After setting the global discriminator key, all classes will use ``_the_type`` as
the discriminator key and will not contain a ``_type`` field.

Note that when defining the discriminator key on the global level, it must be set before the
child class is defined for the child class to use that global value.
On the global level, however, if the user does not set the discriminator key before defining a child
class, the discriminator field will use the default ``_type`` and not the new global setting in
that child class.


.. _discriminator-value:

Changing the Discriminator Value
================================

Mongoid also supports changing the discriminator value from the default value, which is the class name.
One can change the discriminator value by using the ``discriminator_value=`` method on that specific class.

Take the above example:

.. code-block:: ruby

   class Shape
     include Mongoid::Document
     field :x, type: Integer
     field :y, type: Integer
     embedded_in :canvas
   end

   class Circle < Shape
     field :radius, type: Float

     self.discriminator_value = "round thing"
   end

   class Rectangle < Shape
     field :width, type: Float
     field :height, type: Float
   end

Here, a call to the ``discriminator_value=`` setter was added to ``Circle``.
Now, on creation of a ``Circle``, the document will contain a field with the key ``_type`` (or whatever the ``discriminator_key`` was changed to)
and the value "round thing."

.. note::

  Because the discriminator value overrides are declared in child classes,
  the child classes potentially found by a query must be loaded prior to
  sending that query. In the above example, the ``Circle`` class definition
  must be loaded when querying on ``Shape`` if the returned documents could
  potentially be instances of ``Circle`` (since autoloading wouldn't resolve
  ``"round thing"`` to ``Circle``).


Querying Subclasses
===================

Querying for subclasses is handled in the normal manner, and although the documents are
all in the same collection, queries will only return documents of the correct type,
similar to Single Table Inheritance in ActiveRecord.

.. code-block:: ruby

   # Returns Canvas documents and subclasses
   Canvas.where(name: "Paper")
   # Returns only Firefox documents
   Firefox.where(name: "Window 1")


Associations
============

You can add any type of subclass to a has one or has many association, through
either normal setting or through the build and create methods on the association:

.. code-block:: ruby

   firefox = Firefox.new
   # Builds a Shape object
   firefox.shapes.build({ x: 0, y: 0 })
   # Builds a Circle object
   firefox.shapes.build({ x: 0, y: 0 }, Circle)
   # Creates a Rectangle object
   firefox.shapes.create({ x: 0, y: 0 }, Rectangle)

   rect = Rectangle.new(width: 100, height: 200)
   firefox.shapes


.. _inheritance-persistence-context:

Persistence Contexts
====================

Mongoid allows the persistence context of a subclass to be changed from the
persistence context of its parent. This means that, using the ``store_in``
method, we can store the documents of the subclasses in different collections
(as well as different databases, clients) than their parents:

.. code:: ruby

  class Shape
    include Mongoid::Document
    store_in collection: :shapes
  end

  class Circle < Shape
    store_in collection: :circles
  end

  class Square < Shape
    store_in collection: :squares
  end

  Shape.create!
  Circle.create!
  Square.create!

Setting the collection on the children causes the documents for those children
to be stored in the set collection, instead of in the parent's collection:

.. code:: javascript

  > db.shapes.find()
  { "_id" : ObjectId("62fe9a493282a43d6b725e10"), "_type" : "Shape" }
  > db.circles.find()
  { "_id" : ObjectId("62fe9a493282a43d6b725e11"), "_type" : "Circle" }
  > db.squares.find()
  { "_id" : ObjectId("62fe9a493282a43d6b725e12"), "_type" : "Square" }

If the collection is set on some of the subclasses and not others, the subclasses
with set collections will store documents in those collections, and the
subclasses without set collections will be store documents in the parent's
collection.

.. note::

  Note that changing the collection that a subclass is stored in will cause
  documents of that subclass to no longer be found in the results of querying
  its parent class.