homeASCIIcasts

238: Mongoid 

(view original Railscast)

Other translations: Es Pt

A few months ago Episode 194 [watch, read] covered MongoDB and MongoMapper. MongoMapper is a great gem but there is now an alternative called Mongoid that is well worth considering if you’re thinking of using MongoDB as the back-end for a Rails application. One of the things that makes Mongoid stand out is its website which looks great and which has detailed documentation. Mongoid is a project that other open-source projects can learn from.

Installing MongoDB

If you haven’t yet installed MongoDB on your system the first thing you’ll need to do is go to the MongoDB downloads page and download the appropriate files. If you’re using Mac OS X then you can install MongoDB via Homebrew instead. Once installed, you can check that MongoDB is working by visiting http://localhost:28017. If you see a page like the one below then everything is running correctly.

The page we see when MongoDB is running correctly.

Creating a new Rails Application With Mongoid

Now that we have MongoDB installed we’ll create a new Rails 3 application that uses Mongoid. In the great tradition of example Rails applications this will be a blogging app.

$ rails new blog

The first thing we need to do is to add the Mongoid gem to the Gemfile. At the time of writing version 2, which is the version that supports Rails 3, is still in beta so we’ll need to specify the version number.

/Gemfile

source 'http://rubygems.org'
gem 'rails', '3.0.1'
gem 'sqlite3-ruby', :require => 'sqlite3'
gem 'mongoid', '2.0.0.beta.19'
gem 'bson_ext'

We also need to add the bson_ext gem. BSON is a binary version of JSON and this gem provides some C extensions to accelerate Ruby’s BSON serialization. We can then install the gems in the usual way

$ bundle

Once the gems have installed we’ll need to run the Mongoid configuration generator so that it can create the configuration YAML file.

$ rails g mongoid:config

The default file is shown below. We can leave it as it is while we’re developing our application.

/config/mongoid.yml

defaults: &defaults
  host: localhost
  # slaves:
  #   - host: slave1.local
  #     port: 27018
  #   - host: slave2.local
  #     port: 27019
development:
  <<: *defaults
  database: blog_development
test:
  <<: *defaults
  database: blog_test
# set these environment variables on your prod server
production:
  host: <%= ENV['MONGOID_HOST'] %>
  port: <%= ENV['MONGOID_PORT'] %>
  username: <%= ENV['MONGOID_USERNAME'] %>
  password: <%= ENV['MONGOID_PASSWORD'] %>
  database: <%= ENV['MONGOID_DATABASE'] %>

Everything is in place now for us to begin building our application. We’ll start by creating an Article model with name and content fields and use Rails’ scaffolding to create the associated controller and view code.

$ rails g scaffold article name:string content:text
    invoke  mongoid
    create    app/models/article.rb

Mongoid provides generators for models so that Mongoid’s model generator is invoked when a model is created and therefore ActiveRecord isn’t used. If we open up the model file we’ll see that it’s a simple class that includes Mongoid::Document.

/app/models/article.rb

class Article
  include Mongoid::Document
  field :name, :type => String
  field :content, :type => String
end

One difference this class has from an ActiveRecord model class is that each field is explicitly defined, along with its type. The default type is String so we can remove the types from this class if we want.

/app/models/article.rb

class Article
  include Mongoid::Document
  field :name
  field :content
end

Our application is now ready to run. We don’t need to run any database migrations as MongoDB is schema-less and we can pass any fields we like into a document. If we visit the /articles page we’ll see the usual scaffold-generated page and we can create a new Article just like we would if we had an ActiveRecord back-end.

The articles page after a new article has been created.

Adding Fields

One of the great advantages of using a schema-less database is that it’s simple to add new fields to a model. Let’s say that we forgot to add a publishing date to Article. To add one we just need to modify the model class.

/app/models/article.rb

class Article
  include Mongoid::Document
  field :name
  field :content
  field :published_on, :type => Date
end

To enable us to view and modify the publishing date field we’ll modify the view code and add the new field to the form.

/app/views/articles/_form.html.erb

 <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :published_on %><br />
    <%= f.date_select :published_on %>
  </div>
  <div class="field">
    <%= f.label :content %><br />
    <%= f.text_area :content %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>

And also the view for the show action.

/app/views/articles/show.html.erb

<p id="notice"><%= notice %></p>
<p>
  <b>Name:</b>
  <%= @article.name %>
</p>
<p>
  <b>Content:</b>
  <%= @article.content %>
</p>
<p>
  <b>Published:</b>
  <%= @article.published_on %>
</p>
<%= link_to 'Edit', edit_article_path(@article) %> |
<%= link_to 'Back', articles_path %>

We can now edit the article we just created and add a published-on date to it.

Adding a published-on date to an article.

Validations

Mongoid used ActiveModel which means that a lot of the functionality we use in ActiveRecord is available to us here as well, for example validations, callbacks, dirty tracking, attr_accessible and so on. This makes it as easy to add validations to Mongoid models as ActiveRecord ones.

/app/models/article.rb

class Article
  include Mongoid::Document
  field :name
  field :content
  field :published_on, :type => Date
  validates_presence_of :name
end

If we try to create an article without a name now we’ll get the same validation errors we’d see for an equivalent ActiveRecord model.

Validation errors work just as they do with ActiveRecord models.

Associations

We can’t have a blog without comments, so we’ll create a Comment model along association so that each article can have many comments. There are two ways to define associations with Mongoid. The first is through a reference association. This behaves in a similar way to the relations between tables in ActiveRecord and relational databases in that there are two separate records that are related through an id column. The other way is an embedded association which would mean in this case that the comments are embedded inside the same document as the article.

When you’re deciding which of these approaches is to use you need to ask yourself if you’ll ever need the associated records to stand on their own or if you’ll always be accessing them through their parent model. In this case we’ll only ever be getting comments through their associated article so we’ll use an embedded association. We define the relationship in the Article class by using the embeds_many method.

/app/models/article.rb

class Article
  include Mongoid::Document
  field :name
  field :content
  field :published_on, :type => Date
  validates_presence_of :name
  embeds_many :comments
end

Next we’ll generate the Comment model.

$ rails g model comment name:string content:text

In this new Comment class we can now define its relationship with Article.

/app/models/comment.rb

class Comment
  include Mongoid::Document
  field :name
  field :content
  embedded_in :article, :inverse_of => :comments
end

We use embedded_in to define a comment’s relationship to an article. The inverse_of option is required in order to tell Mongoid what the comment should be embedded through.

In order to create comments for each article and to view them we’ll need to create a CommentsController and some views, but before we do so we’ll need to alter the routes file. For embedded associations like this we generally want to use nested routes as the child object is always accessed through its parent and so we’ll nest the comments resource under articles.

/config/routes.rb

Blog::Application.routes.draw do
  resources :articles do
    resources :comments
  end
end

Next we’ll generate the controller.

$ rails g controller comments

In the controller we’ll write a create action so that we can create new comments for an article. This action will find an article based on the article_id parameter, create a comment for that article and then redirect back to the article’s page.

/app/controllers/comments_controller.rb

CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create!(params[:comment])
    redirect_to @article, :notice => "Comment created!"  
  end
end

Finally we’ll add some code to the articles’ show view that will show the comments for an article and allow comments to be added.

/app/views/articles/show.html.erb

<% if @article.comments.size > 0 %>
  <h2>Comments</h2>
  <% for comment in @article.comments %>
    <h3><%= comment.name %></h3>
    <p><%= comment.content %></p>
  <% end %>
<% end %>
<h2>New Comment</h2>
<%= form_for [@article, Comment.new] do |f| %>
  <p><%= f.label :name %> <%= f.text_field :name %></p>
  <p><%= f.text_area :content, :rows => 10 %></p>
  <p><%= f.submit %></p>
<% end %>

When we visit an article’s page now we’ll be able to create a new comment and after we have submitted it it will be shown below the article.

Adding a comment to an article.

If we take a look at the development log we can see the MongoDB queries. When we created a comment just now, this query was made.

MONGODB blog_development['articles'].update({"_id"=>BSON::ObjectId('4cd01fa4a74209eacc000003')}, 
{"$push"=>{"comments"=>{"_id"=>BSON::ObjectId('4cd04c74a74209ecb4000002'), 
  "name"=>"Eifion", "content"=>"I agree."}}})

The query updates an article model and adds a comment attribute to it so the comments are not stored separately. This means that if we open up the Rails console and count all the comments we’ll get an unexpected result.

> Comment.count
 => 0

The comments we have are embedded in articles and are not available at a global document level. To access them we always have to go through their associated article.

>   Article.first.comments.count
 => 1

To get at the attributes for a comment we’ll always need to get that comment through the association as in this application comments are embedded records.

Reference Type Associations

If we want an associated record that is also available as a separate document then we'll need to create a reference type association. We’ll demonstrate this by modifying our application so that we can associate each article with an author. First we’ll generate a scaffold for a new Author model.

$ rails g scaffold author name:string

Before we demonstrate associations we’ll take a quick look at a cool feature of Mongoid. If we add a key method to Mongoid model class that key will be used as the id to identify that model. We’ll make the name attribute the key for Author.

/app/models/author.rb

class Author
  include Mongoid::Document
  field :name
  key :name
end

If we create an author now, when we’re redirected to that author’s page we’ll see the id in the URL.

The page for an author showing the name as an id.

If we’re going to use this feature then we’ll need to ensure the field we choose to use as a key isn’t editable so that the document has a permanent string as an id that won’t change throughout the life of the document.

Back to associations now. In the Author class we use references_many to define the relationship with articles.

/app/models/author.rb

class Author
  include Mongoid::Document
  field :name
  key :name
  references_many :articles
end

Then in the Article model we use referenced_in.

/app/models/article.rb

class Article
  include Mongoid::Document
  field :name
  field :content
  field :published_on, :type => Date
  validates_presence_of :name
  embeds_many :comments
  referenced_in :author
end

We can now use that association like we would in ActiveRecord. In our form for editing an article we can add a collection_select so that we can select an author when we create or update an article.

/app/views/articles/_form.html.erb

<div class="field">
  <%= f.label :author_id %><br />
  <%= f.collection_select :author_id, Author.all, :id, :name %>
</div>

If we modify our article now and select an author we’ll can see that author’s id embedded in the article when we examine it in the console.

> Article.first
 => #<Article _id: 4cd01fa4a74209eacc000003, name: "Mongoid", content: "it's awesome!", published_on: 2010-11-02 00:00:00 UTC, author_id: "eifion-bedford">

Unlike the comments association, however, we can access the author separately.

> Author.first
 => #<Author _id: eifion-bedford, name: "Eifion Bedford">

That’s it for this episode on Mongoid. There’s much that we haven’t covered here but the documentation is fairly comprehensive and will give you nearly all that you need to know to use Mongoid and MongoDB in your Rails application.