Single Table Inheritance in Mongoid

Single table inheritance is a software pattern described by Martin Fowler. STI is basically an idea of using a single table(colection in case of mongo) to reflect multiple models that inherit from a base model.

We use STI pattern when we are dealing with classes that have same attributes and behaviour. Rather than duplicate the same code over and over, STI helps us to use a common base model and write specific behaviours in its inherited class while keeeping data on a same table.

Mongoid supports inheritance in both root and embedded documents. In scenarios where documents are inherited from their fields, relations, validations and scopes get copied down into their child documents, but not vice-versa.

A very simple example:

  class Employee
    include Mongoid::Document
    field :name, type: String
    field :employee_code, type: Integer
  end

  class FullTimeEmployee < Employee
    field status, type: String, default: "Temporary"
  end

  class InternEmployee < Employee
    field intern_period, type: Integer
  end

In the above example, both FullTimeEmployee and InternEmployee will be saved in the Employee collection. An additional attribute _type is automatically stored in order to make sure when loaded from the database the correct document is returned.

Following behaviour can be seen, much similar to Single Table Inheritance in ActiveRecord.

  emp1 = Employee.new
  emp2 = FullTimeEmployee.new
  emp3 = InternEmployee.new

  Employee.count
  #=> 3
  FullTimeEmployee.count
  #=> 1
  InternEmployee.count
  #=> 1
  emp1._type
  #=> "Employee"
  emp2._type
  #=> "FullTimeEmployee"
  emp3._type
  #=> "InternEmployee"

  Employee.where(name: "...")
  #=> Returns Employee documents and subclasses
  FullTimeEmployee.where(name: "...")
  #=> returns only FullTimeEmployee documents

An advance example:

  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

Canvas, Browser and Firefox will all save in the canvases collection.This also holds true for the embedded documents Circle, Rectangle, and Shape.

To query for subclasses within an embedded collection you need to leverage the _type attribute in each subclassed object. Canvas and Shape documents, would not have it, but Browser, Firefox, Circle, and Rectangle would. Keep in mind that _type is a string that stores the name of the document’s class, and as such can only be used to query for a specific subclass, and not anything it is a subclass of.

If, for example, Rectangle was a subclass of Parallelogram which was in turn a subclass of Shape, you could search the Canvas’s shapes collection for objects with a _type of “Parallelogram” but it would never return a Rectangle object, and vice-versa.

  # Returns all the Rectangle shapes in a previously found Canvas
  my_canvas.shapes.where(_type: "Rectangle")

  # Returns no entries (see above)
  my_canvas.shapes.where(_type: "Shape")

  # Returns all the Canvasas that have Circles
  Canvas.where("shapes._type"=>"Circle")

  # Returns no entries (see above)
  Canvas.where("shapes._type'=>"Shape")

Comments