Relational Database Persistence with NHibernate, Part 2
This article continues from the May/June 2009 issue of CODE Magazine (Quick ID 0906081) which covered why you want to use NHibernate, techniques for configuring NHibernate, how to map your objects to your data entities, and how to load basic objects. One-to-Many Collections You now have the ability to load a given SalesOrderHeader and load its associated Customer object. What about loading all the SalesOrderHeaders for a given Customer? That’s called a one-to-many (OTM) relationship. Since there will potentially be many orders for a customer, you will need a property on Customer whose type will be an IEnumerable-derived type or list structure of some kind (Array, IList<T>, IEnumerable<T>, and so on). This will allow you to for-each over the list of orders for a given customer, which would be the most natural thing from the .NET code perspective. In order to map this situation to a database structure with NHibernate, you have to choose among five different types of list structures. - Set - This is similar to the mathematical “finite set” concept (i.e., a list with no duplicates which may or may not be ordered). This corresponds to the Iesi.Collections.ISet interface from the Iesi.Collections.dll assembly shipped with NHibernate. A separate assembly is required because there is no ISet available in the .NET Framework Base Class Library. ISet is not included in NHibernate because it is beyond the scope of what a persistence framework is responsible for. Thus, ISet has its own assembly.
- Bag - A list of entities which may contain duplicates and which has no specific or intentional order. This type corresponds to the ICollection/ICollection<T> or IList/IList<T> .NET types.
- Array - A plain, regular .NET array of a particular type.
- Map - An IDictionary/IDictionary<T> whose key and value are specified by the mapping.
- List - Another form of ICollection/IList (or their generic forms) similar to bag except the list index is specified by the mapping. A real-world example of this might be line items for an order where the index is the LineItemNumber column so that, when loaded from the database, NHibernate will set up the IList ordered by the LineItemNumber.
Bag and Set are perhaps the most commonly used list mappings. I would encourage you to try the others to see what fits best with your situation. Adding the Collection to the Entity Start by adding a property called “SalesOrders” to the Customer class of type IList<SalesOrderHeader> (IList(Of SalesOrderHeader) for the VB folks). Make sure to make it virtual. You should also consider initializing the IList in your constructor to a default empty List<SalesOrderHeader> in case you use your Customer object before it’s saved or loaded through NHibernate. At this point, you may be wondering: “What if I don’t want an IList directly exposed to callers? What if I want the IList private and have accessor/mutator methods like AddOrder() or RemoveOrder()?” These are valid questions and NHibernate can handle them just fine. I’d like to demonstrate the simple case first, before I get into some more complicated aspects of mapping such as mapping private fields, and so on. Mapping a One-to-Many Collection Since the SalesOrderHeader table has no implied ordering (i.e., a LineItemNumber column), you can safely map the relationship using a <bag> or <set>. Bags are a little easier to get started with because they don’t involve having to mess with the ISet assembly reference hassle mentioned above. Ultimately, I recommend you seriously consider using ISet for most one-to-many situations like this because it has the semantics most situations would expect. There are some downsides to using a <bag> that may trip you up later. However, in an effort to get started quickly, let’s map the “SalesOrder” property as a bag. You can change it later if you start running into problems. You need three things to map a collection like this: - The collection type (bag, set, map, list, etc.).
- The key column on the child table that points back to this entity’s ID (i.e., the CustomerID column in the SalesOrderHeader table).
- The name of the child class that will be contained in the collection (i.e., SalesOrderHeader).
The one-to-many mapping from Customer to SalesOrderHeader will end up looking like this: <class name="Customer"> <id name="CustomerID"> <generator class="native" /> </id>
<!-- <property> tags here -->
<bag name="SalesOrders" inverse="true"> <key column="CustomerID"/> <one-to-many class="SalesOrderHeader"/> </bag> </class>
A Quick Note About Bi-directional Associations You may have noticed that I slipped in an inverse=true attribute into that mapping. This is because you’ve now mapped SalesOrderHeader to Customer as well as Customer to SalesOrderHeader. You’ve mapped the relationship both ways (bi-directional) and, without the “inverse” attribute, haven’t indicated to NHibernate that these two relationships are, in fact, the same one. Perhaps in a future version NHibernate may grow smart enough to determine this automatically, but for now the problem can get complicated when more relationships are involved, so you have to give NHibernate some hints. Without the “inverse” attribute, NHibernate will view these mappings as two, distinct relationships and treat them as such. Thus, when adding a new SalesOrderHeader to an existing Customer, NHibernate would likely INSERT the SalesOrderHeader and then issue a separate UPDATE statement to update its CustomerID field, thus relating it to the Customer. This is excessive because NHibernate actually has all the information it needs to issue one INSERT statement with everything set up properly. In order to help NHibernate understand your intention, you must tell it that one side of the relationship is the “primary” one. For an OTM/MTO relationship, the OTM side (Customer to SalesOrderHeader) should be the “inverse” and the MTO side (SalesOrderHeader to Customer) should be the primary. Saving and Updating OTM Collections Now that I have the bi-directional relationship wired up correctly, I can talk about how to properly relate the objects together through code. Again, imagine what would be required to create this association purely in memory as if there were no database to worry about it. Likely, that code would look like: var customer = createNewCustomer();
for (var i = 0; i < 5; i++) { var order = createNewOrder("PO" + i); order.Customer = customer; customer.SalesOrders.Add(order); }
Not surprisingly, except for the addition of a call or two to ISession.Save(), this is exactly the code that will create the proper structure for NHibernate to persist the relationship properly to the database. This goes back to some of my original points about a good O/RM not being too intrusive into how the object-oriented code works and functions while also not imposing unnecessary requirements on the relational database side. By default, NHibernate will not do any cascading of operations (such as deleting a related entity when you remove it from the collection, etc.). NHibernate does, however, have very powerful and configurable cascade and life-cycle management features. I’ll go into more depth on these later. Let’s take a look at what the above code looks like after I add the NHibernate calls into it: var customer = createNewCustomer(); session.Save(customer);
for (var i = 0; i < 5; i++) { var order = createNewOrder("PO" + i); order.Customer = customer; customer.SalesOrders.Add(order); session.Save(order); }
xaction.Commit();
Notice that the only difference is the two calls to session.Save(). After I cover the cascading options, I’ll show you how to get rid of one of those save calls. Retrieving the Collection By default, when you get a Customer from the database, the SalesOrders list will be an empty proxy for lazy loading purposes. When you attempt to enumerate over the collection (call foreach() or pass the list to some method that will end up looping over the list), the proxy will trigger a lazy load and then load all the related SalesOrderHeader objects. As I mentioned before, you can configure this behavior. This is only the default. var customer = session.Get<Customer>(customerId); foreach( var order in customer.SalesOrders ) { Console.WriteLine(order.PurchaseOrderNumber); }
| & | | 
By: Chad Myers
Chad Myers is the Director of Development at Dovetail Software, Inc. in Austin, TX. He has over 10 years of software development experience creating elegant, functional, and durable Web-based enterprise software systems in Java and .NET/C#. Chad spends most of his professional time practicing Agile development techniques as well as object-oriented principles. He tries to spread as much knowledge has he has received by giving user group and conference talks, writing articles, and maintaining an active blog at http://chadmyers.lostechies.com.
chad@activewin.com |