Docs Menu

Docs HomeMongoid

Inheritance

On this page

  • Overview
  • Changing the Discriminator Key
  • Changing the Discriminator Value
  • Querying Subclasses
  • Associations
  • Persistence Contexts

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.

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.

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:

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:

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:

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.

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:

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

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

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:

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

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:

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:

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

←  Field DefinitionAssociations →