Myotragus 元组 - 自动化 NHibernate 复合键






4.86/5 (6投票s)
自动创建相等成员 (Equals 和 GetHashCode)。
引言
主键通常是自动递增的整数或GUID。不幸的是,许多领域需要更多。主键不是定义为单列,而只是唯一。迟早你会发现自己需要复合主键。使用复合主键时,能够将相等函数应用于键值非常有用。实际上,NHibernate 要求复合主键重写相等成员 (Equals 和 GetHashCode)。本文旨在使实现相等函数的过程变得轻松。
目录
模型
让我们来看一下下面的领域。
<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>
在之前的代码中,定义了一个包含三个实体的域。Product 和 Category 不需要解释。另一方面,CategoryProducts 需要解释。如果您有NHibernate经验,您可能会在Product 和 Category 中都使用集合来表示多对多关系。我更喜欢让我的POCO保持关系清晰,但这只是我个人的偏好。为了这个例子,我们将按照我在现实生活中使用这种映射的方式来使用它。现在让我们看一下POCO。
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 使用具有两个字段的复合键,一个引用Product,另一个引用Category。NHibernate 强制重写复合键类型的相等成员。现在让我们来看一下键的实现。
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 ;
    }
}
如您所见,相等成员的实现非常简单。实际上,在大多数情况下它非常直接。
自动相等函数
现在让我们正式地 (C#) 定义一个用于复合键的简单相等实现。
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 ;
}
可以进行一些优化,但现在它们并不重要。
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 ;
}
使用代码
明白了吗?我们现在要做的是将此定义封装在一个类中。这个类将生成相等函数,客户端稍后可以使用它来比较复合键。使用它看起来就像
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) ;
您可以想到成千上万种用途,但实际上,您可以赋予这些函数的最佳用途是重写对象的定义并让客户端完成其余工作。
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); 
    } 
}
扩展一个元组将使一切正常工作。
public class CategoryProductsKey : Tupple<CategoryProductsKey>
{
    public virtual int ProductId { get; set; }
    public virtual int CategoryId { get; set; }
} 
只是缺少函数生成器的整个实现,这里就是
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);
    }
}
统计数据
如果它很慢,它有什么用?我进行了一些性能测试,结果不如预期好,但足够好。测试是在一台1 CPU/1GB VirtualBox虚拟机上运行的,该虚拟机运行在Phenom II x6 1055T 2.46Ghz处理器上。创建了两种元组类型来进行测试,一种是自动实现的,另一种是手动实现的。性能结果将与int、Point 和手动实现的元组的等效测试一起显示。
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 测试结果
| 测试 | 1000万次测试 | 1亿次测试 | 10亿次测试 | 
| int相等性测试 | 0 | 0.04 | 0.566 | 
| 使用 ==运算符的Point相等性 | 0.01 | 0.1 | 1.398 | 
| 使用 Equals的Point相等性 | 0.08 | 0.671 | 8.12 | 
| 手动实现的元组 | 0.06 | 0.491 | 5.31 | 
| 自动生成的元组 | 0.19 | 1.122 | 13.2 | 
GetHashCode 测试结果
| 测试 | 1000万次测试 | 1亿次测试 | 10亿次测试 | 
| Point | 0.01 | 0.05 | 0.496 | 
| 手动实现的元组 | 0.09 | 0.831 | 9.136 | 
| 自动生成的元组 | 0.23 | 1.402 | 15.03 | 
对自动生成的元组进行了一些额外的测试以确定每秒执行次数。还有一个包含线性回归的Excel文件可用于确定以下值。
| 函数 | 每秒百万次 | 
| Equals | 85.4 | 
| GetHashCode | 67.53 | 
结论
每当我发现重复性任务时,我都会尝试将其自动化。反射、Emit和现在的Linq.Expressions在这方面非常有用。这个小包是我最近完成的一个库的一部分。希望你会发现它有用。


