Sunday, February 14, 2010

Why I don't like ActiveRecord for Domain Model Persistence

When it comes to a rich domain modeling, I am not a big fan of the ActiveRecord model. The biggest problem that it entails is invasiveness - the persistence model invades into my domain model. And this was also my first reaction to the Lift-CouchDB integration module which was released recently.

The moment I say ..

class Person extends CouchRecord[Person] {
  //..
}


my domain model becomes tied to the persistence concerns.

When I started scouchdb, my very first thought was to make it non-invasive. The Scala objects must be pure and must remain pure and completely oblivious of the underlying persistence model. In the age of polyglot persistence there is every possibility that you may need to persist a domain model across multiple storage engines. I may be using a JPA backed relational store as the enterprise online database, which gets synchronized with some offline processing that comes from a CouchDB backend. I need the domain model persistence in both my Oracle and my CouchDB engines. The ActiveRecord pattern is difficult to scale to such requirements.

Here's what I implemented in scouchdb ..

// Scala abstraction : pure
case class ItemPrice(store: String, item: String, price: Number)

// specification of the db server running
val couch = Couch("127.0.0.1")
val item_db = Db("item_db")

// create the database
couch(item_db create)

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

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

// add
couch(doc add s)

// query by id to get the id and revision of the document
val id_rev = couch(item_db by_id "best_buy")

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

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


It's a full cycle session of interaction with the CouchDB persistence engine without any intrusion into the domain abstraction. I am free to use ItemPrice domain abstraction for a relational storage as well.

CouchDB offers a model of persistence where the objects that we store should be close to the granularity of domain abstractions. I should be able to store the entire Aggregate Root of my model components directly as JSON. ActiveRecord model offers a lower level of abstraction and makes you think more in terms of persistence of the individual entities. The thought process is so relational that you ultimately end up with a relational model both in terms of persistence and domain. With CouchDB you need to think in terms of documents and views and NOT in terms of relations and tables. I blogged on this same subject some time back.

The philosophy that I adopted in scouchdb was to decouple the domain entities from the persistence layer. You hand over a pure Scala object to the driver, it will extract a JSON model from it and write it to CouchDB. I use sjson for this serialization. sjson works totally based on reflection and can transparently serialize and deserialize Scala objects that you hand over to it. From this point of view, the three aspects of managing domain abstractions, JSON serialization and persistence into CouchDB are totally orthogonal. I think this is difficult to get with an ActiveRecord based model.

8 comments:

Sergio Bossa said...

Definitely agree.

Active Record may be a good fit only for anemic domains, which are a bad thing anyways :)

So go with pure domain modeling, choose proper roots and hide persistence behind repositories, possibly using automatically serialized or custom "view" objects.

Great post.
Cheers,

Sergio B.

Unknown said...

FWIW, it's possible to do this with lift-couchdb. The part of the integration you mention is merely the lift-record implementation, which is truly bound to one store. However, lift-couchdb also includes a straight JSON-oriented approach which you can easily combine with lift-json's case class extraction functionality.

This spawned a mail thread (currently short) on the lift list as well:

http://groups.google.com/group/liftweb/browse_thread/thread/1eaec1097b11f02c?hl=en

Unknown said...

@Ross :

Yes, I am talking about the Lift-CouchDB integration only. The moment you define a domain abstraction as being statically dependent on a persistence implementation, you lose the ability to reuse it in other contexts. As I mentioned in my blog post, I may need Person object for a JPA based persistence in a relational store as well.

Unknown said...

Yes, I am talking about lift-couchdb as well -- it has two separate layers and you can use the underlying JSON layer closer to the scouchdb example. I just mean to clarify that it's the lift-couchdb record implementation, not lift-couchdb as a whole.

We're starting to talk about different ways to enable multiple persistence domains with a single model type on the lift list, so thanks for your post.

Giorgio said...

Persistence ignorance is a good thing. In Php we are finally achieving it after 15 years of language evolution.

Unknown said...

@Ross:

I should have been more specific. I was indeed talking about the Record implementation, which is an incarnation of the ActiveRecord pattern. Looking forward to a couchdb based persistence strategy in Lift :) ..

Dustin Ted Whitney said...

Totally agree, and though it has nothing to do with Scala, I love the Grails framework for completely decoupling its domain objects from persistence code.

Anonymous said...

http://martinfowler.com/bliki/AnemicDomainModel.html

with your approach you end up with a model that has data but not behavior, check out Martins Fowler's Anemic Domain model descriptio.