Let's start with a simple domain model without any associations:
class Canvas
include Mongoid::Document
field :name
end
class Browser < Canvas
field :version, :type => Integer
end
Given the above model, my Canvas class would be stored by default in a "canvases" collection in the database. All subclasses of Canvas will share this collection, similar to the way Single Table Inheritance works in ActiveRecord. The underlying implementation is simple: a "_type" field is stored on the document in order for Mongoid to know what class the document is when it performs queries to retrieve them from the database.
As with traditional inheritance, properties of the parent class are available on the child class, but not vise-versa. In the above case Browser would have a field "name", but Canvas would not have a "version" field.
Querying for either is as expected:
Canvas.where(:name => "paper") # Will only return Canvas documents
Browser.where(:name => "firefox") # Will only return Browser documents
Let's get into associations, and expand the above domain model to include some shapes:
class Canvas
include Mongoid::Document
field :name
has_many :shapes
def render
shapes.each { |shape| shape.render }
end
end
class Browser < Canvas
field :version, :type => Integer
end
class Shape
include Mongoid::Document
field :x, :type => Float
field :y, :type => Float
belongs_to :canvas, :inverse_of => :shapes
def render; end
end
class Circle < Shape
field :radius, :type => Float
end
class Rectangle < Shape
field :width, :type => Float
field :height, :type => Float
end
Now we have an embedded collection of shapes that will be stored in the canvas or browser documents. We can add shapes in the normal manner:
browser = Browser.new(:name => "firefox")
circle = Circle.new(:x => 0, :y => 0, :radius => 20)
rectangle = Rectangle.new(:x => 5, :y => 10, :width => 100, :height => 50)
browser.shapes << [circle, rectangle]
The resulting document when saved would resemble the following hash:
{
"_type" => "Browser",
"name" => "firefox",
"shapes" => [
{
"_type" => "Circle",
"x" => 0,
"y" => 0,
"radius" => 20
},
{
"_type" => "Rectangle",
"x" => 5,
"y" => 10,
"width" => 100,
"height" => 50
}
]
}
The methods #create and #build on has many and has one associations now have an additional optional parameter, the type of class to create. If not provided it will create a class that is determined by the association name:
browser = Browser.new(:name => "firefox")
browser.shapes.build({ :x => 0, :y => 0, :radius => 50 }, Circle) # Creates a Circle
browser.shapes.build({ :x => 0, :y => 0 }) # Creates a Shape
And that's it, nice and simple.
