#238 Mongoid (revised)
- Download:
- source codeProject Files in Zip (41.8 KB)
- mp4Full Size H.264 Video (16.2 MB)
- m4vSmaller H.264 Video (9.5 MB)
- webmFull Size VP8 Video (11.4 MB)
- ogvFull Size Theora Video (17.3 MB)
If you’re looking for a NoSQL database option for a Rails application, Mongoid is an excellent choice. With version 3 recently released it’s now better than ever and in this episode we’ll cover its basics while building a new Rails application.
Installing MongoDB and Mongoid
First we need to install the MongoDB database. Its website has installation instructions for a number of platforms, but if you’re running Mac OS X the easiest way to install it is to use Homebrew3. With it we can install MongoDB by running
$ brew install mongodb
This will download and install MongoDB. When it completes it provides some instructions for starting it up. We’ll use the following command to start it manually.
$ mongod run --config /usr/local/etc/mongod.conf
We can test that the database is up and running by visiting http://localhost:28017
where we should see MongoDB’s Web interface.
Creating a Rails Application With Mongoid
Now that we have MongoDB up and running we’ll create a new Rails application called store
. As we want to use Mongoid instead of ActiveRecord we’ll use the skip-active-record
option so that it doesn’t load the framework or generate files such as the database configuration file.
$ rails new store --skip-active-record
Next we’ll add the Mongoid gem to the gemfile. To make sure that we’re running the latest version we’ll specify the version number and, as ever, we’ll need to run bundle afterwards to install it.
gem 'mongoid', '~>3.0.4'
This gem provides a generator that we can use to generate a configuration file.
$ rails g mongoid:config
We’ll leave the config options at their defaults but the file is well commented and it’s worth reading through it to see all of the options we can set. Next we’ll generate the scaffolding for a Product
resource with name
and price
attributes. Note that the price field has a type of big_decimal
as this is the correct type for MongoDB.
$ rails g scaffold product name price:big_decimal
Mongoid overrides the model generator and this custom generator is used when we generate our model. If we look at the model file we’ll see how a Mongoid model looks. This is a simple Ruby class that includes Mongoid::Document
and which calls field for each attribute we want in our model.
class Product include Mongoid::Document field :name, type: String field :price, type: BigDecimal end
If we visit our Rails app we’ll see that the scaffolding that’s generated is fully-functional. We can create new products and they’ll be saved to the database. There’s no need to run any migrations or set up a database.
Altering a Model
One thing that’s good about working with a schemaless database is its flexibility. If we want to keep track of when each product was released we only need to add another call to field.
class Product include Mongoid::Document field :name, type: String field :price, type: BigDecimal field :released_on, type: Date end
To see this field being used we’ll need to add a released_on
field to the form.
<div class="field"> <%= f.label :released_on %><br /> <%= f.text_field :released_on %> </div>
We’ve used a text field here as if we try to use a date_select
we’ll run into issues with Mongoid. We need to add a module to the model to get date_select
working and the documentation has details on how to do this. When we edit a product now we’ll have a new text field for the date and if we add a date to a product it will be persisted to the database. Again there’s no need for any migrations.
Adding Validations
Mongoid uses ActiveModel so much of what we know from ActiveRecord will work here. We can use attr_accessible
to define the fields that can be set through mass-assignment and use validations like we would if we were using ActiveRecord.
class Product include Mongoid::Document field :name, type: String field :price, type: BigDecimal field :released_on, type: Date attr_accessible :name, :price, :released_on validates_presence_of :name end
If we update a product now and leave the name blank we’ll see an error message.
Querying MongoDB
One area where Mongoid differs from ActiveRecord is when querying for records. We’ll demonstrate this in the console. We can still call where on a model and pass in conditions but Mongoid allows us to pass in more complex conditions and we’ll use these to find the products that cost £40 or less.
>> Product.where(:price.lte => 40).first => #<Product _id: 502952b1a175752aa0000001, _type: nil, name: "Settlers of Catan", price: "34.99", released_on: 2012-01-01 00:00:00 UTC>
We can write the same query by using a conditional method on the class directly.
>> Product.lte(price: 40).first => #<Product _id: 502952b1a175752aa0000001, _type: nil, name: "Settlers of Catan", price: "34.99", released_on: 2012-01-01 00:00:00 UTC>
The object returned by the query call is a Mongoid::Criteria
instance and this is very similar to an ActiveRecord::Relation
object. This means that we can chain queries together and it will do lazy loading.
>> Product.lte(price: 40).gt(released_on: 1.month.ago) => #<Mongoid::Criteria selector: {"price"=>{"$lte"=>"40"}, "released_on"=>{"$gt"=>Fri, 13 Jul 2012 00:00:00 UTC +00:00}}, options: {}, class: Product, embedded: false>
There’s a lot more that we can do here as Mongoid’s query interface is quite powerful. There’s more detail about this in the documentation.
Associations
Another way that Mongoid differs from ActiveRecord is through associations. We can embed an association into a single document and to demonstrate how this works we’ll generate a new model called Review with a content
text field.
$ rails g model review content
We now have two models and we want Product
to have many Reviews
. We can define this association in two different ways. We could add a reference association which works in a similar way to an ActiveRecord association but instead we’ll use an embedded association. To define this we use embeds_many
.
class Product include Mongoid::Document field :name, type: String field :price, type: BigDecimal field :released_on, type: Date attr_accessible :name, :price, :released_on validates_presence_of :name embeds_many :reviews end
In the Review
model we can call embedded_in
.
class Review include Mongoid::Document field :content, type: String embedded_in :product end
We can try this out in the console. Interacting with the association works similarly to ActiveRecord.
>> p = Product.first => #<Product _id: 502952b1a175752aa0000001, _type: nil, name: "Settlers of Catan", price: "34.99", released_on: 2012-01-01 00:00:00 UTC> >> p.reviews.create! content: "great game!" => #<Review _id: 5029656ca17575eaa1000001, _type: nil, content: "great game!">
We now have a review that we can fetch through its Product
as we’d expect. As it’s an embedded association all the review’s data is within the Product
document, not stored as a separate document in MongoDB. This means that calling Review.count
will return 0
as there are no Review
documents; we can only fetch reviews through the Product
model. We can’t even create a Review
record on its own as it needs to exist within a parent Product.
Whenever we define an association in Mongoid we need to ask ourselves whether we’ll ever need to fetch the associated record and work with it on its own, outside the parent. If so then we should use a has_many
reference association instead of an embedded one. The advantage of an embedded association is that all the data is within one document and so our app doesn’t need to perform separate queries in order to fetch the other associated documents.
Additional Modules
Next we’ll show you a few additional modules that we can include alongside Mongoid::Document
. Mongoid::Timestamps
, as its name suggests, is similar to the ActiveRecord functionality. If we add it to, say, the Product
model then when we create or update a product those timestamp fields will be updated.
>> Product.create! name: "Agricola" => #<Product _id: 50296dc5a17575eaa1000002, _type: nil, created_at: 2012-08-13 21:12:37 UTC, updated_at: 2012-08-13 21:12:37 UTC, name: "Agricola", price: nil, released_on: nil>
Another useful module is called Paranoia
. This does soft deletes which means that deleting a record won’t actually remove it from the database. If we include Mongoid::Paranoia
in the Product
model then delete a product all that happens is then a deleted_at
attribute is set. The product looks like it’s gone but we can call restore
on it to bring it back. Mongoid even has built-in support for versioning with the Mongoid::Versioning
module. We won’t cover it here but there’s information about this in the documentation.
Prettier URLs
If we look at the URL for a product we’ll see that the URL has an unusual identifier.
If we want to change this to something a little prettier there are a couple of ways we can do so. One option is similar to ActiveRecord where we override the to_param
method, but here we’ll show you another way to do this with Mongoid by overriding the _id
field.
field :_id, type: String, default: -> { name.to_s.parameterize }
The key here is to set a default value for the field and here we use a proc to set it to the value of the product’s name, parameterized to make it URL-safe. When we create a new product now its name becomes part of its URL.
There area couple of things we need to watch when we take this approach. One is that whenever we create a new Product
we always need to set the name while creating the instance. If we don’t the product’s id
will be an empty string and if we try to set the name later it will remain an empty string. Also we need to ensure the uniqueness of the id
so we should add some validation to make sure that this is the case.
That’s it for our episode on Mongoid. This project’s documentation is comprehensive and is well worth looking at. It’s also worth spending some time learning about MongoDB on its own. There’s an interactive shell on its website with a tutorial that we can work through to get a better idea of how MongoDB works.