[Updated 4/3/07 - Added recommendation to move add/remove methods to a custom collection.]
While using NHibernate 1.0x, developers who wanted support for generics often turned to Ayende's Hibernate.Generics. Since the use of generics is now natively supported by NHibernate 1.2, you'll no longer need this extremely helpful utility. Unfortunately, there's not a very simple way to migrate away from NHibernate.Generics without getting your hands a bit dirty, especially if you used the automatic wiring support for managing parent/child relationships. The steps described below outline a general approach to upgrading from Ayende's NHibernate.Generics to NHibernate's native support for generics. The entire process should take a couple hours to a couple days to complete, depending on the size of your project. Furthermore, once you've taken the initial upgrade steps, you can migrate each parent/child relationship, one at a time, without having to do them all at once.
NHibernate.Generics was certainly cool in that it provided automatic wiring between parent/child relationships. So when a child was added to parent, the parent was automatically assigned to the child. This all happened with a bit of pixie magic which was great for the wow effect. But there's a lot to be said for being explicit, and, as Ayende has explained, losing the magic will be better for project maintenance in the long run. If you've had to troubleshoot these automatic relationships or bring a new developer up to speed, you probably already know this.
- Download latest software:
- Verify that your unit tests are running with all greens...fix that red one there!
- Upgrade to NHibernate 1.2. You'll most definitely run into a few migration issues. The official overview of the changes to the API may be found within the NHibernate 1.2 Migration Guide. Listed below are a few items that you should pay particular attention to:
- Update all references to
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0"> with urn:nhibernate-mapping-2.2. References may be found in HBM and config files.
- All classes and collections with 1.2 are now loaded lazily by default. Consequently, if you run your application without modifying the lazy attribute, you'll most likely receive a number of "method x should be virtual." To resolve this, set
lazy="false" for each class and collection that you do not want loaded lazily.
- Replace your existing references to NHibernate.Generics with the one compatible with NHibernate 1.2, found at the bottom of this post.
At this point, you should be able to get all your unit tests passing again and have the application behaving as it did before the upgrade. Although the above steps are the only required steps for getting to NHibernate 1.2, it is highly recommended that you proceed with replacing all usages of Ayende's NHibernate.Generics with NHibernate's native support for generics. The following steps allow you to refactor away from Ayende's NHibernate.Generics, one parent/child relationship at a time, without breaking much of the existing code.
As an example, assume you have a Customer class having a one-to-many relationships to Orders. The Customer class, among other code, contains the following:
Customer.hbm.xml then contains:
The Order class, the "many" side of the relationship includes:
Finally, Order.hbm.xml contains:
In addition to the above code hiding complexity via automatic wiring, it also exposes the Orders list directly. Essentially, the Orders collection may be directly modified with no enforceable business rules. Suppose, for instance, that a Customer may only have 10 Orders for some reason. Exposing the collection directly makes it very difficult to impose the collection-modifying business rule. So during the refactoring process, we'll also want to improve collection encapsulation.
Another arguable smell in the code is that the parent/child relationship may be modified from either side. This introduces additional maintenance, adds complexity to managing the relationship, and increases opportunities to introduce bugs. Although we'll want to maintain the bi-directional traversability, we'll want to consolidate relationship management to the Customer side of the many-to-one relationship. (As a side note, Jimmy Nilsson, in Applying Domain-Driven Design and Patterns, asserts that even bi-directional traversability should be avoided in general. This is a point worth considering; but since we're focusing on migrating existing functionality, we'll keep the bi-directional traversability for now.)
To refactor the preceding code from Ayende's NHibernate.Generics to NHibernate 1.2 native support for generics with minimal breakage to existing code, repeat the following steps for each parent/child relationship:
- Create a new unit test to drive the add/remove functionality within the Customer class:
- To get the test passing, first add the following code to Customer: (Note that we're not removing any of the existing code yet.)
- Next modify/augment the Order class to reflect the following:
- Run your new unit test and watch it pass. You now have the business logic in place for managing the parent/child relationship. Now we'll move on to getting rid of the legacy code...
- Mark the legacy Customer.Orders collection as obsolete:
- Compile your application to view a listing of warnings showing you which parts of your code are using the deprecated collection. Get rid of the warnings, one by one, by modifying code to use
OrdersTemp and the new methods AddOrder and RemoveOrder. You may have to add one or more additional methods to the Customer class to better accommodate the previous, direct exposure of the collection.
- Mark the legacy Order.OrderedBy property as obsolete:
- Compile your application to view a listing of warnings showing you which parts of your code are using the deprecated property. Get rid of the warnings, one by one, by modifying code to use
OrderedByTemp. Since OrderedByTemp only has a getter, anywhere in your code which sets this property should be modified to use the parent Customer's AddOrder or RemoveOrder method, respectively.
- After compiling successfully, run only your business logic tests. Although NHibernate backed tests may still break at this point, all your business logic tests should be passing successfully.
- Remove the following code items from the Customer class: the reference to
NHibernate.Generics, the obsolete Orders collection, the EntityList<Order> member, and the WireUpEntities method along with any call to this method. Remove the following code items from the Order class: the reference to NHibernate.Generics, the obsolete OrderedBy property, the EntityRef<Customer> member, and the WireUpEntities method along with any call to this method.
- Rename Customer.OrdersTemp to Customer.Orders. Rename OrderedByTemp to OrderedBy. All business logic tests should again be passing.
- Modify Customer.hbm.xml to reflect the following:
- Modify Order.hbm.xml to reflect the following:
- Run all your unit tests including NHibernate backed tests...with a bit of tweaking to account for various object life-cycle scenarios everything should be working. See the note below for additional items to be aware of.
Although it looks like a lot of work, the above represents a structured path for refactoring the code with minimal breakage to existing code. This is the way to go if you have a large development team and can only check out a few files at a time. You could certainly bypass a few steps and do "bulk" changes if you don't mind code being broken; but if you take that path, I would recommend not checking anything in until all your unit tests are passing.
Taking this a bit further...E.T., in the comments, noted that adding AddOrder/RemoveOrder to Customer makes the class less cohesive; besides managing itself, it's now managing the orders collection as well. This could get a bit out of hand if additional collections are added to the Customer class. To alleviate this and keep Customer more cohesive, consider creating a custom collection for managing orders and then move add/remove from Customer to this custom collection. In this way, business rules may still be enforced without cluttering Customer with others' responsibilities. You'll need to take some extra steps to bind NHibernate to a custom collection using IUserCollectionType. Alternatively, if you don't need a full-blown cutom type, you can also simply create a collection wrapper to better encapsulate your collection related methods. An example of doing this is found in the sample code at the NHibernate Best Practices article.
The simple example described in this post has not taken into account the variety of cascade options that are available nor various object life-cycle scenarios. So unfortunately, the above refactoring steps will most likely not account for every parent/child relationship in your code. Consequently, it's very important that you have a strong base of unit tests to ensure that your migration from NHibernate.Generics to NHibernate's support for generics has been completed successfully. As may be assumed, the NHibernate documentation is well written and is your best resource for dealing with the variety of situations that you will likely encounter. For each refactored parent/child relationship, it's good to test the following:
- Do updates to a child, via the parent, still work?
- Do creations of new children, added to the parent, persist to the database?
- Do deletions of existing children from the parent, or by deleting the child directly, work correctly?
- Have other CRUD, cascade scenarios been tested?
As always, your comments (corrections?) are most welcome. If you have a particular scenario which you're having trouble resolving using the sample above and the NHibernate documentation, I'd recommend posting your question to the NHibernate Forums which are generally very responsive.
Finally, I'm happy to say that I'll be releasing the 2nd edition of NHibernate Best Practices very soon which will demonstrate a number of relationship types using NHibernate 1.2.
Billy McCafferty
Posted
03-27-2007 12:46 PM
by
Billy McCafferty