Introducing Mongomatic
Note: This was originally posted on my previous blog. It is archived here for sentimental, educational (to me) and, in the unlikely case, actual value
Mongomatic is a Ruby object mapper for MongoDB that aims to be simple, have minimal dependencies and adhere to native Mongo conventions. Mongomatic brings you just close enough to the Ruby mongo driver wherever possible. It uses Ruby’s hash syntax to access data stored in the document including sub-documents, which are plain nested hashes. Documents are queried using the native query syntax. Mongomatic also supports validation and callbacks with a much simpler API than other Mongo object mappers modeled after ActiveRecord.
Jumping Right In
You can install Mongmatic through RubyGems or by cloning the GitHub repository and building it yourself. To install with RubyGems run:
gem install mongomatic
Since connection details are a bit mundane lets start with a little example first. All Mongomatic models inherit from Mongomatic::Base.
class Post < Mongomatic::Base
include Mongomatic::Expectations::Helper
private
def validate
expectations do
be_present self['title'], "Title must not be empty"
not_be_a_match self['alias'],
"Alias must not contain uppercase characters", :with => /[A-Z]/
be_of_length self['authors'], "Must have alteast one author", :minimum => 1
end
end
end
The above example defines a class called Post. Each document will be stored in the Post collection. I will discuss the validation portion of the code further on.
We can now use our class like this:
# instantiate a new document
post = Post.new(:title => 'Post Title')
post['alias'] = 'A'
p.new? => true
# insert invalid document
post.valid? => false
post.insert => false
post.errors.full_messages => ["Alias must not contain uppercase characters", ...]
# insert valid document
post['alias'] = 'abc'
post['authors'] = ['Jordan West']
post.insert => BSON::ObjectID
post.new? => false
post.insert => false # can only insert new records
# update document
post['title'] = 'New Post Title'
post.update
# iterate through docs with a cursor
posts = Post.find(:alias => 'abc') => Mongomatic::Cursor
posts.each do |post|
# code here
end
# find single document
Post.find_one(:title => 'New Post Title') => Post
Post.find_one(post['_id']) => Post
# remove document
post.remove
post.removed? => true
Post.find_one(:title => 'New Post Title') => nil
Getting Connected
Mongomatic supports global and per-model connections. This means that each model can represent collections on different servers, clusters, or databases. The connection is established using the Mongo Ruby driver and then provided to Mongomatic. Below we define a global connection object.
Mongomatic.db = Mongo::Connection.new.db("modest_rubyist")
Connections are defined in the same way for a single class.
Post.db = Mongo::Connection.new.db("another_mr_db")
Validation
Validation can be done in one of two ways in Mongomatic. Both ways share some common elements. All validation is done inside the class’ validate method. Validity is determined by the number of errors generated on an instance. The first validation method is to generate the errors manually. Error messages are pushed onto the instance’s errors array if an error condition is met. We could rewrite part of our first example like this,
class Post < Mongomatic:Base
private
def validate
errors << ['name', 'cannot be blank'] if self['name'].blank?
end
end
p = Post.new
p.insert => false
p.errors.full_messages => ['name cannot be blank']
The second method is to include the RSpec-inspiried Mongomatic::Expectations::Helper module like we did earlier,
class Post < Mongomatic::Base
include Mongomatic::Expectations::Helper
private
def validate
expectations do
be_present self['title'], "Title must not be empty"
not_be_a_match self['alias'],
"Alias must not contain uppercase characters", :with => /[A-Z]/
be_of_length self['authors'], "Must have alteast one author", :minimum => 1
end
end
end
Expectations can only be used inside the of expectations block helper. Each expectation takes a value, an error message and possibly some options. There are several other expectations you can use. Checkout the README for more info.
Callbacks
Mongomatic supports simple callbacks. Callbacks are defined as instance methods much like validation is. The following callbacks can be defined on our Post model:
class Post < Mongomatic::Base
private
def before_validate
end
def after_validate
end
def before_insert
end
def before_insert_or_update
end
def after_insert_or_update
end
def after_insert
end
def before_update
end
def after_update
end
def before_remove
end
def after_remove
end
end
The Indexes Convention
Although Mongomatic does not provide an API for creating indexes, it is recommended to use this convention
class Post < Mongomatic::Base
def self.create_indexes
collection.create_index('title', :unique => true)
collection.create_index('alias')
end
def self.drop_indexes
collection.drop_indexes
end
end
Define two class methods, one to create the indexes and the other to drop them. Indexes are created and dropped using the Mongo Ruby driver’s create_index and drop_indexes methods. Refer to the Ruby mongo driver docs for more index options.
Safe Methods and Unique Values
You can use unique indexes and Mongomatic’s safe inserts to detect when duplicate values are stored in the same field. Taking the example from the section before we could check for duplicate post titles.
Post.new(:title => "Dont Duplicate").insert!
Post.new(:title => "Dont Duplicate").insert! # raises Mongo::OperationFailure
Each operation has a corresponding safe, bang-method. Safe methods pass the :safe => true option to the Mongo ruby driver when running operations and raise a Mongo::OperationFailure exception if something went wrong.
More Resources
If you like Mongomatic check out it’s shiny new website, designed by Christopher Hein, who did an awesome job. You can also visit the GitHub repository or the docs. We will be adding to the Wiki soon. Also, check back soon because I will have another post on working with sub-documents, relationships, arrays, counters and writing your own expectations.