Mapping a one-to-many relationship

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.

Getting ready

Complete the previous Creating class hierarchy mappings recipe.

How to do it…

  1. In 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; }
    
      }
    }
  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>
  5. Create a new folder named OneToMany in the MappingRecipes project.
  6. Add a new class named Recipe to the folder:
    public class Recipe : BaseMappingRecipe
    {
        protected override void Configure(Configuration cfg)
        {
            cfg.AddAssembly(typeof(Product).Assembly);
        }
    }
  7. Add a new method to the 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"
                }
            } 
        });
    }
  8. Start the application and run the OneToMany recipe. The query log will show something similar to this:
    How to do it…

How it works…

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:

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 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.

There's more…

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.

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 that 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; 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'
    
  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 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.
  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 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.

Lazy loading proxies

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 class
  • Movie must have a protected or public constructor without parameters
  • All public members of Movie must be virtual; this includes methods

Why 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.

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

Property type

IList<T>

ISet<T>

IList<T>

IDictionary<TKey,TValue>

Suggested backing type

List<T>

HashSet<T>

List<T>

Dictionary<TKey,TValue>

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.

Bags

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>

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 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.

Sets

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

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>

See also

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

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