Mongomatic

Mongomatic allows you to map your Ruby objects to Mongo documents. It is designed to be fast and simple.

0.6.0 CHANGELOG NOTES

!! THE ERROR CLASS HAS BEEN REWORKED IN THIS VERSION.
!! DO NOT EXPECT YOUR UPGRADE TO BE SEAMLESS. NOTHING /SHOULD/
!! BREAK BUT YOU SHOULD TEST THOROUGHLY.

* Added methods to bring ActiveModel compliancy

* Pulled in do_callback fix (no more NoMethodError catching)

* Relaxed restrictions on dependencies

* Reworked the Error class to mimic the ActiveModel style errors.

What's different about Mongomatic?

  • Follows Mongo idioms wherever possible.

  • Only strives to do “just enough” and never too much.

  • When possible, we defer to Mongo. For example, there's no Mongomatic query API, instead you use the same query hash syntax you would with the Ruby Mongo driver.

  • No complex relationship management: if you want to model relationships then add your own finder methods.

  • Minimal dependencies.

Basic Usage

require 'mongomatic'

class User < Mongomatic::Base
  def validate
    self.errors.add "name", "can't be empty" if self["name"].blank?
    self.errors.add "email", "can't be empty" if self["email"].blank?
  end
end

# set the db for all models:
Mongomatic.db = Mongo::Connection.new.db("mongomatic_test")
# or you can set it for a specific model:
User.db = Mongo::Connection.new.db("mongomatic_test_user")

User.empty?
=> true

u = User.new(:name => "Ben")
=> #<User:0x00000100d0cbf8 @doc={"name"=>"Ben"}, @removed=false> 
u.valid?
=> false
u["email"] = "me@somewhere.com"
u.valid?
=> true
u.insert
=> BSON::ObjectId('4c32834f0218236321000001')

User.empty?
=> false

u["name"] = "Ben Myles"
=> "Ben Myles" 
u.update
=> 137 

found = User.find_one({"name" => "Ben Myles"})
=> #<User:0x00000101939a48 @doc={"_id"=>BSON::ObjectId('4c32834f0218236321000001'), "name"=>"Ben Myles", "email"=>"me@somewhere.com"}, @removed=false, @is_new=false, @errors=[]>
User.find_one(BSON::ObjectId('4c32834f0218236321000001')) == found
=> true

cursor = User.find({"name" => "Ben Myles"})
=> #<Mongomatic::Cursor:0x0000010195b4e0 @obj_class=User, @mongo_cursor=<Mongo::Cursor:0x80cadac0 namespace='mongomatic_test.User' @selector={"name"=>"Ben Myles"}>>
found = cursor.next
=> #<User:0x00000101939a48 @doc={"_id"=>BSON::ObjectId('4c32834f0218236321000001'), "name"=>"Ben Myles", "email"=>"me@somewhere.com"}, @removed=false, @is_new=false, @errors=[]>
found.remove
=> 67
User.count
=> 0
User.find({"name" => "Ben Myles"}).next
=> nil

Indexes

Mongomatic doesn't do anything special to support indexes, but here's the suggested convention:

class Person < Mongomatic::Base  
  class << self
    def create_indexes
      collection.create_index("email", :unique => true)
    end
  end
end

You can run Person.create_indexes whenever you add new indexes, it won't throw an error if they already exist.

If you have defined a unique index and want Mongomatic to raise an exception on a duplicate insert you need to use insert! or update!. The error thrown will be Mongo::OperationFailure. See the test suite for examples.

Validations

You can add validations to your model by creating a validate method. If your validate method pushes anything into the self.errors object your model will fail to validate, otherwise if self.errors remains empty the validations will be taken to have passed.

class Person < Mongomatic::Base  
  def validate
    self.errors.add "name", "blank" if self["name"].blank?
    self.errors.add "email", "blank" if self["email"].blank?
    self.errors.add "address.zip", "blank" if (self["address"] || {})["zip"].blank?
  end
end

p = Person.new
=> #<Person:0x000001018c2d58 @doc={}, @removed=false, @is_new=true, @errors=[]>
p.valid?
=> false
p.errors
=> [["name", "blank"], ["email", "blank"], ["address.zip", "blank"]]
p.errors.full_messages
=> ["name blank", "email blank", "address.zip blank"]
p["name"] = "Ben"
p["email"] = "Myles"
p["address"] = { "zip" => 94107 }
p.valid?
=> true

The Validation Helper (Expectations)

To make writing your validate method a little simpler you can use Mongomatic::Expectations. You must include Mongomatic::Expectations::Helper on any class you wish to use them. You can use expectations like this:

 class Person < Mongomatic::Base
   include Mongomatic::Expectations::Helper

   def validate
     expectations do
       be_present   self['name'], ["name", "cannot be blank"]
       not_be_match self['nickname'], ["nickname", "cannot contain an uppercase letter"], :with => /[A-Z]/
     end
   end
end

p = Person.new
p.valid?
=> false
p.errors.full_messages
=> ["Name cannot be blank"]
p['name'] = 'Jordan'
p.valid?
=> true
p['name'] = nil
p['nickname'] = 'Jordan'
p.valid?
p.errors.full_messages
=> ["Name cannot be blank", "Nickname cannot contain an uppercase letter"]

You can use any of these expectations inside of the expectations block:

* be_expected value, error_message - validates that value is true
* not_be_expected value, error_message - validates that value is false
* [not]_be_present value, error_message - validates presence of value
* [not]_be_a_number value, error_message, options - validates that value is/is not a number 
                                                    Can be a string containing only a number).
                                                    Only takes :allow_nil option

* [not]_be_match value, error_message, options -  validates that value matches/does not match 
                                                    regular expression specified by option :with.
                                                    Also, takes option :allow_nil
* be_of_length value, error_message, options - validates that value is any of: less than the number
                                               specified in the :minimum option, greater than the number
                                               specified in option :maximum, or in range specified by 
                                               option :range

Relationships

Mongomatic doesn't have any kind of special support for relationship management but it's easy to add this to your models where you need it. Here's an example that models Twitter-like followers on a User model:

class User < Mongomatic::Base  
  class << self
    def create_indexes
      self.collection.create_index("following_ids")
    end
  end

  def follow(user)
    self.push("following_ids", user["_id"])
  end

  def unfollow(user)
    self.pull("following_ids", user["_id"])
  end

  def following
    return nil if new?
    self.class.find( { "_id" => { "$in" => (self["following_ids"] || []) } } )
  end

  def followers
    return nil if new?
    self.class.find( { "following_ids" => self["_id"] } )
  end

  def friends
    return nil if new?
    self.class.find( { "following_ids" => self["_id"], 
                       "_id" => { "$in" => (self["following_ids"] || []) } } )
  end
end

Observers

Mongomatic does have one thing in common with ActiveRecord and that is observers. You can add observers to your class to decouple distinct functionality in callbacks. To add observation to your model you must include the Mongomatic::Observable module. All observers inherit from Mongomatic::Observer.

Unlike ActiveRecord the existence of a CollectionNameObserver will automatically add the observer the CollectionName class. Instead you must use the observer macro or CollectionName.add_observer

You can add your own observers to CollectionName using CollectionName.add_observer(klass).

class MyCustomCallbacks < Mongomatic::Observer
  def after_insert_or_update
    puts "after insert or update"
  end
end

class Person < Mongomatic::Base
  include Mongomatic::Observable
  observer :PersonObserver
  observer MyCustomCallbacks
end

# this observer is automatically added to the Person class
class PersonObserver < Mongomatic::Observer
  def after_insert(instance)
    puts "new person inserted"
  end 
end

It is worth noting that you should be careful the operations you perform on the instance of your class passed to the observer callbacks. Calling operations that invoke callbacks can result in an infinite loop if improperly structured.

Note on Patches/Pull Requests

  • Fork the project.

  • Make your feature addition or bug fix.

  • Add tests for it. This is important so I don't break it in a future version unintentionally.

  • Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)

  • Send me a pull request. Bonus points for topic branches.

Copyright

Copyright © 2010 Ben Myles. See LICENSE for details.