It's usually necessary to relate one entity to another. In this example, I'll show you how to map a one-to-many relationship between Movies
and a new entity class, ActorRoles
.
ActorRole
with the following code:namespace Eg.Core { public class ActorRole : Entity { public virtual string Actor { get; set; } public virtual string Role { get; set; } } }
ActorRole
with the following XML:<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Eg.Core" namespace="Eg.Core"> <class name="ActorRole"> <id name="Id"> <generator class="guid.comb" /> </id> <property name="Actor" not-null="true" /> <property name="Role" not-null="true" /> </class> </hibernate-mapping>
Actors
property to the Movie
class:using System.Collections.Generic; namespace Eg.Core { public class Movie : Product { public virtual string Director { get; set; } public virtual IList<ActorRole> Actors { get; set; } } }
list
element to our Movie
mapping:<subclass name="Movie" extends="Product"> <property name="Director" /> <list name="Actors" cascade="all-delete-orphan"> <key column="MovieId" /> <index column="ActorIndex" /> <one-to-many class="ActorRole"/> </list> </subclass>
Our ActorRole
mapping is simple. Check out Mapping a class with XML for more information. ActorRole
isn't part of our Product
hierarchy. In the database, it gets a table of its own, as shown in the next screenshot:
As expected, the ActorRole table has fields for the Id, Actor, and Role properties. The MovieId and ActorIndex columns come from the mapping of our Actors
list on Movie
, not the ActorRole
mapping.
The Actors
property uses an IList
collection. Another strong design choice with NHibernate, and a good programming practice in general, is the liberal use of interfaces. This allows NHibernate to use its own list implementation to support lazy loading, discussed later in this recipe.
In our Movie
mapping, the Actors
property is mapped with the list
element. To associate an ActorRole
with a Movie
in the database, we store the Movie's Id
with each ActorRole
. The key
element tells NHibernate to store this in a column named MovieId.
We've defined Actors
as a list, which implies that order is significant. Actors in leading roles get top billing. Our index
element defines the ActorIndex column to store the position of each element in the list. Finally, we tell NHibernate that Actors
is a collection of ActorRoles
with <one-to-many class="ActorRole" />
.
The all-delete-orphan
value of the cascade
attribute tells NHibernate to save the associated ActorRole
objects automatically when it saves a Movie
, and delete them when it deletes a Movie
.
There are a few items to discuss with this recipe.
To improve application performance, NHibernate supports lazy loading. In short, data isn't loaded from the database until it is required by the application. Let's look at the steps NHibernate will use when our application fetches a movie from the database:
Id
, Name
, Description
, UnitPrice
, and Director
data from the database for a Movie
with a given Id
. Notice that we do not load the Actors
data. NHibernate uses the following SQL query:select movie0_.Id as Id1_, movie0_.Name as Name1_, movie0_.Description as Descript4_1_, movie0_.UnitPrice as UnitPrice1_, movie0_.Director as Director1_ from Product movie0_ where movie0_.ProductType='Eg.Core.Movie' and movie0_.Id = 'a2c42861-9ff0-4546-85c1-9db700d6175e'
Movie
object.Id
, Name
, Description
, UnitPrice
, and Director
properties of the Movie
object with the data from the database.IList<ActorRole>
, and sets the Actors
property of the Movie
object. It is not a List<ActorRoles>
, but rather a separate, NHibernate-specific implementation of the IList<ActorRole>
interface.Movie
object to our application.Then, suppose our application contains the following code. Remember, we haven't loaded any ActorRole
data.
foreach (var actor in movie.Actors) Console.WriteLine(actor.Actor);
The first time we enumerate the collection, the lazy loading object is initialized. It loads the associated ActorRole
data from the database with a query as shown:
SELECT actors0_.MovieId as MovieId1_, actors0_.Id as Id1_, actors0_.ActorIndex as ActorIndex1_, actors0_.Id as Id0_0_, actors0_.Actor as Actor0_0_, actors0_.Role as Role0_0_ FROM ActorRole actors0_ WHERE actors0_.MovieId= 'a2c42861-9ff0-4546-85c1-9db700d6175e'
We can disable lazy loading of a collection by adding the attribute lazy="false"
to the list
element of our mapping.
In other circumstances, NHibernate also supports lazy loading through the use of proxy objects. Suppose our ActorRole
class had a reference back to Movie
, like the following code:
public class ActorRole : Entity { public virtual string Actor { get; set; } public virtual string Role { get; set; } public virtual Movie Movie { get; set; } }
If we fetch an ActorRole
from the database, NHibernate builds the ActorRole
object as we would expect, but it only knows the Id
of the associated Movie
. It won't have all the data necessary to construct the entire Movie
object. Instead, it will create a proxy object to represent the Movie
and enable lazy loading.
We can, of course, access the Id
of this Movie
proxy without loading the movie's data. If we access any other property or method on the proxy, NHibernate will immediately fetch all the data for this movie. Loading this data is completely transparent to the application. The proxy object behaves exactly like a real Movie
entity.
This proxy object is a subclass of Movie
. In order to subclass Movie
and intercept these calls to trigger lazy loading, NHibernate requires a few things from our Movie
class.
Movie
cannot be a sealed class.Movie
must have a protected or public constructor without parameters.Movie
must be virtual. This includes methods.NHibernate gives us several choices for the creation of these proxy objects. The traditional choice of NHibernate proxy framework is DynamicProxy, part of the Castle stack of projects. Additionally, NHibernate includes support for LinFu and Spring.NET, and allows you to build your own.
If we specify lazy="false"
on the class element of our Movie
mapping, we can disable this behavior. NHibernate will never create a proxy of Movie
. This will force NHibernate to immediately load the associated movie's data any time it loads an ActorRole
. Loading data unnecessarily like this can quickly kill the performance of your application, and should only be used in very specific, well-considered circumstances.
NHibernate supports several collection types. The most common types are as follows:
Bag | ||||
---|---|---|---|---|
Allows Duplicates |
Yes |
No |
Yes |
Keys must be unique. Values may be duplicated. |
Order is significant |
No |
No |
Yes |
No |
Type |
|
|
|
|
All collections may also use the ICollection
type, or a custom collection type implementing NHibernate.UserType.IUserCollectionType
. Only bag
and set
may be used in bidirectional relationships.
A bag collection allows duplicates, and implies that order is not important. Let's talk about a bag of ActorRole
entities. The bag may contain actor role 1, actor role 2, actor role 3, actor role 1, actor role 4, and actor role 1. A typical bag mapping appears as shown in the following code:
<bag name="Actors"> <key column="MovieId"/> <one-to-many class="ActorRole"/> </bag>
The corresponding Actors
property may be an IList
or ICollection
, or even an IEnumerable
.
There is no way to identify an individual entry in the bag distinctly with a SQL statement. For example, there is no way to construct a SQL statement to delete just the second entry of actor role 1 from the bag. The SQL statement delete from Actors where ActorRoleId='1'
will delete all three entries. When an entry is removed, and the updated bag is persisted, the rows representing the old bag contents are deleted, and then entire bag contents are reinserted. For especially large bags, this can create performance issues.
To counter this issue, NHibernate also provides an idBag
where each entry in the bag is assigned an ID by one of the POID generators. This allows NHibernate to uniquely address each bag entry with queries like delete from Actors where ActorRoleBagId='2'
.
The mapping for an idBag
looks like the following code:
<idBag name="Actors"> <collection-id column="ActorRoleBagId" type="Int64"> <generator class="hilo" /> </collection-id> <key column="MovieId"/> <one-to-many class="ActorRole"/> </idBag>
A list collection also allows duplicates, but unlike a bag, the order is significant. Our list may contain actor role 1 at index 0, actor role 2 at index 1, actor role 3 at index 2, actor role 1 at index 3, actor role 4 at index 4, and actor role 1 at index 5. A typical list mapping looks like the following code:
<list name="Actors"> <key column="MovieId" /> <list-index column="ActorRoleIndex" /> <one-to-many class="ActorRole"/> </list>
The corresponding Actors
property should be an IList
. Because NHibernate maintains order with the ActorRoleIndex
column, it can also uniquely identify individual list entries. However, because it maintains order, it also means that these indexes must be reset whenever the list contents change. For example, suppose we have a list of six actor roles and we remove the third actor role. NHibernate updates the ActorRoleIndex
of each list entry.
A set collection does not allow duplicates, and the order of a set is not important. In my applications, this is the most common collection type. A set may contain actor role 1, actor role 3, actor role 2, and actor role 4. An attempt to add actor role 1 to the set again will fail. A typical set mapping appears as shown in the following code:
<set name="Actors"> <key column="MovieId" /> <one-to-many class="ActorRole"/> </set>
The corresponding Actors
property should be an ISet
from Iesi.Collections.dll
. Currently, NHibernate does not directly support the ISet
interface included in the .NET Framework 4.
An attempt to add an item to an uninitialized lazy-loaded set collection will cause the set to be loaded from the database. This is necessary to ensure uniqueness in the collection. To ensure proper uniqueness in a set, you should override the Equals
and GetHashCode
methods, as shown in the next recipe.
Map is another term that crossed over when NHibernate was ported from Java. In .NET, it's known as a dictionary. Each collection entry is a key or value pair. Keys must be unique. Values may not be unique.
<map name="Actors" > <key column="MovieId" /> <map-key column="Role" type="string" /> <element column="Actor" type="string"/> </map>
As you may have guessed, the corresponding Actors
property must be an IDictionary<string, string>
, where the key is the name of the movie role, and the value is the actor's name. You are not limited to basic data types as shown here. NHibernate also allows entities for keys and values as shown in the following code:
<map name="SomeProperty"> <key column="Id" /> <index-many-to-many class="KeyEntity"/> <many-to-many class="ValueEntity" /> </map>