Ayende @ Rahien

Hi!
My name is Oren Eini
Founder of Hibernating Rhinos LTD and RavenDB.
You can reach me by phone or email:

ayende@ayende.com

+972 52-548-6969

, @ Q c

Posts: 5,968 | Comments: 44,489

filter by tags archive

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:

image

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

Brian Chavez

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.

Ken Egozi

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.

public class ModuleSearchCriteria : DetachedCriteria

    {

        private readonly ModuleSpecification specification;

        /// 

<summary
/// Creates a new <see out of a

        /// 

<see
///

    public ModuleSearchCriteria(ModuleSpecification specification) 
        : base(typeof(Module), "module")

{

// manipulating the criteria by the filter

}

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

Demis

hmmm. This just seems like a lot of effort for something Linq would excel at.

Ayende Rahien

Dennis,

You missed the point of complexity, it is not in the actual querying. It is the query building part that is hard.

pete w

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

MD
MD

It is cool indeed, but is it safe to build the search criteria on the client?

Ayende Rahien

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.

Grimace of Despair

I smell a NHQG update + revival :)

(I know, I know, ... I'm free to send a patch :P )

JP
JP

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?

Ayende Rahien

JP,

You are using CreateCriteria to do this

JP
JP

Can you please show an example using the code above?

Ayende Rahien

Please ask in nh users group

JP
JP

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

Ayende Rahien

It is something like:

public T[] FindAll(AbstractSearchFilter filter)

{

        var dc = DetachedCriteria.For

<t();

        filter.Apply(dc);

       return        dc.GetExecutableSession(session).List

<t();

}

JP
JP

Thanks!

You're the man.

Neil

Hi Oren,

I'm sure I saw some code like this in a pub recently :)

Thanks for putting it up on the blog.

Chris Tavares

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 Tavares

Then the JSON serializer is busted.

Comment preview

Comments have been closed on this topic.

FUTURE POSTS

No future posts left, oh my!

RECENT SERIES

  1. Career planning (6):
    24 Jul 2015 - The immortal choices aren't
  2. Production postmortem (4):
    23 Jul 2015 - The case of the native memory leak
  3. API Design (7):
    20 Jul 2015 - We’ll let the users sort it out
  4. What is new in RavenDB 3.5 (3):
    15 Jul 2015 - Exploring data in the dark
  5. The RavenDB Comic Strip (3):
    28 May 2015 - Part III – High availability & sleeping soundly
View all series

RECENT COMMENTS

Syndication

Main feed Feed Stats
Comments feed   Comments Feed Stats