The name MongoDB is derived from humongous. We use DataMappers to work with MongoDB. In Ruby, the most popular MongoDB mapper is Mongoid, pronounced mann-gyod. But is this the only mapper available? There also exists MongoMapper, MongoODM, and in the realm of open source there could well be many more!
So, what is so awesome about Mongoid? In a nutshell, its ability to gel with the Rails framework makes it very popular. For me, it is this adaptability to refactor and improve that makes Mongoid a very close ally of Rails.
In this chapter, we shall get a taste of the power of Mongoid. We shall see:
How Mongoid adheres to Rails ActiveModel and ActiveRelation syntax
Differences between Mongoid Version 2.x and 3.x
A brief introduction to Moped and Origin
Some differences between MongoMapper and Mongoid and their usage
Sodibee (pronounced saw-di-bee) is a library-management system that can manage books, reviews, authors, and bookings. Here are some of the functions that are supported in Sodibee:
An author has many books and a book belongs to an author
A book has many reviews and has one booking
A review belongs to a user and is about a book
A booking belongs to a user and a book
Note
In the course of this book we will be working with the latest versions of Ruby 2.0, Mongoid 4, Rails 4, and MongoDB 2.4.
First and foremost, we need to ensure that we have our development environment set up. It's common to use multiple versions of Ruby for development; I use RVM to manage these versions. As we can have multiple versions of the same gem installed on our machines, we use RVM gemsets to manage the gems we need for our work.
To check the Ruby version, check the version that is installed using the following command:
$ rvm list rvm rubies jruby-1.7.4 [ x86_64 ] ruby-1.9.3-p385 [ x86_64 ] =* ruby-2.0.0—p247 [ x86_64 ] # => - current # =* - current && default # * - default $ ruby –v ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-darwin12.4.0]
First and foremost, let's install the Rails gem.
$ gem install rails
This installs Rails 4.0.0.
Note
At the time of writing this book, the Rails version was 4.0.0. All commands would be fully compatible with the latest Rails version.
Now, let's create the Sodibee project.
$ rails new sodibee -O -T
This creates a new Rails project. The –O
option tells Rails to skip ActiveRecord (we don't need it), and –T
tells Rails to skip test unit. (We plan to use rspec
later).
Tip
When you run the preceding command, it initiates a bundle install and updates our bundle with the default gems. If you are as impatient as I am, you may interrupt the process and press Ctrl + C to stop it, as we need to modify Gemfile
to add other gems anyway.
Now, open Gemfile
and configure for Mongoid.
gem 'mongoid', git: "git://github.com/mongoid/mongoid.git"
Note
Mongoid master is currently in sync with Rails 4. So, if we install using the released gem, it will install Version 3.x.
We're almost done. Issue the following command to update the bundle with our Mongoid gem:
$ bundle
If you did not use the –O
option, you can run the following instructions to remove ActiveRecord from the application as we don't need it. Check and remove database.yml
under config
, if it has been generated. Next ensure that application.rb
under config
has the following lines:
require File.expand_path('../boot', __FILE__) require "action_controller/railtie" require "action_mailer/railtie" require "sprockets/railtie" # Assets should be precompiled for production (so we don't need the gems loaded then) Bundler.require(*Rails.groups(assets: %w(development test)))
Tip
Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
Notice that there is no require "rails/all"
. This ensures that the ActiveRecord
railtie is not loaded. However, sometimes this causes a conflict with the environment settings. So, in case you face a problem starting the Rails console, remove the following line from development.rb
under config/environments
(and as required from the other environment files):
# config.active_record.migration_error = :page_load
This should get us going. Now issue the following command to set up Mongoid:
$ bundle exec rails generate mongoid:config
This generates mongoid.yml
under config
.
Test this basic Rails setup by starting the console.
$ rails c Loading development environment (Rails 4.0.0) 2.0.0-p247 :001 >
If you see the preceding command prompt, we are set.
Now that we have our environment set up, let's create our basic models. In the Author
model, we shall now add a field called name
, and create a relation between the Author
model and the Address
model.
$ rails generate model Author # app/models/author.rb class Author include Mongoid::Document include Mongoid::Attributes::Dynamic field :name, type: String embeds_one :address end
Now, let's create the Address
model with a number of fields and relations.
$ rails generate model Address class Address include Mongoid::Document field :street, type: String field :city, type: String field :state, type: String field :zipcode, type: String field :country, type: String embedded_in :author end
Now that we have created the models, let's test it out quickly:
irb> a = Author.create(name: "Charles Dickens") => #<Author _id: 5143678345db7ca255000001, name: "Charles Dickens"> irb> a.create_address(street: "Picadilly Circus", city: "London", country: "UK") => #<Address _id: 514367f445db7ca255000003, street: "Picadilly Circus", city: "London", state: nil, zipcode: nil, country: "UK">
As we can see, this creates an Author
object and its corresponding Address
object. Mongoid includes ActiveModel and you may notice the similarity in these methods if you have used ActiveRecord.
Tip
We have used create_address
because an author has only one embedded address. If, an author had multiple addresses, we would have used a.addresses.create
.
irb> Author.first => #<Author _id: 5143678345db7ca255000001, name: "Charles Dickens"> irb> Author.first.address => #<Address _id: 514367f445db7ca255000003, street: "Picadilly Circus", city: "London", state: nil, zipcode: nil, country: "UK">
Here, we have double-checked that the author is indeed persisted to the database. Since this is MongoDB, we can dynamically add attributes to the object!
irb> a['language'] = "English" => "English" irb> a.save => true irb> Author.first => #<Author _id: 5143678345db7ca255000001, name: "Charles Dickens", language: "English">
So, let's see what happened in Mongoid and MongoDB. First, let's see what is in the log file development.log
under log
.
When we issued the command Author.create(name: "Charles Dickens")
, it generated the following output:
MOPED: 127.0.0.1:27017 INSERT database=sodibee_development collection=authors documents=[{"_id"=>"5143678345db7ca255000001", "name"=>"Charles Dickens"}] flags=[] (0.2460ms)
Now, when we issued the second command a.create_address(street: "Picadilly Circus", city: "London", country: "UK")
, it updated the Author
object, and created an embedded Address
document as seen in the following line:
MOPED: 127.0.0.1:27017 UPDATE database=sodibee_development collection=authors selector={"_id"=>"5143678345db7ca255000001"} update={"$set"=>{"address"=>{"_id"=>"514367f445db7ca255000003", "street"=>"Picadilly Circus", "city"=>"London", "country"=>"UK"}}} flags=[] (0.1211ms)
Now that we have seen what INSERT
and UPDATE
look like, querying the Author collection with Author.first
generates the following result:
MOPED: 127.0.0.1:27017 QUERY database=sodibee_development collection=authors selector={"$query"=>{}, "$orderby"=>{:_id=>1}} flags=[:slave_ok] limit=-1 skip=0 batch_size=nil fields=nil (66.7090ms)
And since we want to query the address, we look it up using Author.first.address
. This generates the following line:
MOPED: 127.0.0.1:27017 QUERY database=sodibee_development collection=authors selector={"$query"=>{}, "$orderby"=>{:_id=>1}} flags=[:slave_ok] limit=-1 skip=0 batch_size=nil fields=nil (0.5021ms)
Now there's something interesting about the preceding output—the last two commands on the Author
model fired the same query, and look at the difference in the query result! The same query is fired because the address is an embedded document. So, to fetch the address of an author, you fetch the Author
object itself. The difference of 66 ms and 0.5 ms in the query response is because for the first lookup MongoDB loads the document from the disk and puts it into its memory-mapped file. The second time, the document is simply looked up in cache (the memory-mapped file) and hence the lookup is faster.
When we issued the command a['language'] = "English"
, and saved the object using a.save
; this is what we see:
MOPED: 127.0.0.1:27017 UPDATE database=sodibee_development collection=authors selector={"_id"=>"5143678345db7ca255000001"} update={"$set"=>{"language"=>"English"}} flags=[] (0.1121ms)
This is the result of dynamic attribute update. Even though we did not specify language
as a field in the Author
model, we can set it as an attribute for the Author
object. Did you notice that the update for dynamic attributes is no different from the standard update query in MongoDB?
However, there is a difference when accessing it in Mongoid. The Author.first.language
parameter may throw an error sometimes, but Author.first[:language]
will always succeed. Let's see an example:
irb> a = Author.create(name: "Gautam") => #<Author _id: 515085fd45db7c911e000003, name: "Gautam">
Here we have created a new Author
object. However, when we try to update the object using the dot notation a.language
, it gives an error. As we can see in the following command lines, method_missing
does not dynamically create the accessor method if the dynamic attribute does not already exist.
irb> a.language = "English" NoMethodError: undefined method `language=' for #<Author _id: 515085fd45db7c911e000003, name: "Gautam"> from lib/mongoid/attributes.rb:317:in `method_missing' from (irb):12 from lib/rails/commands/console.rb:88:in `start' from lib/rails/commands/console.rb:9:in `start' from lib/rails/commands.rb:64:in `<top (required)>' from bin/rails:4:in `require' from bin/rails:4:in `<main>'
Now, if we try to update the dynamic attribute without using the dot notation, it works!
irb> a[:language] = "English" => "English" irb> a.save => true
Since we have saved it now, when we access the dynamic attribute language
again, method_missing
creates the accessor method because the dynamic attribute exists. So, now even the dot notation works.
irb> a.language => "English" irb> a[:language] => "English"
Origin is a gem that provides the DSL for Mongoid queries. Though at first glance, a question may seem to arise as to why we need a DSL for Mongoid queries; If we are finally going to convert the query to a MongoDB-compliant hash, then why do we need a DSL?
Origin was extracted from Mongoid gem and put into a new gem, so that there is a standard for querying. It has no dependency on any other gem and is a standalone pure query builder. The idea was that this could be a generic DSL that can be used even without Mongoid!
So, now we have a very generic and standard querying pattern. For example, in Mongoid 2.x we had the criteria any_in
and any_of
, and no direct support for the and
, or
, and nor
operations. In Mongoid 2.x, the only way we could fire a $or
or a $and
query was like this:
Author.where("$or" => {'language' => 'English', 'address.city' => 'London '})
And now in Mongoid 3, we have a cleaner approach.
Author.or(:language => 'English', 'address.city' => 'London')
Origin also provides good selectors directly in our models. So, this is now much more readable:
Book.gte(published_at: Date.parse('2012/11/11'))
As we shall see later in the book, Origin has a lot more cool features.
It's important to realize that quite a bit has changed between the two major releases of Mongoid. It's an uncanny coincidence that just like Rails, major versions (2.x and 3.x) differ from each other vastly. Let's take a look at some of the differences between Mongoid 2.x and Mongoid 3.x.
Mongoid 3.x and later versions support Ruby 1.9.3 and onwards only.
There are entirely new options in
mongoid.yml
for database configuration.Mongoid has changed its serialization mechanism.
Apart from these, there are quite a few more changes, which have made using Mongoid better and faster.
It's also important to know about the other Ruby ODMs (Object Document Managers) besides Mongoid. Among the most popular ones out there, is MongoMapper.
MongoMapper, though older than Mongoid, has a slower evolution cycle. Unlike Mongoid, MongoMapper differentiates between documents and embedded documents.
MongoMapper uses a non-ActiveModel syntax such as
many
andone
, where Mongoid useshas_many
andhas_one
.Mongoid has currently become more popular because of its adherence to the Rails ActiveModel and ActiveRelation syntax.
Mongoid has much better documentation than MongoMapper.
Mongoid has power. Just like Rails, it evolves fast. It has Moped and Origin that ensure speed, flexibility, and consistency.
In the upcoming chapters, we shall see what documents are, in the context of Mongoid. We shall learn about the different data types, dynamic attributes, and how data is managed.
The journey has just begun.