Myotragus Tupples - Automatizing NHibernate Composite Keys
Introduction
Primary Keys are very usually auto-incrementing integers or Guid.
Unfortunately, many
domains need much more than that. Primary keys are not defined as single column
but just as unique. Sooner rather than later you'll find your self in the need
for composite primary keys. When using composite keys, it is very useful being able
to apply equality functions to key values. Actually, NHibernate requires
composite keys to override equality members (Equals
and GetHashCode
). This article is about making the
process
of implementing equality functions painless.
Contents
The Model
Let's take a look at the following Domain.
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="Myotragus.Data.Tupples.Tests"
namespace="Myotragus.Data.Tupples.Tests.Domain">
<class name="Product">
<id name="Id" column="ProductId">
<generator class="identity"/>
</id>
<property name="Name"/>
</class>
<class name="Category">
<id name="Id" column="CategoryId">
<generator class="identity"/>
</id>
<property name="Name"/>
</class>
<class name="CategoryProducts">
<composite-id name="Key">
<key-property name="ProductId"/>
<key-property name="CategoryId"/>
</composite-id>
<property name="CustomDescription"/>
</class>
</hibernate-mapping>
In previous code a 3 entities Domain is defined. Product
and Category
need no explanation.
CategoryProducts
, in the other hand, does. If you are experienced with NHibernate,
you'd probably use a collection within both Product
and Category
to represent
many-to-many relationships. I prefer to leave my POCOs clear of relationships, but
that's me. For the sake of this example, we're going to use this mapping
the way I would do it in real life.
Let's now take a look at the POCOs.
public class Category
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
public class Product
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
public class CategoryProducts
{
public CategoryProducts()
{
Key = new CategoryProductsKey() ;
}
public virtual CategoryProductsKey Key { get; set; }
public virtual int ProductId
{
get { return Key.ProductId ;}
set { Key.ProductId = value ; }
}
public virtual int CategoryId
{
get { return Key.CategoryId;}
set { Key.CategoryId = value ;}
}
public virtual string CustomDescription { get;set;}
}
CategoryProducts
uses a composite key with 2 fields, one references Product
the
other, Category
. NHibernate forces to override equality member in composite keys
types. Now let's take a look at the key implementation.
public class CategoryProductsKey
{
public int ProductId { get; set; }
public int CategoryId { get; set; }
public override in GetHashCode()
{
return ProductId ^ CategoryId ;
}
public override Equals(object x)
{
return Equals(x as CategoryProductsKey) ;
}
public bool Equals(CategoryProductsKey x)
{
return x != null && x.ProductId == ProductId &&
x.CategoryId == CategoryId ;
}
}
As you can see, equality members implementation has been very simple. Actually is extremely straight forward in most cases.
Automatic Equality Functions
Let's now formally (C#) define a straightforward equality implementation for composite key.
public bool AreEqual(TKey x, TKey)
{
var result = true ;
foreach(var property in typeof(TKey).GetProperties(All))
result &= object.Equals(property.GetValue(x), property.GetValue(y));
return result ;
}
A couple of optimizations could be done, but right now they are not important.
public in GetHashCode(TKey x)
{
var getHashCodeMethod = typeof(object).GetMethod("GetHashCode") ;
var result = 0;
foreach(var property in typeof(TKey).GetProperties(All))
return ^= getHashCodeMethod(property.GetValue(x));
return result ;
}
Using the code
Got the idea, right?? What we're gonna do now is to encapsulate this definition in a class. This class would generate equality functions, clients later would be able to use to compare composite keys. Using it would look just like:
var o1 = new TKey { P1 = v11, P2 = v21, P3 = v31 } ;
var o2 = new TKey { P1 = v21, P2 = v22, P3 = v33 } ;
Func<TKey, TKey, bool> AreEquals = EqualityFunctionsGenerator<TKey>.CreateEqualityComparer();
var r = AreEquals(o1, o2) ; // would work as expected
Func<TKey, int> GetHashCode = EqualityFunctionsGenerator<TKey>.CreateGetHashCode();
var c1 = GetHashCode(o1) ;
var c2 = GetHashCode(o2) ;
You could think of thousand of uses for it, but actually the best use you could give to this functions would be overriding object's definition and letting clients do the rest.
public class Tupple<TObject> : IEquatable<TObject>
where TObject : class
{
private static readonly Func<TObject, int> GetHashCodeMethod = EqualityFunctionsGenerator<TObject> .CreateGetHashCode();
private static readonly Func<TObject, TObject, bool> EqualsMethod = EqualityFunctionsGenerator<TObject> .CreateEqualityComparer();
public override bool Equals(object obj)
{
return Equals(obj as TObject);
}
public override int GetHashCode()
{
var @this = ((object)this) as TObject;
if (@this == null) return 0 ;
return GetHashCodeMethod(@this);
}
public bool Equals(TObject other)
{
var @this = ((object)this) as TObject ;
if (other == null || @this == null) return false ;
return EqualsMethod(@this, other);
}
}
Extending tupple would make all work.
public class CategoryProductsKey : Tupple<CategoryProductsKey>
{
public virtual int ProductId { get; set; }
public virtual int CategoryId { get; set; }
}
Just missing the whole implementation for the functions generator, here it is:
public class EqualityFunctionsGenerator<TObject>
{
public static readonly Type TypeOfTObject = typeof(TObject);
public static readonly Type TypeOfBool = typeof(bool);
public static readonly MethodInfo MethodEquals = typeof(object).GetMethod("Equals",
BindingFlags.Static | BindingFlags.Public);
public static readonly MethodInfo MethodGetHashCode = typeof(object).GetMethod("GetHashCode",
BindingFlags.Instance | BindingFlags.Public);
public static Func<TObject, TObject, bool> CreateEqualityComparer()
{
var x = Expression.Parameter(TypeOfTObject, "x");
var y = Expression.Parameter(TypeOfTObject, "y");
var result = (Expression)Expression.Constant(true, TypeOfBool);
foreach (var property in GetProperties())
{
var comparison = CreatePropertyComparison(property, x, y);
result = Expression.AndAlso(result, comparison);
}
return Expression.Lambda<Func<TObject, TObject, bool>>(result, x, y).Compile();
}
private static Expression CreatePropertyComparison(PropertyInfo property, Expression x, Expression y)
{
var type = property.PropertyType;
var propertyOfX = GetPropertyValue(x, property);
var propertyOfY = GetPropertyValue(y, property);
return (type.IsValueType)? CreateValueTypeComparison(propertyOfX, propertyOfY)
:CreateReferenceTypeComparison(propertyOfX, propertyOfY);
}
private static Expression GetPropertyValue(Expression obj, PropertyInfo property)
{
return Expression.Property(obj, property);
}
private static Expression CreateReferenceTypeComparison(Expression x, Expression y)
{
return Expression.Call(MethodEquals, x, y);
}
private static Expression CreateValueTypeComparison(Expression x, Expression y)
{
return Expression.Equal(x, y);
}
public static IEnumerable<PropertyInfo> GetProperties()
{
return TypeOfTObject.GetProperties(BindingFlags.Instance | BindingFlags.Public);
}
public static Func<TObject, int> CreateGetHashCode()
{
var obj = Expression.Parameter(TypeOfTObject, "obj");
var result = (Expression)Expression.Constant(0);
foreach (var property in GetProperties())
{
var hash = CreatePropertyGetHashCode(obj, property);
result = Expression.ExclusiveOr(result, hash);
}
return Expression.Lambda<Func<TObject, int>(result, obj).Compile();
}
private static Expression CreatePropertyGetHashCode(Expression obj, PropertyInfo property)
{
var type = property.PropertyType;
var propertyOfObj = GetPropertyValue(obj, property);
return type.IsValueType ? CreateValueTypeGetHashCode(propertyOfObj)
: CreateReferenceTypeGetHashCode(propertyOfObj);
}
private static Expression CreateReferenceTypeGetHashCode(Expression value)
{
return Expression.Condition(
Expression.Equal(Expression.Constant(null), value),
Expression.Constant(0),
Expression.Call(value, MethodGetHashCode));
}
private static Expression CreateValueTypeGetHashCode(Expression value)
{
return Expression.Call(value, MethodGetHashCode);
}
private static Expression CheckForNull(Expression value)
{
return Expression.Condition(
Expression.Equal(Expression.Constant(null), value),
Expression.Constant(0),
value);
}
}
Statistics
What is it good for if it is slow? I ran some performance test, results were not as good as I expected, but they were good enough. Test were run on a 1 CPU/1GB VirtualBox virtual machine, over a Phenom II x6 1055T 2.46Ghz. 2 tupple types were created to perform the tests upon, an automatic and a manually implemented. Performance is shown along with equivalent test over int, Point and a manually implemented tupple.
public class AutomaticCompositeKey : Tupple<AutomaticCompositeKey>
{
public string KeyField1 { get; set; }
public int KeyField2 { get; set; }
public int KeyField3 { get; set; }
}
public class ImplementedCompositeKey
{
public string KeyField1 { get; set; }
public int KeyField2 { get; set; }
public int KeyField3 { get; set; }
public override int GetHashCode {...}
public override bool Equals(object x) {...}
}
Equals Test Results
Test | 10M Cases | 100M Cases | 1G Cases |
ints equality test | 0 | 0.04 | 1.747 |
Point equality using == operator
|
0.01 | 0.1 | 1.398 |
Point equality using Equals | 0.08 | 0.671 | 11.236 |
Manually implemented tupple | 0.06 | 0.491 | 18.448 |
Automatic tupple | 0.19 | 1.122 | 33.076 |
GetHashCode Test Results
Test | 10M Cases | 100M Cases | 1G Cases |
Point | 0.01 | 0.05 | 0.496 |
Manually implemented tupple | 0.09 | 0.831 | 9.136 |
Automatic tupple | 0.23 | 1.402 | 15.03 |
Some additional cases were run over the automatic tupple to determine executions per second. An excel with a linear regression to determine following values is also available.
Function | Million Times per second |
Equals | 85.4 |
GetHashCode | 67.53 |
Conclusions
Every time I found a repetitive task I try to make automatic. Reflection, Emit and now Linq.Expressions are amazingly helpful for doing so. This small package is part of a library I'm finishing these days. Hope you'd find this useful.
Useful links
发表评论
The Woman experiences headache, treatment and egg. some the above they like. Your middle natal gradually get of infection conceiving commonly.