June 27, 2009

Posted by John

Tagged database, mongo, and mongomapper

Older: JSONQuerying Your Rails Responses

Newer: Code Review: Weary

MongoMapper, The Rad Mongo Wrapper

MongoMapper
A few weeks ago, I wrote about Mongo and how awesome it is. Towards the end of the article (and in the slideshow) I mentioned MongoMapper, a project I’ve been working on.

Over the past few weeks my buddies at Squeejee and Collective Idea have started using MongoMapper and they’ve helped me squash a few bugs and add a few features.

Despite the fact that I would call it far from finished, I’ve decided to release it in hopes that people can start playing with it, finding bugs, adding features and submitting pull requests. The documentation is sparse to none, but there are plenty of tests and the code is pretty readable, I believe.

Installation

# from gemcutter
gem install mongo_mapper

Usage

So how do you use this thing? It’s pretty simple. MongoMapper uses a default connection from the Ruby driver. This means if you are using Mongo on the standard port and localhost, you don’t have to give it connection information. If you aren’t, you can do it like this:

MongoMapper.connection = Mongo::Connection.new('hostname')

Connection accepts any valid Mongo ruby driver connection. The only other setup you need to do is to tell MongoMapper what the default database is. This is pretty much the same as setting up the connection:

MongoMapper.database = 'mydatabasename'

These two operations only define the default connection and database information. Both of these can be overridden on a per model basis so that you can hook up to multiple databases on different servers.

Include Instead of Inherit

To create a new model, I went with the include pattern, instead of inheritance. In ActiveRecord, you would define a new model like this:

class Person < ActiveRecord::Base
end

In MongoMapper, you would do the following:

class Person
  include MongoMapper::Document
end

Just like ActiveRecord, this makes assumptions. It assumes you have a collection named people. Oh, and the good news is you don’t need a migration for it. The first time you try to create a person document, the collection will be created automatically. Heck yeah! I mentioned that you can override the default connection and database on a per document level. If you need to do that, it would look like this:

class Person
  include MongoMapper::Document

  connection Mongo::Connection.new('hostname')
  set_database_name 'otherdatabase'
end

Defining Keys

Each document is made up of keys. Keys are named and type-casted so you know your data is stored in the correct format. Lets fill out our Person document a bit.

class Person
  include MongoMapper::Document
  
  key :first_name, String
  key :last_name, String
  key :age, Integer
  key :born_at, Time
  key :active, Boolean
  key :fav_colors, Array
end

Now that we have defined our schema, we can create, update and delete documents.

person = Person.create({
  :first_name => 'John',
  :last_name => 'Nunemaker',
  :age => 27,
  :born_at => Time.mktime(1981, 11, 25, 2, 30),
  :active => true,
  :fav_colors => %w(red green blue)
})

person.first_name = 'Johnny'
person.save

person.destroy
# or you could do this to destroy
Person.destroy(person.id)

Looks pretty familiar, eh? Where it made sense, I tried to stay close to ActiveRecord in API.

Validations

But wait you say, how do I validate my data? Well, you can do it pretty much the same way as ActiveRecord.

class Person
  include MongoMapper::Document
  
  key :first_name, String
  key :last_name, String
  key :age, Integer
  key :born_at, Time
  key :active, Boolean
  key :fav_colors, Array

  validates_presence_of :first_name
  validates_presence_of :last_name
  validates_numericality_of :age
  # etc, etc
end

But, if you find that a bit tedious as I do, you can use some shortcuts that I’ve added in.

class Person
  include MongoMapper::Document
  
  key :first_name, String, :required => true
  key :last_name, String, :required => true
  key :age, Integer, :numeric => true
  key :born_at, Time
  key :active, Boolean
  key :fav_colors, Array
end

Most of the validations from Rails are supported. I still need to build in support for validates_uniqueness of and some of the options that rails supports might not be right now, but it is a good first pass.

Callbacks

Did you hear that? I swear I just heard someone whisper about callbacks. Umm, yeah, we got that too. The good news? I just used ActiveSupport’s callbacks so they are identical to Rails and most of Rails defined callbacks are supported such as before_save and the like.

Embedded Documents

So the cool thing about Mongo is that you can embed documents in other documents. Let’s say our person has multiple addresses. To handle that, we would create an embedded address document to go along with our person document.

class Address
  include MongoMapper::EmbeddedDocument
  
  key :address, String
  key :city,    String
  key :state,   String
  key :zip,     Integer
end

class Person
  include MongoMapper::Document

  many :addresses
end

Now we can add addresses to the person like so:

person = Person.new
person.addresses << Address.new(:city => 'South Bend', :state => 'IN')
person.addresses << Address.new(:city => 'Chicago', :state => 'IL')
person.save

Doing this actually saves the address right inside the person document. Yep, no joins. Yay! Cheers resound from the heavens! You can even query for documents based on these embedded documents. For example, if you wanted to find all people that are in the city Chicago, you could do this:

Person.all(:conditions => {'addresses.city' => 'Chicago'})

Finding Documents

The find API is very similar to AR as well. Below are a bunch of other examples:

Person.find(1)
Person.find(1,2,3,4)
Person.find(:first)
Person.first
Person.find(:last)
Person.last
Person.find(:all)
Person.all
Person.all(:last_name => 'Nunemaker', :order => 'first_name')

For more information about how to provide criteria to find, you can see the stuff covered in the finder options. If you need to, you can even throw custom mongo stuff into the mix and it just gets passed through to the mongo ruby driver (ie: $gt, $gte, $lt, $lte, etc.).

Conclusion

We take ActiveRecord for granted. It really has a lot of handy features and does a pretty good job at modeling our applications. I never realized how much it does, until I decided to create MongoMapper. That said, the experience has been fun thus far and I’m excited to see what people use it for.

There is a ton more I could talk about, but frankly, this article is long enough. Rest assured that I think Mongo is cool and that MongoMapper is headed in the right direction, but far from complete. I haven’t actually built anything with MongoMapper yet, but I will be soon. I’m sure that will lead to a lot of handy new features.

Any general discussion can happen in the comments below while they are open or over at the google group. If you find a bug or have a feature idea, create an issue at github.

18 Comments

  1. Sweet! As an early customer, I can vouch that it is sweeeeeet. I hope to be contributing to this bad boy soon.

  2. cool! love the logo too.

  3. @Mike – Thanks! It was created by Steve, the other half of Ordered List. Eventually, I’ll have a site dedicated to MongoMapper so the logo will be handy. :)

  4. I have mixed feelings. It’s duplicating effort writing another ORM when you could be writing an adapter for an existing one. At the same time all of the existing ORM’s are very RDBMS/SQL centric so adapters for other databases often lack features because they can’t be shoehorned into that mindset. But, unless we keep attempting to hack in the more exotic databases this will never change and people won’t see all the benefits that come with shared development, testing and API.

    Disclaimer: Late last week I started a hacky attempt at a DataMapper adapter for Mongo but I’m really not the right person to be attempting it and I haven’t progressed very far yet.

  5. @Shane – Remember that ORM stands for Object Relational Mapping. DataMapper is awesome, but it’s for relational databases, not document-based ones.

    Taking the idea a step further, because document-based stores like Mongo and Couch implicitly hold objects (in JSON), we don’t even need object mapping. It’s more like object translation, from a JSON object to a Ruby object.

  6. @Luigi – I always took relational to mean ‘mapping relationships between incompatible type systems’ not a literal reference to the most common underlying storage type. Then again I’m self taught so what do I know :P

    Ignoring the abbreviation aren’t common problems are being repeatedly solved here though?

    • Database to Ruby object mapping. Call them keys, properties, attributes or whatever you like but the mongo driver returns a collection of hashes which must all be mapped into an instance of your higher level document class with possibly different named attribute accessors/mutators.
    • Validations, type checking and serialization. Sooner or later you’ll want to store and validate a URI object or some other non primitive. You’ll have to create types for your keys that (de-)serialize into one of the known primitives (String).
    • Modeled relationships. Mongo sub-objects can be thought of as one-to-one (hash) or one-to-many (array of *)? Beyond that DBRef’s between collections are foreign keys by another name.
    • Query abstraction. Admittedly mongomapper isn’t hiding much of the XGen driver from you here.
    • Identity mapping? Dirty attribute tracking to avoiding stale in memory representations of the record in the database.

    Sorry for bringing this in your release post and congrats on the library.

  7. Dan Kubb Dan Kubb

    Jun 28, 2009

    John, congrats on the release. The API is nicely designed, and I look forward to seeing how you develop this library.

    @Luigi: DataMapper has over 30 adapters right now, and only about 5 of those are for relational databases. I’ve seen no evidence that it couldn’t support document oriented datastores. In fact if someone wanted to developa DM adapter for MongoDB, CouchDB or another document oriented store I would love to hack with them on it and would even be willing to evolve DM to make it work seamlessly, if it doesn’t already.

  8. Great work! I’ve been looking forward to this since you announced it.

    Interesting that Datamapper was raised. I actually started exploring DM last week for the first time and realised that it could work very well with mongo. In some ways MongoMapper looks quite similar to DMs concepts. @Dan: Worth discussing ideas for a DM adapter for Mongo.

    John, Not that I’m trying to invalidate the work done on MM. It’s released and looks very very impressive. I’m going to be trying it out on a new project.

  9. @Shane, @Dan – I’m not saying that a DM adapter couldn’t be done for a document-based store (Dan Kubb seems to have worked on one for CouchDB). It definitely can be done, and can be done well.

    But I am defending John’s decision to create a custom wrapper for Mongo. I think that document-based stores are a significant enough departure from the ORM model (and yes, it does refer to mapping objects to relational tables) that fitting them into the ORM paradigm would be overkill in some parts, and lacking in others. It wouldn’t fit as well as a tailored solution like MongoMapper.

    I’m reminded of CouchREST being a better wrapper than any of the ActiveRecord-based CouchDB adapters. It speaks natively to what Couch is all about.

  10. @Shane – I struggled with the same feelings before I started. My conclusion was kind of the same in that there should be something more abstract out there to provide a start. I see someday pulling some stuff out of MM and making a more generic gem that MM then depends on, much in the same way I abstracted crack out of HTTParty. No worries about bringing that into the release post. :)

    @Dan – I might be interested in hacking on a DM adapter. If it could fit the same kind of mold as I’ve started, I have a feeling it would save a lot of time in the long run. I totally should have talked to you before starting this, rather than just looking at DM and deciding it wasn’t for me.

    Either way, if MM stays as it is and abstracts out common stuff or if it evolves into a pimped out DM adapter, I don’t much care. I just want a good API for working with MongoDB. I have no fear of throwing this code out the window if we can come up with a better solution.

    The learning experience alone and realizing how much ActiveRecord and DataMapper are taken for granted was worth it. :)

  11. Dan Kubb Dan Kubb

    Jun 28, 2009

    @John: That sounds great. Whenever you want to talk about it feel free to hit me up on IRC in #datamapper (I’m “dkubb”), or you can ping me on IM/email (dan.kubb [at] gmail.com). I’m very committed to making sure DataMapper abstracts document oriented databases, and I’ll do my best to give adapter authors the framework they need to build on top of.

    I actually think every programmer should try to write their own ORM at least once. I never truly appreciated the work the Rails team did with ActiveRecord, until I started hacking on DataMapper regularly. I think it’s the same thing with web frameworks too.

    @Luigi: I didn’t actually work on the DM CouchDB adapter, other than a couple of small commits here and there. It was user contributed. I actually removed it from the “official” DM repo, not necessarily because it didn’t work well, but because I believe at the time CouchDB was a fast moving target and the maintainer didn’t have enough time to keep the adapter in sync. However, I’ve heard that things may have calmed down, and would love to work on a new CouchDB adapter with anyone that is interested.

    (BTW: John, sorry if I hijacked your comment thread to be DM related. It wasn’t my intention to redirect attention from your library release, it just seemed like it would be easiest to respond to comments/questions here)

  12. Yeah, the criticism for not just implementing a proper DataMapper adapter for MongoDB is completely legitimate. Way too much duplication of effort here for my tastes. I’ll wait for a good adapter, thanks.

  13. boo hoo to all these negative people.

    this is good stuff. i don’t want to learn Datamapper! i’m happy with it looking like AR.

  14. Great orm. Really enjoying it.

    The _id that gets created does not seem totally random and actually increments upward when using mongomapper. Is this a mongomapper thing or a mongodb thing? Here’s 3 records I created in order:

    _id: 2f1282f64a4ce0bb00000023
    _id: 2f1282f64a4ce6df00000023
    _id: 2f1282f64a4ce72400000023

    Coincidentally, I actually prefer this over couchdb’s/couchrest’s approach. For the app I was building I still needed an incrementing id and the application code to do this with couchdb was brittle. With mongomapper, it seems I can just do Model.last.increment_id + 1 in a callback.

  15. @Scot – Glad you are enjoying it. It is a mongo ruby driver thing. You can override it to use your own id’s if you prefer, but I like not having to think about it.

  16. I’m wondering whether your MongoWrapper could be adapted for use with M/DB:X which is an interesting hybrid JSON/XML database. It’s a somewhat different approach to CouchDB – JSON objects are mapped to/from persistent XML DOM format and can be modified, analysed and searched in the XML domain and automatically converted back out to JSON strings.

    See http://www.mgateway.com/mdbx.html and let me know what you think

  17. Harry Vangberg Harry Vangberg

    Jul 06, 2009

    This looks pretty good, and from what I have seen the code is simple to follow, great job! Just a shame it’s only coming now, I would really like to use MongoDB on a new project, but it’ll probably not be a safe bet :) Embedded documents for the win, though.

  18. @Harry – I would start with a side project to get a feel for it. Then you don’t have to worry about safe bets and what not.

Sorry, comments are closed for this article to ease the burden of pruning spam.

About

Authored by John Nunemaker (Noo-neh-maker), a programmer who has fallen deeply in love with Ruby. Learn More.

Projects

Flipper
Release your software more often with fewer problems.
Flip your features.