Mongomatic allows you to map your Ruby objects to Mongo documents. It is designed to be fast and simple.
This project follows semantic versioning as detailed here (semver.org). This means that minor version bumps (0.x => 0.y) can break compatibility. Please see the CHANGELOG for upgrade notes.
-
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 just the modifications, do not mess with Rakefile, VERSION, or CHANGELOG.
-
Add a separate commit with modifications to the CHANGELOG file
-
Send me a pull request. Bonus points for branches.
-
Follows MongoDB idioms wherever possible.
-
Only strives to do “just enough” and never too much.
-
When possible, we defer to MongoDB. For example, there’s no Mongomatic query API, instead you use the same query hash syntax you would with the Ruby MongoDB driver.
-
No complex relationship management: if you want to model relationships then add your own finder methods.
-
Minimal dependencies.
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"] = "[email protected]" 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"=>"[email protected]"}, @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.first => #<User:0x00000101939a48 @doc={"_id"=>BSON::ObjectId('4c32834f0218236321000001'), "name"=>"Ben Myles", "email"=>"[email protected]"}, @removed=false, @is_new=false, @errors=[]> found.remove => 67 User.count => 0 User.find_one({"name" => "Ben Myles"}) => nil
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.
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
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
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
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.
Copyright © 2010 Ben Myles. See LICENSE for details.