Rails 3, MongoDB, Mongoid, & Embedded Documents
So, I've been diving head first into MongoDB lately. It's awesome and quickly becoming my go-to database set up.
One of the nice things about a document database like MongoDB, is the notion of embedded documents. Yesterday I wanted to achieve modeling embedded documents within my rails application and found the documentation a little sparse. After succeeding I figured I should write a little post.
If you're unclear about what exactly embedded documents are, keep reading, it *should* be come clear as long as I don't write like a 4 year old.
For this example I will be assuming a that you have a working knowledge of Ruby on Rails. It's also important to note that I am using Mongoid as my MongoDB connector.
Let's begin and for this tutorial pretend we are making a site about dogs. One thing we *should* all be able to agree on is that a dog can have many breeds (think mutt). This means that a dog can have or has many breeds.
Let's start with the models
# Dog Model - dog.rb class Dog include Mongoid::Document field :name, :type => String field :weight, :type => String field :height, :type => String embeds_many :breeds accepts_nested_attributes_for :breeds end
Notice the embeds_many :breeds. That tells the dog model that it will accept an array of hashes that will represent breeds.
# Breed Model - breed.rb class Breed include Mongoid::Document field :breed_name, :type => String field :breed_origin, :type => String field :breed_personality, :type => String embedded_in :dog, :inverse_of => :needs end
The big thing to note in the breed.rb file is the embedded_in :dog macro. It's required for the relationship to work properly.
# Dog Controller - dog_controller.rb
def new
@dog = Dog.new
@dog.breeds.build
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @dog }
end
end
It's subtle, it's easy, but it's also easy to miss. Notice the @dog.breeds.build statement? That simple line prepares the controller for the association.
<%= form_for(@dog) do |f| %> <% if @dog.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(@dog.errors.count, "error") %> prohibited this dog from being saved:</h2> <ul> <% @dog.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :weight %><br /> <%= f.text_field :weight %> </div> <div class="field"> <%= f.label :height %><br /> <%= f.text_field :height %> </div> <%= # Don't miss the subtleties here. # This builds the part of the form that embeds breeds. f.fields_for :breeds do |b| %> <div class="field"> <%= b.label :name %><br /> <%= b.text_field :name %> </div> <div class="field"> <%= b.label :weight %><br /> <%= b.text_field :weight %> </div> <div class="field"> <%= b.label :breed_personality %><br /> <%= b.text_field :breed_personality %> </div> <% end %> <div class="actions"> <%= f.submit %> </div> <% end %>
Above is a standard rails form used in creating and updating objects. The important part of this file is f.fields_for : breeds loop. That builds out the relevant form elements for embedding a breed (or two) inside the dog object.
After submitting the form above (with various data) you could see MongoDB return the following:
{ "_id" : ObjectId("4d5302e28b21a75180000009"), "name" : "Bailey", "weight" : "180lbs", "height" : "36\"", "_type" : "Dog", "breeds" : [
{
"name" : "German Shepherd",
"breed_origin" : "Germany",
"breed_personality" : "Loyal, Protective, Intelligent",
"_id" : ObjectId("4d5302e28b21a75180000007"),
"_type" : "Breed"
},
{
"name" : "Labrador",
"breed_origin" : "Unknown",
"breed_personality" : "Loving",
"_id" : ObjectId("4d5302e28b21a75180000008"),
"_type" : "Breed"
}
] }
And there you have it - two breeds embedded inside of the dog object. I hope this helps someone out there. Once you implement embeds_many using macros like embeds_one is simple.
The Mongoid web site has good documentation on associations, however they are by no means complete. I should mention though, without them, I wouldn't have been able to figure this out as quickly as I did.
thanks -