How can the following problem be dealt with?
We're using lazy loaded NHibernate properties and whenever we're calling Equals()
or GetHashCode()
any properties used, will be lazy-loaded, potentially causing a cascade of lazy-loading operations. Eager-loading could be used as an alternative, but I think only in specific cases and not as a general solution.
A typical scenario would look like this:
public class AbstractSaveableObject {
[Id(0, Name = "Id", UnsavedValue = null)]
[Generator(1, Class = "native")]
public virtual long? Id { get; set; }
}
[Class(NameType = typeof(ClassA))]
public class ClassA : AbstractSavableObject {
[Bag(0, Inverse = true, Cascade = "none")]
[Key(1, Column = "ClassA")]
[OneToMany(2, ClassType = typeof(ClassB))]
public virtual ICollection<ClassB> ClassBs { get; set; }
}
[Class(NameType = typeof(ClassB))]
public class ClassB : AbstractSavableObject {
[ManyToOne(Column = "ClassA")]
public virtual ClassA ClassA { get; set; }
[ManyToOne]
public virtual ClassC ClassC { get; set; }
[ManyToOne]
public virtual ClassD ClassD { get; set; }
public virtual bool Equals(ClassB other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return Equals(other.ClassC, ClassC) && Equals(other.ClassD, ClassD);
}
}
Implementation of GetHashCode
and Equals(object)
have been omitted for brevity.
What strategies can be been used to tackle this issue?
Two entities are equal if they are of the same type and has the same primary key.
If you have integers for keys:
If you have GUIDs for keys:
If I have integers for keys I usually have something like this Equal-override in a base class for my entities:
public virtual bool Equals(EntityBase other)
{
if (other == null)
{
return false;
}
if (ReferenceEquals(other, this))
{
return true;
}
var otherType = NHibernateProxyHelper.GetClassWithoutInitializingProxy(other);
var thisType = NHibernateProxyHelper.GetClassWithoutInitializingProxy(this);
if (!otherType.Equals(thisType))
{
return false;
}
bool otherIsTransient = Equals(other.Id, 0);
bool thisIsTransient = Equals(Id, 0);
if (otherIsTransient || thisIsTransient)
return false;
return other.Id.Equals(Id);
}
Now if you entities that inherit from others using table per hierarchy you will face the problem that GetClassWithoutInitializingProxy will return the base class of the hierarchy if it's a proxy and the more specific type if it's a loaded entity. In one project I got around that by traversing the hierarchy and thus always comparing the base types - proxy or not.
In these days though I would always go for using GUIDs as keys and do as described here: http://nhibernate.info/doc/patternsandpractices/identity-field-equality-and-hash-code.html
Then there is no proxy type mismatch problem.
If you are using identity equality, you should be able to access the key without triggering a load:
public virtual bool Equals(ClassB other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
// needs to check for null Id
return Equals(other.ClassC.Id, ClassC.Id) && Equals(other.ClassD.Id, ClassD.Id);
}
You can handle comparisons between objects before and after persisting by caching the hash code when it was transient. This leaves a small gap in the Equals contract in that a comparison between an existing object that was transient will not generate the same hash code as a newly-retrieved version of the same object.
public abstract class Entity
{
private int? _cachedHashCode;
public virtual int EntityId { get; private set; }
public virtual bool IsTransient { get { return EntityId == 0; } }
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
var other = obj as Entity;
return Equals(other);
}
public virtual bool Equals(Entity other)
{
if (other == null)
{
return false;
}
if (IsTransient ^ other.IsTransient)
{
return false;
}
if (IsTransient && other.IsTransient)
{
return ReferenceEquals(this, other);
}
return EntityId.Equals(other.EntityId);
}
public override int GetHashCode()
{
if (!_cachedHashCode.HasValue)
{
_cachedHashCode = IsTransient ? base.GetHashCode() : EntityId.GetHashCode();
}
return _cachedHashCode.Value;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With