NHibernate - Getting up and running

Configuring Child Collections with NHibernate

Index

Introduction to NHibernate
Simple Queries on a single table with NHibernate
Configuring Child Collections with NHibernate
Overriding Equals, GetHashCode and Database Design with NHibernate
Querying Collections Efficiently with NHibernate
Persisting objects back to the database with NHibernate

Introduction

There's a lot to understand when trying to configure child collections. NHibernate (as always) gives you many ways of setting them up. The most common implementations seem to be bags and sets. This article will go through those options in detail, but for those who just need to be up and running as quickly as possible, the recommended configuration is described below.

Recommended configuration

The documentation states

Sets are expected to be the most common kind of collection in NHibernate applications. This is because the "set" semantics are most natural in the relational model.

and

in well-designed NHibernate domain models, we usually see that most collections are in fact one-to-many associations with inverse="true". For these associations, the update is handled by the many-to-one end of the association.

Given this, and the advantages of bi-directional association (described below), the recommended configuration is...

Parent Object

  public virtual ICollection<Child> Children { get; set; }

  public Parent() {
      Children = new List<Child>();
  }

  public virtual void AddChild(Child child) {
      child.Parent = this;
      Children.Add(child);
  }

  public virtual void RemoveChild(Child child) {
      child.Parent = null;
      Children.Remove(child);
  }

Parent Mapping

  <set name="Children" inverse="true" cascade="all-delete-orphan">
    <key column ="PrimaryKey" />
    <one-to-many class ="ChildClass" />
  </set>

Child Object

public virtual Parent Parent { get; set; }

Child Mapping

<many-to-one name="Parent" column="ForeignKey" class="ParentClass" />

Bag, sets and their recommended implementation

Bags

The documentation states A bag is an unordered, unindexed collection which may contain the same element multiple times. Bags can map to the IList<T> interface

Sets

A set is very similar to a bag, except it can only store unique objects (duplicates will throw a NonUniqueObjectException). Sets map to the the ICollection<T> interface.

There are some interesting differences in the data retrieval and persistence depending on whether a bag or set is used, which is discussed in the unit tests below.

Bi directional association

The recommended implementation for the child collection mapping is using the bi-directional association pattern. This pattern allows navigation down...

Bidirectional association from parent object

and up through the object graph.

Bidirectional association from child collection

Coding the object model

The child collection is going to be the Orders table in the Northwind database. The important point to note on the Order class is the Employee property which is an Employee class, rather than the EmployeeId integer field which is in the table.

namespace ORM.Nhibernate.Model {
    public class Order {
        public virtual int OrderId { get; set; }
        public virtual Employee Employee { get; set; }
        public virtual string CustomerID { get; set; }
        public virtual DateTime OrderDate { get; set; }
        public virtual DateTime RequiredDate { get; set; }
        public virtual DateTime? ShippedDate { get; set; }
        public virtual Shipper ShipVia { get; set; }
        public virtual decimal Freight { get; set; }
        public virtual string ShipName { get; set; }
        public virtual string ShipCity { get; set; }
        public virtual string ShipAddress { get; set; }
        public virtual string ShipRegion { get; set; }
        public virtual string ShipPostalCode { get; set; }
        public virtual string ShipCountry { get; set; }
    }
}

The Employee object is extended to include an ICollection of Orders. Both sets and bags will map to an IList interface. An AddOrder method is also added, its primary purpose is to set the child Employee object to the Employee object instance.

namespace ORM.Nhibernate.Model {
    public class Employee {
        public virtual int EmployeeId {get; set; }
        public virtual string LastName {get; set;}
        public virtual string FirstName {get; set;} 
        public virtual string Title {get; set;} 
        public virtual string TitleOfCourtesy {get; set;} 
        public virtual DateTime BirthDate {get; set;} 
        public virtual DateTime HireDate {get; set;} 
        public virtual string Address {get; set;} 
        public virtual string City {get; set;} 
        public virtual string Region {get; set;} 
        public virtual string PostalCode {get; set;} 
        public virtual string Country {get; set;} 
        public virtual string HomePhone {get; set;} 
        public virtual string Extension {get; set;} 
        public virtual string Photo {get; set;} 
        public virtual string Notes {get; set;} 
        public virtual string ReportsTo {get; set;}
        public virtual string PhotoPath { get; set; }

        public virtual ICollection<Order> Orders { get; set; }

        public Employee() {
            Orders = new List();
        }

        public virtual void AddOrder(Order order) {
            order.Employee = this;
            Orders.Add(order);
        }
    }
}

Configuring the mapping files

The Employee mapping file required to support this object model looks like below. Note: to support bi-directional association, both the collections inverse and the parents cascade options must be set.

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping 
  xmlns="urn:nhibernate-mapping-2.2"
  assembly="ORM.Nhibernate"
  namespace="ORM.Nhibernate.Model">

  <class name="Employee" table="Employees">
    <id name="EmployeeId">
      <generator class="identity" />
    </id>
    
    <property name="LastName" />
    <property name="FirstName" />
    <property name="Title" />
    <property name="TitleOfCourtesy" />
    <property name="BirthDate" />
    <property name="HireDate" />
    <property name="Address" />
    <property name="City" />
    <property name="Region" />
    <property name="PostalCode" />
    <property name="Country" />
    <property name="HomePhone" />
    <property name="Extension" />
    <property name="Photo" />
    <property name="Notes" />
    <property name="ReportsTo" />
    <property name="PhotoPath" />

    <bag name="Orders" inverse="true" cascade="all-delete-orphan">
      <key column ="EmployeeId" />
      <one-to-many class ="Order" />
    </bag>
  </class>

</hibernate-mapping>

Inverse Attribute

The Inverse attribute is used to delegate the responsibility of saving the relationship to the database. Changes made only to the inverse end of the association are not persisted. e.g. if a collection it set to inverse="true", making a change to an item in the collection, then calling Session.Update<Child> will have no effect. The responsibility of the relationship has been delegated to the employee.

The documentation states..

If the <key> column of a <one-to-many> association is declared NOT NULL, NHibernate may cause constraint violations when it creates or updates the association. To prevent this problem, you must use a bidirectional association with the many valued end (the set or bag) marked as inverse="true".

and

Large NHibernate bags mapped with inverse="false" are inefficient and should be avoided

Having a NULL column on a foreign key, or an inefficient collection is never a good idea, so its recommended that inverse is always set.

Cascade attribute

Cascade enables DML operations to be cascaded to the child object. In this case all-delete-orphan cascades Save, Update, Delete and deletes the child row when its removed from the collection. For the bi-directional association pattern, this works in conjunction with the inverse="true" attribute on the bag.

Key Element

The Key element describes the foreign key in the child table

One-to-many Element

The one-to-many element describes the relationship (there's also many-to-many) and child class

For the Order mapping file...

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping 
  xmlns="urn:nhibernate-mapping-2.2"
  assembly="ORM.Nhibernate"
  namespace="ORM.Nhibernate.Model">

  <class name="Order" table="Orders">
    <id name="OrderId">
      <generator class="identity" />
    </id>

    <many-to-one name="Employee" column="EmployeeId" class="Employee" />
    <many-to-one name="ShipVia" column="ShipVia" class="Shipper" />

    <property name="CustomerID" />
    <property name="OrderDate" />
    <property name="RequiredDate" />
    <property name="ShippedDate" />
    <property name="Freight" />
    <property name="ShipName" />
    <property name="ShipAddress" />
    <property name="ShipCity" />
    <property name="ShipRegion" />
    <property name="ShipPostalCode" />
    <property name="ShipCountry" />
  </class>

</hibernate-mapping>

many-to-one Element

The many-to-one element describes the relationship to the parent, note the column and class attributes. The Shipper table has also been mapped and the Shipper class and mapping created.

Reading the Orders

Reading Andrew Fullers orders. Note from the profiler screenshot below, the orders are loaded outside the transaction. This is because the orders have be lazily loaded by the framework, the collection only being populated when its needed. This can be an efficient way of managing collections, but is also open to abuse, particularly with the infamous SELECT N + 1 anti-pattern. This will be covered in later chapters.

        [Fact]
        public void CanGetEmployeeOrders() {
            Employee e = null;

            using (var tx = Session.BeginTransaction()) {
                e = Session.QueryOver<Employee>()
                    .Where(x => x.LastName == "Fuller")
                    .SingleOrDefault();

                tx.Commit();
            }

            Assert.NotNull(e);
            Assert.NotEmpty(e.Orders);
            Assert.Equal(96, e.Orders.Count);
        }
Lazily loaded orders outside transaction

Inserting a Order

The test below shows inserting a new order, and returning the orderId which is automatically populated into the Order object. Note, with a bi-directional bag, a record can be inserted without having to fetch the entire collection first, this is because there is no unique key, the insert will always work!

        [Fact]
        public void CanAddAndDeleteOrder() {
            Employee employee = null;
            Order order = null;

            // Adding the Order
            Assert.DoesNotThrow(() => {
                using (var tx = Session.BeginTransaction()) {
                    employee = Session.Get<Employee>(4);
                    order = new Order {
                        CustomerID = "CHOPS",
                        OrderDate = DateTime.Now,
                        RequiredDate = DateTime.Now.AddDays(7),
                        ShipVia = Session.Load(2),
                        ShippedDate = null,
                        ShipName = "",
                        ShipAddress = "",
                        ShipCity = "",
                        ShipRegion = "",
                        ShipPostalCode = "",
                        ShipCountry = ""
                    };

                    employee.AddOrder(order);
                    Session.Update(employee);
                    tx.Commit();
                }
            });

            // Deleting it...
            Assert.DoesNotThrow(() => {
                using (var tx = Session.BeginTransaction()) {
                    employee.RemoveOrder(order);
                    Session.Update(employee);
                    tx.Commit();
                }
            });

        }
Profiler showing Bag inserting child records

Deleting an Order

The second part of the test deletes the newly created order. This is split into 2 transactions, as NHibernate will write to the database on a commit. There is a very interesting point to the profiler screenshot below

Before deleting the order, all orders are selected from the database

As the collection is a bag, there is no mapped index column or primary key (for the orders). NHibernate has no way to distinguish between rows, so it selects them all.... this is very inefficient

Profiler showing addtional select when deleting from a Bag collection

Running the Tests (Sets)

By altering the Employee mapping file to use a set and re-running the tests, we can see how NHibernate treats the collections differently. While the read test is identical, there insert has a gotcha...

Inserting an Order

Unlike bags, when inserting a child record into a set, the entire collection has to be loaded first so NHibernate can ensure there's no duplicates!

Profiler showing Inserting into a Set

Deleting an Order

Deleting an Order, again is opposite to that of a bag, as there are no duplicates, the order can be deleted without having to retrieve the entire collection first

Attempting to insert a duplicate

Attempting to insert a duplicate into a set will throw a NonUniqueObjectException

        [Fact]
        public void InsertingDuplicateIntoSetThrows() {
            Employee e = null;

            Assert.Throws <NHibernate.NonUniqueObjectException>(() => {
                using (var tx = Session.BeginTransaction()) {

                    e = Session.QueryOver<Employee>()
                        .Where(x => x.LastName == "Peacock")
                        .SingleOrDefault();

                    e.AddOrder(new Order {
                        OrderId = 11029,        // This Id already exists
                        CustomerId = "CHOPS",
                        OrderDate = DateTime.MinValue,
                        RequiredDate = DateTime.MinValue,
                        ShippedDate = DateTime.MinValue,
                        ShipVia = Session.Load(1),
                        Freight = 0m,
                        ShipName = string.Empty,
                        ShipAddress = string.Empty,
                        ShipRegion = string.Empty,
                        ShipPostalCode = string.Empty,
                        ShipCountry = string.Empty,
                    });

                    Session.Update(e);
                    tx.Commit();
                }
            });
        }

Summary

The key points to take away from the above are

  • Sets are the recommended collection
  • Bi-directional association is the recommended pattern
  • Bags are very efficient at inserting children, but inefficient at deleting them
  • Sets are very inefficient at inserting children, but efficient at deleting them

Given the above, choosing a Bag or Set will be dictated by your data flow into the table, e.g. a bag is an obvious choice for a high volume of inserts.

Source Code

git clone https://github.com/mindfulsoftware/ORM.Nhibernate.git
(version: bd371c06fdd5a3d1428708c38dee9fe528d5de34)