Looped Strange

Ramblings of Programmer who likes Code & Math

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.