Skip to content
debasishg edited this page Sep 13, 2010 · 17 revisions

scouchdb : Persisting Scala Objects in CouchDB

scouchdb offers a Scala interface to using CouchDB. Scala offers objects and classes as the natural way to abstract entities, while CouchDB stores artifacts as JSON documents. scouchdb makes it easy to use the object interface of Scala for persistence and management of Scala objects as JSON documents.

Motivation

The primary motivation for making scouchdb is to offer a form of CouchDB driver to manipulate objects in a completely non-intrusive manner. The Scala objects are not CouchDB aware and remain completely transparent of any CouchDB dependency. Incorporating CouchDB specific attributes like id and rev take away a lot of reusability goodness from domain objects and make them constrained only for the specific platform.

Sample Session

Suppose I have a Scala class used to record item prices in various stores ..

case class ItemPrice(store: String, item: String, price: Number)

and I would like to store it in CouchDB through an API that converts it to JSON under the covers and issues a PUT/POST to the CouchDB server.

Here is a sample session that does this for a local CouchDB server running on localhost and port 5984 ..

// specification of the db server running
val http = new Http
val test = Db(Couch(), "test") 

// create the database
http(test create)

// create the Scala object
val s = ItemPrice("Best Buy", "mac book pro", 3000)

// create a document for the database with an id
val doc = Doc(test, "best_buy")

// add
http(doc add s)

// query by id to get the id and revision of the document
val id_rev = http(test ref_by_id "best_buy")

// query by id to get back the object
// returns a tuple3 of (id, rev, object)
val sh = http(test.by_id[ItemPrice]("best_buy"))

// got back the original object
sh._3.item should equal(s.item)
sh._3.price should equal(s.price)

Suppose the price of a mac book pro has changed in Best Buy and I get a new ItemPrice. I need to update the document that I have in CouchDB with the new ItemPrice object. For updates, I need to pass in the original revision that I would like to update ..

val new_itemPrice = //..
http(doc update(new_itemPrice, sh._2))

The Scala client is at a very early stage. All the above stuff works now, a lot more have been planned and is present in the roadmap. The main focus has been on the non intrusiveness of the framework, so that the Scala objects remain pure to be used freely in other contexts of the application. The library uses the goodness of Nathan Hamblen’s dispatch (http://databinder.net/dispatch) library, which provides elegant Scala wrappers over apache commons Java http client and a great JSON parser with a set of extractors.

Property filtering through Annotations

Very often we need to have different property names in the JSON document than what is present in the Scala object. Sometimes we may also want to filter out some properties while persisting in the data store. The framework uses annotations to achieve these functionalities (much like the ones used by jcouchdb (http://code.google.com/p/jcouchdb/), the Java client of CouchDB) ..

case class Trade(
  @JSONProperty("Reference No")
  val ref: String,

  @JSONProperty("Instrument"){val ignoreIfNull = true}
  val ins: Instrument,
  val amount: Number
)

When this class will be spitted out in JSON and stored in CouchDB, the properties will be renamed as suggested by the annotation. Also selective filtering is possible through usage of additional annotation properties as shown above.

Handling aggregate data members for JSON serialization is tricky, since erasure takes away information of the underlying types contained in the aggregates. e.g.

case class Person(
  lastName: String
  firstName: String,

  @JSONTypeHint(classOf[Address])
  addresses: List[Address]
)

Using the annotation makes it possible to get the proper types during runtime and generate the proper serialization format.

CouchDB Views

One of the biggest hits of CouchDB is the view engine that uses the power of MapReduce to fetch data to the users. The current version of the framework does not offer much in terms of view creation apart from basic abstractions that allow plugging in “map” and “reduce” functions in Javascript to the design document. There are some plans to make this more Scala ish with little languages that will enable map and reduce function generation from Scala objects.

But what it offers today is a small DSL that enables building up view queries along with the sea of options that CouchDB server offers ..

// fetches records from the view named least_cost_lunch
http(test view(Views.builder("lunch/least_cost_lunch").build))

// fetches records from the view named least_cost_lunch 
// using key and limit options
couch(test view(
  Views.builder("lunch/least_cost_lunch")
       .options(optionBuilder key(List("apple", 0.79)) limit(10) build)
       .build))

// fetches records from the view named least_cost_lunch 
// using specific keys and other options for deciding output filters
http(test view(
  Views.builder("lunch/least_cost_lunch")
       .options(optionBuilder descending(true) limit(10) build)
       .keys(List(List("apple", 0.79), List("banana", 0.79)))
       .build))

// temporary views
val mf = 
  """function(doc) {
       var store, price;
       if (doc.item && doc.prices) {
         for (store in doc.prices) {
           price = doc.prices[store];
           emit(doc.item, price);
         }
       }
  }"""

val rf = 
  """function(key, values, rereduce) {
       return(sum(values))
  }"""

// with grouping
val aq = 
  Views.adhocBuilder(View(mf, rf))
       .options(optionBuilder group(true) build)
       .build
val s = http(test adhocView(aq))
s.size should equal(3)

// without grouping
val aq_1 = 
  Views.adhocBuilder(View(mf, rf))
       .build
val s_1 = http(test adhocView(aq_1))
s_1.size should equal(1)

Attachment Handling

val att = "The quick brown fox jumps over the lazy dog."

val s = Shop("Sears", "refrigerator", 12500)
val d = Doc(test, "sears")
var ir:(String, String) = null
var ii:(String, String) = null

// create a document from an object    
couch(d add s)
ir = http(d ># %(Id._id, Id._rev))
ir._1 should equal("sears")

// query by id should fetch a row
ii = http(test by_id ir._1)
ii._1 should equal("sears")

// sticking an attachment should be successful
http(d attach("foo", "text/plain", att.getBytes, Some(ii._2)))

// retrieving the attachment should equal to att
val air = http(d ># %(Id._id, Id._rev))
air._1 should equal("sears")
http(d.getAttachment("foo") as_str) should equal(att)

Attachments can also be created for non-existing documents. In that case, the document also gets created along with the attachment. Have a look at the test suite for the spec.

Handling Bulk Inserts / Updates / Deletes

Documents can be manipulated in bulks. Through one single POST, new documents can be simultaneously added, existing ones updated and deleted. Here is a sample session ..

// should insert 2 new documents, update 1 existing document and delete 1 

// a scala object
val s = Shop("Shoppers Stop", "refrigerator", 12500)
val d = Doc(test, "ss")

// another scala object      
val t = Address("Monroe Street", "Denver, CO", "987651")
val ad = Doc(test, "add1")

var ir:(String, String) = null
var ir1:(String, String) = null

// create a new document    
http(d add s)
ir = http(d ># %(Id._id, Id._rev))
ir._1 should equal("ss")

// create another new document      
http(ad add t)
ir1 = http(ad ># %(Id._id, Id._rev))
ir1._1 should equal("add1")

// new scala objects     
val s1 = Shop("cc", "refrigerator", 12500)
val s2 = Shop("best buy", "macpro", 1500)
val a1 = Address("Survey Park", "Kolkata", "700075")

// a dsl that adds s1 and s2, updates s and deletes t      
val d1 = bulkBuilder(Some(s1)).id("a").build 
val d2 = bulkBuilder(Some(s2)).id("b").build
val d3 = bulkBuilder(Some(s)).id("ss").rev(ir._2).build
val d4 = bulkBuilder(None).id("add1").rev(ir1._2).deleted(true).build

http(test bulkDocs(List(d1, d2, d3, d4), false)).size should equal(4)

Dependencies