Saturday, March 17, 2012

Keeping a Domain Model Pure with RavenDB

Within the last few months I've finally started using NoSQL databases in some of my projects. I started with db4o, then refactored my code to use RavenDB. Part of the learning curve, that was tough for me, was the idea that a domain model would change based on the chosen persistence layer (db4o, Raven, etc...). That didn't sit well with me. While it can be a fine solution, it's just not for me.

Based on a few months of using these tools, I demonstrate here how I was able to keep my domain model relatively pure and still use RavenDB. What I mean by "pure" is that I don't want traces of the persistence layer manifesting themselves in the domain model. And I want properties, that are natural for a class, to exist in that class. For example, a Team class should have a List<Player> property, not just a list of Player IDs, or parts of the Player class represented again in the Team class.

For this post, we'll be looking at Presto (http://presto.codeplex.com/) source code, and how it kept a somewhat pure domain model while using RavenDB. We'll be working with these two domain classes: Application and CustomVariableGroup. An Application is simply an app, such as an app to store music, or an app to record employee information for your company. A CustomVariableGroup is simply a container for CustomVariables. Defining a CustomVariable is unnecessary here. It's simply a POCO, used as a property on an Application class.

Let's start by showing how we create a document store. Here is the actual property:






And here is the method that sets it:


Now let's show the two domain classes. Here is Application:


The property that has the comment "// For RavenDB" after it, exists so that we know the IDs of other entities that belong to this entity. This is really the entire point of what we're doing here. We store the IDs of the entities within this class, so they can be populated from a RavenDB call when it's time to retrieve our entities from the DB. It is the responsibility of the Raven data layer to manage this.

This is absolutely Raven-specific. We could have defined a partial class to hold this Raven-specific property, and kept this class file completely pure, but I didn't want to take it that far.

Also notice that we have a property CustomVariableGroups. This is what I mean about maintaining a pure domain model. With RavenDB, there are some guidelines that, since it's a document database, we don't store full documents/entities in another entity. Instead, you store only some properties of other entities in your current entity. But, I'm getting ahead of myself here.

Also notice the JsonIgnore attribute on the CustomVariableGroups property. RavenDB will ignore this property when saving to the database. Raven, like db4o, will persist POCO properties. However, unlike db4o, that persisted property is frozen in state, and the *instance* of that property belongs to that class. For example, if changes are made to a CustomVariableGroup in the DB, those changes *will not* be reflected when retrieving our class. This is why we store the IDs; so that we can get the latest version of our property, even if it has been changed by someone, or something, else.

Let's show our CustomVariableGroup class:


To finish showing our entities, let's show the base class. This *is* Raven-specific, but it's a base class for exactly that purpose, and the derived classes don't show this information in their class files.


The Etag property is out of the scope of this blog.

If you've been able to stay with me so far, we're almost done. Now we need to show how the Raven data layer manages these properties.

Here is how we save an Application:


What happens here is that we loop through all of the CustomVariableGroups in the Application, and add their IDs to the CustomVariableGroupIds property. When the entity gets saved, the IDs will get saved, and not the actual POCOs. This way, when we load an Application from the DB, we can retrieve the latest instance of the POCOs by the IDs:


The important part here is the Include() line. That instructs RavenDB to retrieve, in the same session, the CustomVariableGroups with those IDs. Because we've done that, the code to call HydrateApplication can then add the appropriate CustomVariableGroups to each Application object, all on the same session:


That's it. Now that that's done, each Application has the latest CustomVariableGroup associated with it. Those CustomVariableGroups can change over time, yet we still get the latest, and correct, one using this approach.

6 comments:

Ayende Rahien said...

Robert,
Please note that what you are doing is actually going directly _against_ the design zen of RavenDB.
You might want to read this article about it:

http://ravendb.net/docs/theory/document-structure-design

Robert Horn said...

Ayende,

I've actually read that article before. Two points are particularly interesting to me.

First: "You can certainly store references to other documents, but if you need to refer to another document to understand what the current document means, you are probably using Raven incorrectly."

There is still a need for that. Indeed a team could have List property. If you want to list all of the players for a team, a reference is needed.

Second: "The Aggregate Root for an Order will contain Order Lines, but an Order Line will not contain a Product. Instead, it contains a reference to the product id."

That's exactly what this blog is addressing.

Raven is awesome. I'm glad I found it. But I haven't yet found an answer about what to do in situations where aggregates reference roots. Sure, you could store Player.Name in the aggregate, but what if the name changes?

Robert Horn said...

In my previous comment, this:

"Indeed a team could have List property."

Should be this:

"Indeed a team could have List<Player> property."

Unknown said...

Robert,

You have pointed the doubts that I still have about the use of RavenDB, when references are needed. But I agree with you, Raven is great!!

Maybe the problem is that to work with Document Databases, we need to change our mind and the concept of how things are created and driven, even if that implies changes in the object design.

I think we need to keep in mind that we are not really creating a pure object oriented system, where there are references from objects to objects. But documents, which are almost complete collections of information data, even if that implies in breaking references sometimes.

Robert Horn said...

Daniel, I agree with you. The intent of this blog is simply how to use Raven with a good object model *if that's what you want to do.*

The intent here is not to say that this is how it should be done. This blog just describes *one* way it could be done.

Chris Marisic said...
This comment has been removed by a blog administrator.