It's usually necessary to relate an entity to other entities. For example, an order can be related to many order lines and to a customer. In this example, we'll show you how to map a one-to-many relationship between Movie
and a new entity class, ActorRole
.
Eg.Core
, create a new class named 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>
OneToMany
in the MappingRecipes
project.Recipe
to the folder:public class Recipe : BaseMappingRecipe { protected override void Configure(Configuration cfg) { cfg.AddAssembly(typeof(Product).Assembly); } }
Recipe
class:protected override void AddInitialData(ISession session) { session.Save(new Movie { Name = "Hibernation", Description = "The countdown for the lift-off has begun", UnitPrice = 300, Actors=new List<ActorRole> { new ActorRole { Actor = "Adam Quintero", Role = "Joseph Wood" } } }); }
OneToMany
recipe. The query log will show something similar to this:Our ActorRole
mapping is simple. Check out mapping a class with XML for more information. ActorRole
isn't a 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 features such as lazy loading, as 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 the order is significant. After all, 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 ActorRole
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.
A few items in this recipe are worth discussing. Note how the relationship isn't inserted at once. Instead an UPDATE
at the end sets up the relationship. This is due to our unidirectional relationship. Only the Movie
knows about its ActorRole
and not the other way around. But, in order to persist the relationship, the ActorRole
will first be saved.
This behavior is best circumvented by creating a bi-directional relationship (discussed later in this chapter), where the ActorRole
is taking ownership of the relation. However, it is also possible to force NHibernate to insert to correct relationship keys at once, by simply adding not-null="true"
to the <key>
element:
<list name="Actors" cascade="all-delete-orphan" > <key column="MovieId" not-null="true"/> <index column="ActorIndex" /> <one-to-many class="ActorRole"/> </list>
The purpose of this is to avoid NULL
values in the MovieId
columns. It doesn't stop NHibernate from issuing the extra UPDATE
. An added update="false"
will do that, but with unfortunate side effects, especially when using <list>
collections.
Lesson learned? Read up on bi-directional relationships.
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 that 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; note 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 assigns it to 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 lazy="false"
attribute to the <list>
element of our mapping.
It's worth noting that lazy loading always uses the same NHibernate session as the one used to load the parent object. In other words, the session must stay open or the lazy loading will fail.
Suppose our ActorRole
class had a reference back to Movie
, similar to 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 will build the ActorRole
object as we 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 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 similar to a real Movie
entity.
This proxy object is a subclass of Movie
. NHibernate requires a few things from our Movie
class to subclass Movie
and intercept these calls to trigger lazy loading; they are:
Movie
cannot be a sealed classMovie
must have a protected or public constructor without parametersMovie
must be virtual; this includes methodsWhy all members and not just the lazy properties? It is because NHibernate wants to ensure that the instance is fully initialized, as soon as something interacts with it. The proxy will need to override all the members and they have to be virtual for that.
By default, NHibernate creates these proxy objects using the DefaultProxyFactory
. As the name implies, this factory can be replaced if you require special proxy functionalities, such as logging or integration with an IoC container.
If we specify lazy="false"
on the class element of our Movie
mapping, we can disable this behavior and NHibernate will never create a proxy of Movie
. Instead, it forces NHibernate to load the associated movie's data as soon as it loads an ActorRole
. Loading data unnecessarily like this can kill the performance of your application, and it should only be used in very specific and well-considered circumstances.
NHibernate supports several collection types. The most common types are as follows:
Bag |
Set |
List |
Map | |
---|---|---|---|---|
Allows Duplicates |
Yes |
No |
Yes |
Keys must be unique. Values may be duplicated. |
Order is significant |
No |
No |
Yes |
No |
Property type |
|
|
|
|
Suggested backing type |
|
|
|
|
All collections can use the ICollection
type or a custom collection type implementing NHibernate.UserType.IUserCollectionType
. Only bag and set can be used in bidirectional relationships. We will explore those in the Bidirectional class relationships recipe.
A bag collection allows duplicates and implies that order is not important. Let's consider 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
, ICollection
or even an IEnumerable
.
In a one-to-many scenario, this behavior with duplicates is usually of limited consequence. Since ActorRole
is an entity with an Id, duplicates can and will not be inserted, even if they have been added to the collection property. This would cause primary key violations in the database. However, in a many-to-many relationship, which we will discuss in the next recipe, the duplicates become a real issue (or opportunity). There is no way to identify an individual entry in the bag distinctly with a SQL statement. When an entry is removed, and the updated bag is persisted, all the rows representing the old bag contents are deleted, using a SQL statement, such as delete from MovieActors where MovieId='1'
. After this, the entire contents of the bag are reinserted. For 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 address each bag entry uniquely with queries, such as:
delete from Actors where ActorRoleBagId='2'
The mapping for an idBag
looks similar to 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 again at index 3, actor role 4 at index 4, and actor role 2 again at index 5. A typical list mapping looks similar to 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 will then have to update the ActorRoleIndex
of each list entry after the second. For large lists, this can cause real performance issues.
A set collection does not allow duplicates and the order of a set is not important. It may contain actor role 1, actor role 3, actor role 2, and actor role 4 but 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 System.Collections.Generic
.
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 Setting up a base entity class recipe.
While it is very common to use IList<T>
in .NET applications for any type of collection, an ISet<T>
is actually the option that often represents the actual intent. No duplicates.
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 with a corresponding value. The keys must be unique, whereas the values can be valid instances of the value type:
<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>
18.221.249.198