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!
Comments
So where’s that documentation again?
Ron: Exactly.
See my footnote. I’m using this post to try to get it shipped.
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
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
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?