Breaking ActiveCouch in Fun and Inventive Ways

January 08, 2009 · 5 min read

It's been just over five months since I started playing with CouchDB. Until a few days ago I hadn't had much time to explore it properly, but since Christmas I've been tinkering with it almost non-stop — seeing what it can do and experimenting with it in my favourite language, Ruby.

Since I hadn't used Ruby with CouchDB before, I picked up ActiveCouch. It's a solid library, but after a few days I found that it worked with CouchDB in ways that didn't quite match how I think about data. That could be down to my inexperience, or it could just be that everyone models things differently. Either way, I pushed a copy of ActiveCouch to my server and started hacking on it.

One Application, One Database

Out of the box, ActiveCouch used one database per class. People went into a people database, comments into a comments database, articles into an articles database. My approach is to store all application data in a single database and differentiate document types with a doc.type attribute.

ActiveCouch now also installs views that let you access just the documents of a given type. You'll see these in the Futon client after your application has run once.

Unknown Functionality Dropped

I broke ActiveCouch::Base#find_from_url while I was working. I didn't know what it was for, and I wasn't using it, so I dropped it in 9982b348c. If you rely on this, please let me know what it does!

Syntactic Sugar

One of ActiveCouch's goals is to feel like ActiveRecord, and ActiveRecord provides #all and #first. I like them. ActiveCouch now provides them too.

New Attribute Types

Sometimes data is too simple to warrant its own class and an association. I've added a new attribute type, :array. Simple tags, for example, are a perfect fit. The default value is an empty array.

class Article < ActiveCouch::Base
  has :title, :which_is => :text
  has :tags, :which_is => :array
end

article = Article.new :title => "Sandwiches", :tags => [ "pickle" ]
article.tags << "cheese"
article.tags # => [ "pickle", "cheese" ]

I've also added a :datetime attribute type that defaults to Time.now.

Calculated Default Values

You can now set a default value that's lazily evaluated — computed when the instance is created rather than when the class is declared. Just set the default to a proc (or anything that responds_to?(:call)):

class Egg < ActiveCouch::Base
  has :hatches_at, :type => :datetime, :with_default_value => proc { 3.weeks.from_now }
end

The instance is yielded into the proc in case you want to base the calculation on it.

Conversion to Native Ruby Types

When you declare a type for a document attribute, ActiveCouch now tries to convert the value from the document into the corresponding Ruby type. For example, if you declare a :datetime attribute, you'll get a Time instance back instead of a String:

class Person < ActiveCouch::Base
  has :birthday, :which_is => :datetime
end

Person.find(:first).birthday.class # => Time

Changes to Associations and Adding belongs_to

I've changed has_many and has_one so they no longer embed data in the declaring document. These associations declare that other documents contain keys pointing back to the current class, so a query is needed to fetch them.

To complement that, there's a new belongs_to association that says the declaring class holds a foreign key pointing to an owning class:

class Pet < ActiveCouch::Base
  # This document will have a person_id attribute
  belongs_to :person
end

class Person < ActiveCouch::Base
  # Queries for doc.type = "pet" and doc.person_id = self.id
  has_many :pets
end

For now, you need to set the association on the belongs_to side. Setting it from the has_many side won't work yet:

# BAD
craig.pets << cat

# GOOD
cat.person = craig

Views with Multiple Keys

You can now create a view with more than one key attribute. Just call ActiveCouch::View#with_key multiple times and each key will be added to the view.

Design Documents with Multiple Views

The version of ActiveCouch I checked out only allowed one view per design document. I think that was a bug — there was existing code meant to merge views, but it wasn't working. I've fixed it, and design documents now properly support multiple views.

Finders Have Conditions, Not Params

It felt unnatural typing :params => { ... } when writing finders. ActiveRecord uses :conditions, so now ActiveCouch does too:

Person.find(:all, :conditions => { :last_name => "Smith" })

Automatic View Generation for Custom Finders

I don't want to worry about manually writing and installing views before running a finder with conditions. Now, the first time you run such a finder, ActiveCouch generates and installs the appropriate view for you.

Probably Lots More

I've still got to clean up quite a few changes, improve test coverage, and write documentation. I'm using this fork for a real application, so things should get better over time.

Want It?

You can clone my changes with Git:

git clone http://barkingiguana.com/~craig/code/activecouch.git

Getting Started

If you don't already have CouchDB set up, do that first. On Ubuntu, I wrote a brief guide to getting it running. On OS X, install MacPorts and run sudo port install couchdb.

First, configure ActiveCouch to connect to your CouchDB instance. Set site to the URL CouchDB is listening on, and pick a database name that makes sense for your application:

ActiveCouch::Base.class_eval do
  set_database_name 'blog'
  site 'http://localhost:5984/'
end

Then define some classes to work with:

class Author < ActiveCouch::Base
  has :name, :which_is => :text
  has :email_address, :which_is => :text
  has_many :articles
end

class Article < ActiveCouch::Base
  has :title, :which_is => :text
  has :status, :which_is => :text, :with_default_value => "draft"
  has :body, :which_is => :text
  belongs_to :author
end

has declares an attribute. has_many, has_one, and belongs_to work similarly to ActiveRecord — though without the extensive customisation options. The association name must match the class name on the other side.

And that's it. Use your classes however makes sense for your application:

author = Author.create :name => "Craig R Webster",
  :email_address => "craig@barkingiguana.com"

a = Article.new
a.title = "Getting started with ActiveCouch"
a.body =<<-EOF
  Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
  tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
  quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
  consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
  cillam dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
  proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
EOF
a.author = author
a.save

Article.find(:all)
Author.first
Article.find(:first, :conditions => { :status => "draft" })

Known Issues

Not so much a bug as a not-yet-implemented feature: ActiveCouch::Base#find doesn't support ordering. It should be possible to add, but I haven't started on it yet. If you need ordering, a patch would be very welcome.

Problems or Feedback?

There are bound to be bugs lurking in there. Bug reports, patches, and feedback are always welcome — leave a comment or get in touch directly.

Questions or thoughts? Get in touch.