CouchRest::Model - ORM, the CouchDB way

CouchRest::Model - ORM, the CouchDB way
By jchris in Coding 3 months ago.

There are a few ActiveRecord-style wrappers for CouchDB out there these days, but many of them have fallen behind as CouchDB’s powers have increased. I’ve taken an interest in writing regular old web apps again – so now is the time when CouchRest grows wings and flies to the vaulted heights of DataMapper and ActiveRecord. (I could have written a DataMapper adapter for CouchDB, but much of DataMapper’s code is based around SQL-like problems that CouchDB just doesn’t have.) CouchRest::Model provides the ease-of-use Rubyists have come to expect from their ORMs, and it does it in CouchDB style.

Getting Started

Here is an example (taken from the bright shiny new documentation) of what your Models can look like. If you click through to the actual blog post, there is also a longer example pasted as a gist embed which details the capabilities of the view-generation system even more.

class Article <  CouchRest::Model

  use_database CouchRest.database!('http://localhost:5984/couchrest-model-test')
  unique_id :slug

  view_by :date, :descending => true
  view_by :user_id, :date

  view_by :tags,
    :map =>
      "function(doc) {
        if (doc.type == 'Article' && doc.tags) {
          doc.tags.forEach(function(tag){
            emit(tag, 1);
          });
        }
      }",
    :reduce =>
      "function(keys, values, rereduce) {
        return sum(values);
      }" 

  key_reader :slug, :created_at, :updated_at
  key_accessor :title, :tags

  # You can make dates work nice without magic or special fields
  key_writer :date 
  def date
    Time.parse(@doc['date'])
  end

  timestamps!

  before(:create, :generate_slug_from_title)
  def generate_slug_from_title
    doc['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
  end
end

You can do the standard CRUD:

@post = Post.get(params[:id])
@post.title = "New Title" 
@post.save

View Generation

The view generation system is what I’m proudest of. It’s designed to be performant, and it strikes a balance between magic and usability. Simple views are as easy to declare as:

  view_by :date, :descending => true

They can be queried easily like this:

# get the 10 most recent posts
@posts = Post.by_date :count => 10

# get the raw view
@view = Post.by_date :count => 10, :raw => true

Views defined in this way take all the options of a CouchRest::Database#view call, and those options can be provided either as defaults at definition time, or overriden at query time. There is an additional option, :raw, which defaults to false. When :raw is true, you get back just what CouchDB sends. When :raw is false, CouchRest::Model::MagicViews requests the associated document for each view row, and gives you an array of documents. W00t!

CouchRest::Model views use _design documents, not temp views, so they take advantage of all the performance CouchDB has to offer.

Views can also be manually composed, if you want to write something that’s more complex than the built-in helpers can provide.

If you’re at the permalink page for this post, you’ll see a Gist embed here with an even longer example of CouchRest::Model in action.

Enjoy!

3 comments on CouchRest::Model - ORM, the CouchDB way

Great stuff!!! Keep up with the good workd! I have a few questions. Are you planning on adding relationships and validations? Also, do you know a way of ensuring that an attribute is unique across a Model domain? For example, email attribute for a User model?

Thanx

Do you have a sample rails/merb application that we can look at? I’d like to get a peek at the various init and config files.

Hope you don’t mind the silly question.

Any advice on how best to support mapping a an couchrest-type to a an Object? Id like to get a little meta and it would be cool if I could do this:

@types = Types.by_instances

Here’s the class I have so far. The views get created but reads fail. Need to figure that out. Ideally the class would be readonly… not sure if this is the simplest way to go about it as type isn’t really a type, if you know what I mean.

class Types <  CouchRest::Model
view_by :instances,
  :map =>
    "function(doc) {
      emit(doc['couchrest-type'],1);
}",
:reduce =>
  "function(keys, values, rereduce) {
    return sum(values);
  }"
end
Post a comment