Tuesday, January 24, 2006

Why Equals and GetHashCode are so important to NHibernate

So I have been applying NHibernate.Generics to all my projects.  I have learned some valuable lessons.

NHibernate has always warned that all entity classes should implement their own Equals and GetHashCode methods.  I've procrastinated implementing them because NHibernate seemed to work just fine without them.  Then I started using proxy classes for lazy loading, and problems started popping up with objects comparing themselves against each other and deciding they were not equal when in fact they represented the same entity.  Lately while applying the new NHibernate.Generics collections, I found that if I had a collection of two entities, there would be three entities in the collection, where one of the entities was in there twice!  My first reaction was "Ugh!  This collection class is buggy!"  Then I remembered my neglected absence of GetHashCode and Equals.  Without them, how is a collection to know that two objects (where one is only a proxy class) are actually the same entity?

So here is the code I hurriedly injected into every last one of my entity classes:
/// 
/// Tests whether this and another object are equal in a way that
/// will still pass when proxy objects are being used.
///

public override bool Equals(object obj)
{
ClassNameHere other = obj as ClassNameHere;
if (other == null) return false;
if (Id == 0 && other.Id == 0)
return (object)this == other;
else
return Id == other.Id;
}

public override int GetHashCode()
{
if (Id == 0) return base.GetHashCode();
string stringRepresentation =
System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName
+ "#" + Id.ToString();
return stringRepresentation.GetHashCode();
}
With these two methods, my problems disappeared!  I even went so far as to write a reusable Code Snippet that would assist me in setting the class name that must appear in each segment.  I include the snippet file here:

xml version="1.0" encoding="utf-8" ?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>Entity Equals and GetHashCodeTitle>
<Shortcut>nheqShortcut>
<Author>Andrew ArnottAuthor>
<SnippetTypes>
<SnippetType>ExpansionSnippetType>
SnippetTypes>
<Description>Inserts the Equals and GetHashCode methods that NHibernate requires to run correctly.Description>
Header>
<Snippet>
<Code Language="CSharp">
[CDATA[ /// <summary>
/// Tests whether this and another object are equal in a way that
/// will still pass when proxy objects are being used.
/// summary>
public override bool Equals(object obj)
{
$type$ other = obj as $type$;
if (other == null) return false;
if (Id == 0 && other.Id == 0)
return (object)this == other;
else
return Id == other.Id;
}

public override int GetHashCode()
{
if (Id == 0) return base.GetHashCode();
string stringRepresentation =
System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName
+ "#" + Id.ToString();
return stringRepresentation.GetHashCode();
}

]]>
Code>
<Declarations>
<Literal Editable="true">
<ID>typeID>
<ToolTip>The name of the class being injected.ToolTip>
<Default>ClassNameDefault>
Literal>
Declarations>
Snippet>
CodeSnippet>
CodeSnippets>
By the way, I'm not too proud of my implementation of GetHashCode().  If you know a better way, please comment on the blog and let me know.

1 comment:

  1. I think you need to be careful about using Id (database identity) in your GetHashCode() function. If you have two transient (unsaved) objects in a collection, their IDs default and are unset (until NH persists them). If IDs are DB identities, then GetHashCode() would generate the same hash code for two different (unsaved) transient objects of the same type.

    It looks like Equals() will take care of this ambiguity, but it would lead to a lot of duplicated code in your domain objects.

    Check out Billy's solution:
    here

    I think billy deals with Equals() and GetHashCode() problem quite well.

    Take care,
    Brian Chavez

    ReplyDelete