Clever Custom Datatypes with MongoMapper

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!

Photo of Daniel Morrison

Daniel founded Collective Idea in 2005 to put a name to his growing and already full-time freelance work. He works hard writing code, teaching, and mentoring.

Comments

  1. ronhornbaker@gmail.com
    Ron
    January 19, 2011 at 18:50 PM

    So where’s that documentation again?

  2. January 19, 2011 at 18:52 PM

    Ron: Exactly. 

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

  3. January 19, 2011 at 21:39 PM

    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

  4. special.j4y@gmail.com
    Jay
    March 26, 2011 at 18:09 PM

    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

  5. xoen@xoen.org
    Aldo "xoen" Giambelluca
    May 06, 2015 at 13:46 PM

    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?