What’s New?

  • 02/02/09 – mostly fixes and tweaks for CouchDB trunk (tested against revision 0.9.0a740000). This includes a couple of breaking API changes – count became limit, 409 rather than 412 is returned on document update conflict, and ‘/’ in design docs is treated differently.
  • Potentially breaking change. Skipping validations is now done by adding attribute symbols to an object’s list rather than passing them to save. For example my_obj.validation_skip_list << :foo. This offers per object granularity over validations when working with bulk_save.
  • Potentially breaking change. load now returns an array if passed an array of size one. Previously it would have returned a single object.
  • Update conflict hook and property
  • Semantic consistency for bulk_save and bulk_save! wrt to save and save!
  • Multiple exception handling improvements
  • save_all that issues a bulk_save for an object and its has_one and has_many children
  • assignment of has_many relationships
  • Validations may be skipped by passing the attribute symbol(s) to save or save!.
  • Denormalisation via derived properties. Examples in spec/derived_properties_spec.rb.
  • Semantic changes for @ has_many#<< @. The parent object is now assigned to the child object prior to validation. This potentially breaking change was made to allow child objects to derive properties from a parent object.
  • Semantic consistency for load, load!, save and save!. The bang versions raise an exception when their more relaxed siblings would simply return nil.
  • Minimal support for CouchDB validation
  • Time storage changes. All Time objects are now converted to UTC and formatted as @ %Y/%m/%d %H:%M:%S +0000 @. Storing all Times as UTC should have been happening anyway. Formatting Times as above (as opposed to ISO 8601 as was done prior to 0.2.3) allows the Time strings to be passed directly to Date.new in a JavaScript interpreter.
  • Pagination! CouchDB offers great support for retrieving a subset of data, but the housekeeping is tricky. RelaxDB takes care of it.
    • Note that if you invoke paginate_by on an already created view, the necessary reduce function won’t be automatically created. Take a look at SortedByView and create the reduce func by hand.
  • Support for multi key post
    • For example, @ Numbers.all.sorted_by(:val) { |q| q.keys([1,2,3,5]) } @
  • Works with CouchDB 0.9 trunk as of 2009/01/02. Note that pagination won’t work correctly on trunk until issue COUCHDB-135 is fixed.

Note: Current versions require CouchDB 0.9 trunk. If you’re working with CouchDB 0.8 or 0.8.1, please build from commit @ a8a2d496462 @.

Overview

RelaxDB provides a Ruby interface to CouchDB. It offers a simple idiom for specifying object relationships. The underlying objects are persisted to the mighty CouchDB. Combined with the schema free nature of CouchDB, RelaxDB’s current strength lies in quick prototyping of object models.

A few facilities are provided including pretty printing of GET requests and uploading of JavaScript views.

A basic merb plugin, merb_relaxdb is also available.

For more complete documentation take a look at docs/spec_results.html and the corresponding specs.

Details

Getting started


  RelaxDB.configure :host => "localhost", :port => 5984
  RelaxDB.use_db "scratch"

Defining models


  class Writer < RelaxDB::Document
    property :name, :default => "anon"
    
    has_many :posts, :class => "Post"
    has_many :ratings, :class => "Post", :known_as => :critic
  end

  class Post < RelaxDB::Document
    property :created_at
    property :contents
  
    belongs_to :writer  
    has_many :ratings, :class => "Rating"
  end

  class Rating < RelaxDB::Document
    property :thumbs_up, :validator => lambda { |tu| tu >= 0 && tu < 3 }, :validation_msg => "No no"

    belongs_to :post
    belongs_to :critic
  end

Exploring models


  paul = Writer.new(:name => "paul").save

  post = Post.new(:contents => "foo")
  paul.posts << post                                          # post writer is set and post is saved
  post.created_at                                             # right now
  paul.ratings << Rating.new(:thumbs_up => 3, :post => post)  # returns false as rating fails validation
  paul.ratings.size                                           # 0

  # Simple views are auto created
  Rating.all.sorted_by(:thumbs_up) { |q| q.key(2).limit(1) }  # query params map directly to CouchDB

Paginating models


  # Controller (merb-action-args used for extracting view_params)

  def action(page_params={})
    u_id = @user._id

    @posts = Post.paginate_by(page_params, :writer_id, :created_at) do |p|
      p.startkey([u_id, {}]).endkey([u_id]).descending(true).limit(5)
    end
    render
  end
  
  # In your view
  
  <% @posts.each do |p| %>
    <%= p.contents %>
  <% end %>
  
  <%= link_to "prev", "/posts/?#{@posts.prev_query}" if @posts.prev_query %>
  <%= link_to "next", "/posts/?#{@posts.next_query}" if @posts.next_query %>  

Paginating over your own views



RelaxDB.paginate_view(page_params, "Letter", "by_letter_and_number", :letter, :number) do |p|
  p.startkey(["b"]).endkey(["b", {}]).limit(2)
end

A more illustrative example is listed in the .paginate_view spec in spec/paginate_spec.rb

Creating views by hand


  $ cat view.js 
  function Writer-allnames-map(doc) {
    if(doc.class == "Writer")
      emit(null, doc.name);
  }

  function Writer-allnames-reduce(keys, values) {
    var allnames = "";
    for(var i = 0; i < values.length; i++)
      allnames += values[i];
    return allnames;
  }
  $

  RelaxDB::ViewUploader.upload("view.js")
  RelaxDB.view("Writer", "allnames")                          # paul

Visualise

Create an object graph by simply running


RelaxDB::GraphCreator.create

Requires graphviz. Useful for visualising relationships between a limited number of document e.g. test fixtures. Description and example.

Incomplete list of limitations

  • Error handling is not robust
  • Destroying an object results in non transactional nullification of child/peer references
  • Objects can talk to only one database at a time
  • No caching is used. Although adding an LRU cache would be fairly straightforward, this hasn’t been done as it’s not yet clear what caching strategies will be most effective.