NHibernate Pitfallls: Composite Keys

This is part of a series of posts about NHibernate Pitfalls. See the entire collection here.

At times, there may be a need for entities with composite identifiers, that is, entities that map to tables which have composite primary keys, composed of many columns. The columns that make up this primary key are usually foreign keys to another tables.

NHibernate fully supports this scenario, but you should make sure that you still define a single-property identifier for the entity. Some of NHibernate’s functionalities require so, although it is not strictly necessary if you don’t use them. These include loading by id (methods ISession.Get<T>() and ISession.Load<T>() and detaching an entity from a session (method ISession.Evict()). In particular, Evict() will fail saying that the session does not contain the entity, even though ISession.Contains() tells you so.

In order to have a single-property identifier, you should start by defining a new class, a component, not an entity, which may be defined inside your entity:

   1: [Serializable]
   2: public class UserProduct
   3: {
   4:     [Serializable]
   5:     public class UserProductId
   6:     {
   7:         public User User { get; set; }
   8:         public Product Product { get; set; }
   9:  
  10:         public override Boolean Equals(Object obj)
  11:         {
  12:             if (obj as UserProduct == null)
  13:             {
  14:                 return(false);
  15:             }
  16:  
  17:             if (Object.ReferenceEquals(this, obj) == true)
  18:             {
  19:                 return(true);
  20:             }
  21:  
  22:             UserProductId other = obj as UserProductId;
  23:  
  24:             if (Object.Equals(this.User, other.User) == false)
  25:             {
  26:                 return(false);
  27:             ]            
  28:  
  29:             if (Object.Equals(this.Product, other.Product) == false)
  30:             {
  31:                 return(false);
  32:             }
  33:  
  34:             return(true);
  35:         }
  36:  
  37:         public override Int32 GetHashCode()
  38:         {
  39:             Int32 hash = 0;
  40:  
  41:             hash += (this.User != null) ? this.User.GetHashCode() : 0;
  42:             hash += 1000 * (this.Product != null) ? this.Product.GetHashCode() : 0;
  43:  
  44:             return(hash);
  45:         }
  46:     }
  47:  
  48:     public UserProduct()
  49:     {
  50:         this.Id = new UserProductId();
  51:     }
  52:  
  53:     public UserProductId Id { get; set; }
  54:  
  55:     //...
  56: }
  57:     

And then define the mapping for the entity (the new class does not need one):

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <hibernate-mapping default-lazy="false" namespace="Domain" assembly="Domain" xmlns="urn:nhibernate-mapping-2.2">
   3:     <class name="UserProduct" lazy="false" table="`USER_PRODUCT`">
   4:         <composite-id name="Id">
   5:             <key-many-to-one name="User" column="`USER_ID`" />
   6:             <key-many-to-one name="Product" column="`PRODUCT_ID`" />
   7:         </composite-id>
   8:         <!-- ... -->
   9:     </class>
  10: </hibernate>

Bookmark and Share

                             

2 Comments

  • Please, this code hurts my eyes!

    First of all, your Equals implementation is way to large! Are you a VB programmer or something?

    (Object.Equals(this.Product, other.Product) == false)

    is the same as:

    (Product!=other.Product)


    Second, GetHashCode should NOT change after the object has been created because it is part of the primary key.

    Third, your GetHashCode implementation is pretty weird.


    return User.GetHashCode() ^ Product.GetHashCode();

    Fourth, your ID class should have protected setters.

    Fifth, your parameterless constructors should be protected and have constructors that accept the compositekey values.

    public class UserProduct
    {
    public UserProductId { get; protected set; }
    protected UserProduct(){}
    public UserProduct(User user, Product product){
    Id = new UserProductId(user,product);
    }
    /....
    }


    Hope this helps before huge problems occur due to non static GetHashCode values.

  • @Ramon:
    Thanks for your comment. Let me try to say something about it.
    First: a matter of taste, they are exactly the same, UNLESS you override the == operator for your entities; Object.Equals is always safer;
    Second: the discussion of how to implement GetHashCode is an old one, and I am well aware of Microsoft's recommendations; the reason I don't implement GetHashCode solely on the primary key property is because I use stateless sessions and so I sometimes have to write code like this: new Entity{ Id = 100 };
    Third: your implementation throws an exception if the properties are null;
    Fourth: the reason I don't have protected setters in primary key properties is because of stateless sessions;
    Fifth: OK. That's the only one that's OK, by the way.
    Thanks.
    RP

Comments have been disabled for this content.