Mapping a one-to-many relationship

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.

Getting ready

Complete the previous Creating class hierarchy mappings example.

How to do it...

  1. 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; }
    
      }
    }
  2. Create an embedded resource mapping for 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>
  3. Add this 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; }
    
      }
    }
  4. Add the following 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>

How it works...

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:

How it works...

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's more...

There are a few items to discuss with this recipe.

Lazy loading collections

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:

  1. NHibernate fetches 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'
  2. NHibernate creates an instance of the Movie object.
  3. NHibernate sets the Id, Name, Description, UnitPrice, and Director properties of the Movie object with the data from the database.
  4. NHibernate creates a special lazy loading object that implements 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.
  5. NHibernate returns the 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.

Lazy loading proxies

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.
  • All public members of 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.

Collections

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

Type

IList

Iesi.Collections.ISet

IList

IDictionary

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.

Bags

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>

Lists

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.

Sets

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

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>

See also

  • Mapping a class with XML
  • Creating class hierarchy Mappings
  • Setting up a base entity class
  • Bidirectional one-to-many class relationships
  • Handling versioning and concurrency
  • Creating mappings fluently
  • Mapping with ConfORM
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.191.86