Skip to Content Collective Idea Home

Clever Custom Datatypes with MongoMapper

by Daniel Morrison

Rich Objects. This is a pattern we’re using more and more with MongoDB[1] and it is so easy to get started.

Example: Version Numbers

We[2] had an app today that stored a simple version number (e.g. 1.0, 2.1). We could have done this with a string, a pair of integers, or a float. We decided on something better:

class Article
  include MongoMapper::Document

  key :title,    String
  key :body,     String
  key :version,  Version, :default => '1.0'
  …
end

Version? That’s not a Rails or Ruby thing. That’s our own. We can easily cast data into our own objects. Here’s our basic Version class:

class Version
  attr_accessor :major, :minor

  def self.from_mongo(value)
    new(value.to_s)
  end

  def self.to_mongo(value)
    value.to_s
  end

  def initialize(string)
    self.major, self.minor = string.split('.').map(&:to_i)
  end

  def to_s
    "#{major}.#{minor}"
  end
end

We’re storing the actual version as a string in MongoDB (you could do an array too, as that’s a native type) but the key info is the from_mongo and to_mongo to define how they get [un]serialized to MongoDB.

from_mongo takes a value and turns it into a Version object (we call value.to_s in case value is already a Version or a Fixnum). to_mongo simply turns it into a string for saving. In our instances we decided to keep major and minor versions separate.

Bumping Versions

We added some simple methods to bump the versions:

def bump_minor
  self.minor += 1
end

def bump_major
  self.major += 1
end

Now we can bump our article versions easily:

>> @article.version
=> "1.2"
>> @article.version.bump_minor
>> @article.version
=> "1.3"

Comparing Versions

Since this is just a Ruby object, the ability to compare and sort versions is simple:

class Creator::Version
  include Comparable

  def <=>(value)
    other = self.class.new(value.to_s)
    [major, minor] <=> [other.major, other.minor]
  end

  …
  end

Next Steps

There are lots of other objects you can do this with. Once you start to think in terms of rich objects, you find all sorts of great applications.

There are a couple other examples in the MongoMapper documentation[3], too. Head over there to see more!


  1. There’s no reason you can’t do this with ActiveRecord and SQL too, but it isn’t quite as easy. ↩︎

  2. Our newest team member, Steve Richert, asked for “mad props” in this post. Way to write some sweet code, Steve! ↩︎

  3. As of this writing, the MongoMapper documentation is up and running but hasn’t been linked to or publicized yet. Help me bug @jnunemaker to make it happen! ↩︎

Comments

Ron
::

So where’s that documentation again?


Daniel Morrison
::

Ron: Exactly.

See my footnote. I’m using this post to try to get it shipped.


Brian Ryckbost
::

Might be worth noting mongo’s increment ($inc) operator. If version was a number, I think you could use that to bump versions.

http://www.mongodb.org/display/DOCS/Updating#Updating-%24inc


Jay
::

It would be great MongoMapper’s query method worked with the comparable defintions, ie:

Article.where(:version.gte => ‘1.0’)  #will not use custom comparable method


Aldo "xoen" Giambelluca
::

Hi, I read the documentation and I think MongoMapper’s custom types are pretty cool. The idea is very simple but for some reason I’m not having much luck (that’s why I ended up here).

I’ve a class called MongoMapper::EscapedString with .to_mongo and .from_mongo - .from_mongo just returns value while .to_mongo escapes value (using the htmlentities gem, but it’s not relevant). All very easy.

I’ve a mongomapper document using this class as type but for some reason the string is escaped twice when I save a document. Am I missing something stupid here?