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>