Blog @ RohitRox

tips tricks tuts and everything learned

Single Table Inheritance in Mongoid

| Comments

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
    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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    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.

1
2
3
4
5
6
7
8
9
10
11
12

  # 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