Saturday, October 4, 2008

My First NHibernate Project, Part 2

Part1, Part3

The next time...


Now that I have one project under my belt, what will I do differently next time? I think now that I have more experience and a better understanding of NHibernate I'd probably do several things a bit differently if I were starting this project over. Here's a list of the main things I'd consider the next time I started a new project using NHibernate.

NConstruct... to use or not to use?
Although I'll consider NConstruct again (if nothing else, their development staff appears quite responsive to feedback and is very quick to incorporate requests), I'll probably try creating at least some of the classes and mapping files myself. Maybe I'm just a bit of a control freak, but now that I'm familiar with the file structures, I think it will be easier to create them myself than to use a tool and then have to correct the code generated by that tool. Still, it was able to generate quite a lot of code and XML with very little effort, so I'd probably still use it for most of my basic entities unless I decide to use interfaces and then just clean up the property names afterward (Refactor! is a pretty handy tool for helping with that). Maybe I'll beat my DBA around the head a little bit as well to see if I can get him to loosen up on his table naming rules a bit, which would drastically cut down on the amount of fixing up I need to do to property names!

Interfaces
Next time i think I'll also look into using interfaces with NHibernate. This is a bit of a trade-off, so I'm not sure whether I'll stick with that approach though. On one hand, using interfaces makes unit testing easier. On the other hand, with interfaces you can't map database columns to fields... you must map to properties, which may not be acceptable if your setter method does things that you don't need to do when initializing the object from the database. For example, if your workplace is anything like mine, you may have to work with a legacy database with some not-too-clean data. You might want to add validation in your setter so that new data going in will be clean and shiny, but couldn't use that setter when initializing an object from the database because you need to accept the record as-is, errors and all.

Components
Components sound interesting because they're basically a way to deconstruct part of a table into a separate class which is then included as a property in the table's entity representation. The classic example of this is a table that includes address information (such as almost any database that includes contact records). The address fields (street address, city, state, zip code, etc) can be defined as a component of type Address and the entity will then contain a field of type Address that contains the values of these columns. This is a good way to create small, general-purpose business objects that can be reused in different applications. In my current project I have a couple tables that could have been represented this way had I been able to absorb everything about NHibernate at once! Instead, I modified the NConstruct-derived classes for these tables so that they implemented a pre-existing business object interface, encapsulated a concrete instance of that interface within the class to hold the properties of that interface, and modified the generated getters and setters of the entity class to use that instance. This could have been accomplished more easily using a component (although in this specific case it was a better implementation choice to not use a component, but at the time I didn't have enough familiarity with NHibernate to evaluate the applicability of components to the problem.

Named Queries
Named queries could also be useful for encapsulating some of the data layer. My data layer code consists of a number of methods that query the database to return single entities or collections of entities that match specific criteria. It also contains methods that return collections of arrays consisting of specific properties from entities (for example, when I only need a couple properties such as the id and name in order to populate a combobox or a list). This means that the data layer consists mainly of methods that look basically like this:

Public Function GetSomeEntitiesBySomeProperty(ByVal propertyValue As PropertyType) As IList(Of EntityType)
Try
Dim s As ISession = GetSession()
Dim entities As IList(Of EntityType) = s.CreateQuery(queryString) _
.SetString(0, propertyValue).List(Of EntityType)()
Return entities
Finally
DoSessionCleanup()
End Try
End Function

where GetSession and DoSessionCleanup are methods in my data layer that manage the reuse of Session objects.

The question then is where does the query string come from? It could be hard-coded into each method in a string, but that's a fairly inflexible approach. It could also be coded into the method using properties of the ICriteria interface, but that suffers from the same problem (not to mention is just much to wordy for my taste, but if you like the declarative programming approach, feel free to go for it; I find that approach quite useful when writing unit tests in NUnit, but I'll probably pass in favor of HQL and SQL queries in NHibernate).

I ended up putting my query strings in Resource strings, the advantage being that it takes the query strings out of my code and into a resource file where it can be modified without touching the actual code. I could achieve basically the same end result using named queries, so that's an approach I'll probably look into next time (or maybe this time... there's still time to move the queries from the resource file into the mapping files if I want to give it a shot). The main advantage of moving the queries from the resource file into the named queries is that named queries are parsed once as opposed to queries passed in as strings, which are parsed each time unless they're cached (I believe NHibernate currently caches a certain number of the most recently used queries).

Generics and ICriteria
Since the majority of my queries are very similar -- "Get all of the {some object}s where {some property} is {some value}" -- occasionally with an "ordered by {some property}" I could could probably also drastically cut down on the number of queries I need to write if I used some combination of ICriteria and generic methods. But wait... didn't I just say that ICriteria wasn't any more flexible than hard-coding the queries as strings? Yes, but you for basic lookups you can pack a lot of bang for your buck if you write your lookup method using generics and a few input parameters. Here's a quick example of my previous pseudocode implemented using generics:
Public Function GetFilteredEntities(Of T As EntityBase) _
(ByVal propertyName as String, ByVal propertyValue as Object, _
Optional ByVal orderBy as String = Nothing) As IList(Of T)
Try
Dim s As ISession = GetSession()
Dim crit as ICriteria = s.CreateCriteria(GetType(T)) _
.Add(Expression.Eq(propertyName, propertyValue))
If orderBy IsNot Nothing AndAlso Not String.IsNullOrEmpty(orderBy.Trim()) Then
crit.AddOrder(Order.Asc(orderBy))
End If
Return crit.List()
Finally
DoSessionCleanup()
End Try
End Function

This one method could replace over a dozen methods in my current data layer, along with their associated query string resources.

While generic classes are a very powerful feature, I'm starting to believe that generic methods may even be more powerful. By using generics, this method can return a list of any entity type instead of a single type. In the above example, all of my entity classes inherit from the base class EntityBase so I specified that the generic type passed in be of that type or a subclass. Since each entity will have different properties and I may need to filter the same entity by different properties, I am passing in the name of the property by which to filter as a parameter and the value of that property as another parameter. Also, since ordering the results is a common requirement, I've addedn an optional parameter to specify a column by which to order the results.

Granted, this one method only handles a very limited set of possible queries, but I've found that this limited set actually covers most of the queries I need to do. In my experience I'm usually doing one of three things. Most often, I'm getting a single item from the database by Id. If I'm not getting a single item, I'm either getting everything from a table (but usually only if the table is fairly small), in which case I could use a method like the one above but without the propertyValue an propertyName parameters, or I'm getting a simple subset of a table, filtered on a single value (all users in group X, all orders for customer Y, all items for order Z, etc). In this last case, the method above would work perfectly well. I could also use the method listed above for the first case where I'm getting a single item by Id, but I'd probably write another method similar to the one above except that it executes a Load() method instead of using ICriteria and returns a single item instead of a collection containing one item.

If you're concerned with needing to filter on multiple criteria, you could easily extend the method listed above by passing in a dictionary of property name/value pairs instead of the single propertyName, propertyValue parameters. The method would then iterate over the dictionary, adding crit.Add(Expression.Eq(prop.Key, prop.Value)) for each property in the dictionary. This would build a filter where all of the properties must equal their specified values. If you wanted to return everything from a table, pass in an empty dictionary (I'd probably allow Nothing as a value and check for that to skip the loop as well).

If you really wanted to get fancy, you could probably come up with ways to pass in a data structure that can specify more advanced filtering such as "greater than" and "less than" instead of just "equals" or to allow "or" as well as "and," but then your setup for the method and handling of the data structure start becoming too cumbersome to be useful in opinion. Unless I really needed the data layer to be totally dynamic, I'd go as far as the parameter arrays and write specialized methods for the remaining 10% of my queries that can't fit into my general-purpose methods.

Coming up

In part three of my series on my first NHibernate project, I'll be discussing the main lessons I've learned and giving a few tips for new NHibernate users.
full window

0 comments: