Setting up a base entity class
设置一个实体类的基类
在这节中,我将给你展示怎么样去为我们的实体类设置一个通用的基类。
准备工作
完成前面三节的任务
如何去做
1.在Entity.cs中,为我们的Entity类输入如下代码:
{
public virtual TId Id { get; protected set; }
public override bool Equals(object obj)
{
return Equals(obj as Entity<TId>);
}
private static bool IsTransient(Entity<TId> obj)
{
return obj != null &&
Equals(obj.Id, default(TId));
}
private Type GetUnproxiedType()
{
return GetType();
}
public virtual bool Equals(Entity<TId> other)
{
if (other == null)
return false;
if (ReferenceEquals(this, other))
return true;
if (!IsTransient(this) &&
!IsTransient(other) &&
Equals(Id, other.Id))
{
var otherType = other.GetUnproxiedType();
var thisType = GetUnproxiedType();
return thisType.IsAssignableFrom(otherType) ||
otherType.IsAssignableFrom(thisType);
}
return false;
}
public override int GetHashCode()
{
if (Equals(Id, default(TId)))
return base.GetHashCode();
return Id.GetHashCode();
}
}
2. 在这个文件中,我们添加另一个额外的Entity类,代码如下所示:
{
}
分析原理
NHibernate需要依赖于Equals方法进行等同性判断。该方法默认定义于System.Object类中,它在引用类型中使用引用相等性判断(即内存地址相同则认为相等),即,x.Equals(y)仅仅在x和y指向相同的对象实例时才是true。这个默认的行为在大多数情况下工作的很好。
为了支持延迟加载,NHibernate使用代理对象。就像我们之前学习过的,这些代理对象实际上是真正的实体类的子类,为了支持延迟加载,它的每个成员都已经被重写过了。
这些代理对象,在你的应用程序中这个默认的Equals方法的行为会导致一些隐含和意想不到的bug。一个应用程序对于使用代理对象应该是不知不觉的,是透明的,所以我们期望的情况是,一个代理对象和一个真实的对象如果它们描述的是同一个实体对象,则它们就应该是相等的。如果一个Product对象的ID是8,另一个Product对象的ID也是8,或者有一个代理的Product对象的ID是8,我们应该认为这三个对象是相等的,具有等同性。要做到它们具有等同性,我们必须重写默认的Equals方法,以改变该方法的默认行为(按内存地址判断相等性)。在我们上面的Entity基类中,我们重写了Equals方法,使得该方法基于POID来决定相等性。在Equals(Object obj)方法中,我们简单的调用了其重载方法Equals(Entity<TId> other),尝试把object类型转换为Entity类型,如果转换失败,则会把null值作为参数传递给Equals(Entity<TId> other)方法,如果参数other的值为null,那么这两个对象就不相等。这个有两种情况,第一,x.Equals(null)永远返回false,第二,someEntity.Equals(notAnEntity)也返回false。接下来,我们比较两者的引用地址,很显然,如果两者的变量引用的是同一个实例对象(相同内存地址)则它们必定相等。如果ReferenceEquals(this,other)返回的是true,则我们也返回true。
再接下来,我们比较Id和默认的Id值,用来判断实体对象是否是临时态,一个处于临时态的对象是一个尚未持久化至数据库中的对象。default(TId)始终返回的是TId的默认值,对于Guid来说,它的默认值是Guid.Empty,对于string和其他所有的引用类型来说,它们的默认值是null,对于数字类型,则默认值为0。如果这个Id属性和它的默认值相等,则该实体对象处于临时态。如果一个或者两个实体处于临时态,则我们认为它们必定是不等的(如果待比较的两个对象中存在一个以上的对象处于临时态,则它们必定不等),需要返回false。
如果实体对象处于持久态,并且它们都拥有POID,我们就可以通过比较它们的POID值来确定相等性。如果POID不相等,我们就认为待比较的两个实体对象是不相等的(不等同),我们返回false。
最后,我们还要作最后一次的判断。我们知道至此为止,待比较的对象肯定是处于持久态了,并且它们都有相同的Id值,但是这也不能证明肯定就是相等的。如果一个ActorRole实体和一个Product实体具有相同的POID值,那么如果仅仅是上面的代码的话也会被认为是相等的。我们最后的判断就是要去比较两者的类型,如果其中的一个类型派生了另一个类型,则我们认为它们是相等的。
假如other参数是一个Product的代理对象,并且描述的是一个book实体,而真正的book实例描述的是同一个对象,那么this.Equals(other)应该返回true,因为它们都描述的是同一个实体对象。然而,不幸的是other.GetType()不是返回Product,而是返回ProductProxy12398712938,而typeof(ProductProxy12398712938).IsAssignableFrom(typeof(Book))的结果会是false,在这种情况下,我们的Equals方法将不可行。正因为如此,我们需要使用other. GetUnproxiedType()方法,它会透过代理层从而返回实际的实体类型,因为typeof(Product).IsAssignableFrom(typeof(Book))返回true,所以我们的Equals实现可以工作的很好。
因为我们重写了Equals方法,所以我们也必须重写GetHashCode方法,这也是.NET Framework框架规范的要求。如果x.Equals(y),那么x.GetHashCode()和y.GetHashCode()应该返回相同的值,而反过来并不是必须的(如果x.GetHashCode()和y.GetHashCode()返回相同的值,则x.Equals(y)可以返回false),当它们不相等的时候x和y也可以共享一个hash code。在我们的Entity基类中,我们简单地使用了Id的hash code值。
补充知识
更多的关于Equals和GetHashCode的知识,请参阅MSDN文档的相关内容,http://msdn.microsoft.com/en-us/library/system. object.aspx.
补充:
1. 第四节的映射集合中,对ISet集合的处理我们曾经提到过需要重写Equals 和 GetHashCode方法,判断元素是否已经存在于ISet中就需要使用Equals来进行判断。
2. 上面的泛型中TId如果也是一个引用类型的话则也必须重写其Equals 和 GetHashCode方法。