Making the complex trivial: Rich Domain Querying
It is an extremely common issue and I talked about it in the past quite a few times. I have learned a lot since then, however, and I want to show you can create rich, complex, querying support with very little effort.
We will start with the following model:
And see how we can query it. We start by defining search filters, classes that look more or less like our domain. Here is a simple example:
public abstract class AbstractSearchFilter { protected IList<Action<DetachedCriteria>> actions = new List<Action<DetachedCriteria>>(); public void Apply(DetachedCriteria dc) { foreach(var action in actions) { action(dc); } } } public class PostSearchFilter : AbstractSearchFilter { private string title; public string Title { get { return title; } set { title = value; actions.Add(dc => { if(title.Empty()) return; dc.Add(Restictions.Like("Title", title, MatchMode.Start)); }); } } } public class UserSearchFilter : AbstractSearchFilter { private string username; private PostSearchFilter post; public string Username { get { return username; } set { username = value; actions.Add(dc => { if(username.Empty()) return; dc.Add(Restrictions.Like("Username", username, MatchMode.Start)); }); } } public PostSearchFilter Post { get { return post; } set { post = value; actions.Add(dc=> { if(post==null) return; var postDC = dc.Path("Posts"); // Path is an extension method for GetCriteriaByPath(name) ?? CreateCriteria(path) post.Apply(postDC); ); } } }
Now that we have the code in front of us, let us talk about it. The main idea here is that we move the responsibility of deciding what to query to the hands of the client. It can make decisions by just setting our properties. Not only that, but we support rich domain queries using this approach. Notice what we are doing in UserSearchFilter.Post.set, we create a sub criteria and pass it to the post search filter, to apply itself on that. Using this method, we completely abstract all the need to deal with our current position in the tree. We can query on posts directly, through users, through comments, etc. We don't care, we just run in the provided context and apply our conditions on it.
Let us take the example of wanting to search all the users who posts about NHibernate. I can express this as:
usersRepository.FindAll( new UserSearchFilter { Post = new PostSearchFilter { Title = "NHibernate" } } );
But that is only useful for static scenarios, and in those cases, it is easier to just write the query using the facilities NHibernate already gives us. Where does it shine?
There is a really good reason that I chose this design for the query mechanism. JSON.
I can ask the json serializer to serialize a JSON string into this object graph. Along the way, it will make all the property setting (and query building) that I need. On the client side, I just need to build the JSON string (an easy task, I think you would agree), and send it to the server. On the server side, I just need to build the filter classes (another very easy task). Done, I have a very rich, very complex, very complete solution.
Just to give you an idea, assuming that I had fully fleshed out the filters above, here is how I search for users name 'ayende', who posted about 'nhibernate' with the tug 'amazing' and has a comment saying 'help':
{ // root is user, in this case
Name: 'ayende',
Post:
{
Title: 'NHibernate',
Tag:
{
Name: ['amazing']
}
Comment:
{
Comment: 'Help'
}
}
}
Deserializing that into our filter object graph gives us immediate results that we can pass the the repository to query with exactly zero hard work.
Comments
I did something similar to this in a recent project using JSON to serialize search criterias.
Except, I didn't put the criteria building in the property setters. They're like SearchDTO objects. The DAOs themselves take care of translating the filter objects into their respective DetachedCriterias to avoid SearchObjects and UI dependent on NHibernate.dll.
Cool thing. Never used JSON serialisation for that, usually I'll send the query in Form/Querystring and use the [DataBind] power to get the filter.
My only problem with it, is that the filters (which are domain related) become way too aware of NH imo. I don't like my IReopsitory methods to accept NH based things as parameters
What I do is to have the Filter as a an anaemic NH-free class, located within the Domain.
in the domain implementation assembly (where the NH stuff goes) I'd declare a type inheriting from DetachedCriteria, that accepts the filter in the constructor.
<summary
/// Creates a new <see out of a
<see
///
{
}
That way it's also easy to de-serialise a filter to a view (say for the sake of pre-populating a search screen), using the filter, keeping the view free from NH dependant objects
hmmm. This just seems like a lot of effort for something Linq would excel at.
Dennis,
You missed the point of complexity, it is not in the actual querying. It is the query building part that is hard.
I've done this kind of thing before, both in and out of NHibernate. I have found to to be quite useful.
What I would love to see is a technology that provides a sql server reporting services interface, but works with business objects and not sql, to me this would be a killer app.
All too often, we create applications with rich business objects, only to write sql-based reports, to me, it makes more sens to re-use these object for reporting
It is cool indeed, but is it safe to build the search criteria on the client?
MD,
What do you mean safe?
From SQL injection perspective, NH ensure that this is not a problem.
From optimization perspective, your filter model ensure that you don't allow truly bad things happening.
I smell a NHQG update + revival :)
(I know, I know, ... I'm free to send a patch :P )
Sorry my ignorance but how do you pass the list of actions to a single detachedcriteria? In other words, if you have a repository which accepts a single detachedcriteria, how do you merge them all to a single one?
JP,
You are using CreateCriteria to do this
Can you please show an example using the code above?
Please ask in nh users group
I'm just not getting what you intend to do in the FindAll method. You receive an AbstractSearchFilter as a parameter but I cannot understand what you do in the method's body :)
Can you please reply how you think this method should behave?
Thanks
It is something like:
public T[] FindAll(AbstractSearchFilter filter)
{
<t();
<t();
}
Thanks!
You're the man.
Hi Oren,
I'm sure I saw some code like this in a pub recently :)
Thanks for putting it up on the blog.
Just a minor nit to pick - your JSON is invalid. You're required to quote all strings, including the ones on the left of the ':' characters.
Chris,
It _works_.
Then the JSON serializer is busted.
Comment preview