Sorry, have been quiet for some time due to too much project work... But now lets discuss a fairly important and often misunderstood topic: the mapping of collections in NHibernate
Monitoring the NHUsers Google group I can see a lot of questions around collections and how to best map them in NHibernate. NHibernate offers several possibilities to map a collection of objects. You can use a Set, a Map, a Bag, an IdBag or a List. Let's first analyze what the ("exact" mathematical) definition of each of them is.
Theoretical background
- A Set is a collection of distinct objects considered as a whole.
A valid example of a set (of letters) is: { a, b, c, d }. Each letter occurs exactly once. - A Bag is a generalization of a set. A member of a bag can have more than one membership while each member of a set has only one membership.
A valid example of a bag is { a, a, a, b, c, c, d, ...}. The letters a and c appear more than once in the Bag. - A Map (also called associative container, hash, dictionary, lookup table) is a abstract data type composed of a collection of keys and a collection of values, where each key is associated with one value. The relationship between a key and its value is sometimes called a mapping or binding.
A valid sample would be a map of capitals of the world's countries where the country code is the key and the name of the capital is the value, i.e. { { "CH", "Bern" }, { "D", "Berlin" }, { "USA", "Washington" }, ...} - A List is collection of objects (also called elements or members). Each element in the List has an index. Similar to a Bag a member of the the list can have more than one membership.
A valid sample of a list is e.g. { {1, "Bob"}, {2,"Sue"}, {3,"Ann"}, {4,"Sue"}, ...}. Obviously the name "Ann" occurs twice, once with the index 2 and once with the index 4.
.NET and the CLR
The CLR of .NET only provides us Maps and Lists. Maps are either implemented as Hashtables (non generic) or Dictionaries (generic). Lists are available as ArrayList (non generic) or List<T> (generic). But we have no direct representation of Set and Bag in the CLR. A Bag can easily be simulated by using a List. We just have the superfluous index. It is not obvious why Microsoft didn't implement a Set though. Since NHibernate makes heavy use of sets they provide an implementation of Sets in the so called IESI collection library which is part of the stack.
Samples
The code to this samples can be downloaded from here.
Collection of values
Let's take a simple sample. A customer provides several possibilities to contact him (e.g. Mobile, Office phone, EMail, FAX, etc.). We can represent this list of possible contacts as a Collection of strings.
Obviously we want each contact to be unique. Thus a Set would certainly be a good choice for our Contacts collection. The corresponding code is
using Iesi.Collections.Generic;
namespace CollectionMapping
{
public class Customer
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual ISet<string> Contacts { get; set; }
public Customer()
{
Contacts = new HashedSet<string>();
}
}
}
Note the import of the Iesi.Collections.Generic namespace which is implemented in the Iesi.Collections assembly which is part of the NHibernate stack. Note also that in the constructor we initialize the collection. A possible implementation for the ISet interface is the HashedSet<T> also found in the IESI collection library.
Let's have a look at the NHibernate mapping now
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="CollectionMapping"
namespace="CollectionMapping">
<class name="Customer">
<id name="Id">
<generator class="native"/>
</id>
<property name="Name"/>
<set name="Contacts">
<key column="CustomerId" foreign-key="fk_Contact_Customer"/>
<element column="Description" type="String"/>
</set>
</class>
</hibernate-mapping>
The schema generation script produced by the SchemaExport service of NHibernate is then (for SQL Server 2005)
create table Customer (Id INT IDENTITY NOT NULL, Name NVARCHAR(255) null, primary key (Id))
create table Contacts (CustomerId INT not null, Description NVARCHAR(255) null)
alter table Contacts add constraint fk_Contact_Customer foreign key (CustomerId) references Customer
(For a detailed discussion of Schema Generation see this post.)
Now we can write a unit test to verify we can indeed create customer objects with contacts
[Test]
public void Can_create_customer_with_contacts()
{
var customer = new Customer {Name = "John Doe"};
customer.Contacts.Add("Business phone: 123-12 34 56");
customer.Contacts.Add("Mobile: 555-72 44 55");
customer.Contacts.Add("Email: john.doe@somecompany.com");
Session.Save(customer);
Session.Flush();
Session.Clear();
// Assertions
var fromDb = Session.Get<Customer>(customer.Id);
Assert.AreNotSame(customer, fromDb);
Assert.AreEqual(customer.Name, fromDb.Name);
Assert.AreEqual(customer.Contacts.Count, fromDb.Contacts.Count);
}
which will produce the following sql commands (note: I'm using SQL Light here as my in memory test database. A detailed discussion about setting up an environment for TDD can be found in this post.)
NHibernate: INSERT INTO Customer (Name) VALUES (@p0); select SCOPE_IDENTITY();
@p0 = 'John Doe'
NHibernate: INSERT INTO Contacts (CustomerId, Description) VALUES (@p0, @p1);
@p0 = '1', @p1 = 'Business phone: 123-12 34 56'
NHibernate: INSERT INTO Contacts (CustomerId, Description) VALUES (@p0, @p1);
@p0 = '1', @p1 = 'Mobile: 555-72 44 55'
NHibernate: INSERT INTO Contacts (CustomerId, Description) VALUES (@p0, @p1);
@p0 = '1', @p1 = 'Email: john.doe@somecompany.com'
NHibernate: SELECT customer0_.Id as Id0_0_, customer0_.Name as Name0_0_
FROM Customer customer0_ WHERE customer0_.Id=@p0; @p0 = '1'
NHibernate: SELECT contacts0_.CustomerId as CustomerId0_,
contacts0_.Description as Descript2_0_
FROM Contacts contacts0_
WHERE contacts0_.CustomerId=@p0; @p0 = '1'
So far so good. We have a solution where each customer object has a collection of unique contacts. The contacts are just a collection of strings. Now maybe we want a contact to be a complex object with several properties instead of only a simple string.
Collection of (complex) objects
Let's assume our contact should contain a type and a description property. And let's call the collection of contacts "BusinessContacts".
We can define our customer and contact classes as follows
using Iesi.Collections.Generic;
namespace CollectionMapping
{
public class Customer
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual ISet<Contact> BusinessContacts { get; set; }
public Customer()
{
BusinessContacts = new HashedSet<Contact>();
}
}
public class Contact
{
public virtual ContactTypes Type { get; set; }
public virtual string Description { get; set; }
}
public enum ContactTypes
{
Undefined=0,
Email,
Phone,
Mobile,
Fax
}
}
Note that the Contacts are still treated as value objects (in a DDD sense) and not entities. Thus a contact does not have an ID.
Equality and uniqueness
Now we are sure that each distinct contact can only be added to the Contacts collection once. So trying to (accidentally) add the work phone twice would result in a exception. But wait a moment! How does the Set know that two instances of type Contact are the same? Now, what does equality mean? The (online) help of Microsoft tells us that:
"In C#, there are two different kinds of equality: reference equality and value equality. Value equality is the generally understood meaning of equality: it means that two objects contain the same values. For example, two integers with the value of 2 have value equality. Reference equality means that there are not two objects to compare. Instead, there are two object references and both of them refer to the same object."
And then they add:
"Because Equals is a virtual method, any class can override its implementation. Any class that represents a value, essentially any value type, or a set of values as a group, such as a complex number class, should override Equals."
Well, internally the Set uses value equality (that is the Equals function) to differentiate objects. In our case we want to say that two (different) instances of type Contact are equal if their property Description and Type contain the same values. Thus we have to override the Equals function and provide our own implementation. But when overriding the Equals function one also has to override the GetHashCode function at the same time!
Since the type System.String and System.Int32 (an enum is by default a System.Int32) already provide an Equals and a GetHashCode implementation we take this one and thus the code to add to our Contacts class is fairly simple
public override bool Equals(object obj)
{
if(obj == null || !(obj is Contact)) return false;
var contact = (Contact)obj;
return ((Description == null && contact.Description == null) ||
Description.Equals(contact.Description)) && Type.Equals(contact.Type);
}
public override int GetHashCode()
{
return string.Format("{0}|{1}", Type, Description).GetHashCode();
}
Note that to get the hash code we first convert the two properties into a unique string and then take it's default GetHashCode implementation. This might not be the most efficient solution but it certainly gives me the correct results.
Having done so we can be sure that any distinct contact is never added more than once to the Contacts collection of the Customer object.
Now let's have a look at the mapping of the classes
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="CollectionMapping"
namespace="CollectionMapping">
<class name="Customer">
<id name="Id">
<generator class="native"/>
</id>
<property name="Name"/>
<set name="BusinessContacts" table="BusinessContact">
<key column="CustomerId" foreign-key="fk_BusinessContact_Customer"/>
<composite-element class="Contact">
<property name="Type" not-null="true"/>
<property name="Description" not-null="true"/>
</composite-element>
</set>
</class>
</hibernate-mapping>
Now we have a <composite-element> instead of just an <element> in the mapping file. The <composite-element> can contain as many <property> child nodes as needed.
The schema generation script produced by the SchemaExport service of NHibernate is then (for SQL Server 2005)
create table Customer (Id INT IDENTITY NOT NULL, Name NVARCHAR(255) null, primary key (Id))
create table BusinessContact (CustomerId INT not null, Type INT null, Description NVARCHAR(255) null)
alter table BusinessContact add constraint fk_BusinessContact_Customer foreign key (CustomerId) references Customer
Again we can write a unit test to verify we can indeed create customer objects with business contacts
[Test]
public void Can_create_customer_with_business_contacts()
{
var customer = new Customer {Name = "John Doe"};
customer.BusinessContacts.Add(new Contact
{
Type = ContactTypes.Phone,
Description = "123-12 34 56"
});
customer.BusinessContacts.Add(new Contact
{
Type = ContactTypes.Mobile,
Description = "555-72 44 55"
});
customer.BusinessContacts.Add(new Contact
{
Type = ContactTypes.Email,
Description = "john.doe@somecompany.com"
});
Session.Save(customer);
Session.Flush();
Session.Clear();
// Assertions
var fromDb = Session.Get<Customer>(customer.Id);
Assert.AreNotSame(customer, fromDb);
Assert.AreEqual(customer.Name, fromDb.Name);
Assert.AreEqual(customer.BusinessContacts.Count, fromDb.BusinessContacts.Count);
}
and NHibernate will produce the following output (for an SQL light database)
NHibernate: INSERT INTO Customer (Name) VALUES (@p0); select SCOPE_IDENTITY(); @p0 = 'John Doe'
NHibernate: INSERT INTO BusinessContact (CustomerId, Type, Description) VALUES (@p0, @p1, @p2); @p0 = '1', @p1 = 'Phone', @p2 = '123-12 34 56'
NHibernate: INSERT INTO BusinessContact (CustomerId, Type, Description) VALUES (@p0, @p1, @p2); @p0 = '1', @p1 = 'Mobile', @p2 = '555-72 44 55'
NHibernate: INSERT INTO BusinessContact (CustomerId, Type, Description) VALUES (@p0, @p1, @p2); @p0 = '1', @p1 = 'Email', @p2 = 'john.doe@somecompany.com'
NHibernate: SELECT customer0_.Id as Id0_0_, customer0_.Name as Name0_0_
FROM Customer customer0_ WHERE customer0_.Id=@p0; @p0 = '1'
NHibernate: SELECT businessco0_.CustomerId as CustomerId0_, businessco0_.Type as Type0_, businessco0_.Description as Descript3_0_
FROM BusinessContact businessco0_ WHERE businessco0_.CustomerId=@p0; @p0 = '1'
That's it for the moment. In my next post I'll discuss the Bag and the Map type of collections.
You can find the code for these samples here.
Enjoy!
.