views:

147

answers:

4

How can I get the following test to pass with NHibernate?

I thought it was enough to simply override Equals and GetHashCode in the entity class for this to work the way I want it to. Obviously for "Point" objects, which are quite trivial, it's silly to persist multiple rows for identical coordinates. I have two point objects that have identical coordinates, and I want them to only persist to one row in the database.

    Point p1 = new Point(1, 1, 1);
    Point p2 = new Point(1, 1, 1);
    Assert.AreEqual(p1, p2); //Passes
    session.Save(p1);
    session.Save(p2);
    tx.Commit();
    IList<Point> points = session.CreateCriteria<Point>()
        .List<Point>();
    Assert.AreEqual(1,points.Count); //FAILS

Where my point class looks something like this:

public class Point
{
    public virtual Guid Id { get; set; }
    public virtual double X { get; set; }
    public virtual double Y { get; set; }
    public virtual double Z { get; set; }

    public Point(double x, double y, double z)
    {
        X = x; Y = y; Z = z;
    }
    public override bool Equals(object obj)
    {
        Point you = obj as Point;
        if (you != null)
            return you.X == X && you.Y == Y && you.Z == Z;
        return false;
    }

    public override int GetHashCode()
    {
        int hash = 23;
        hash = hash * 37 + X.GetHashCode();
        hash = hash * 37 + Y.GetHashCode();
        hash = hash * 37 + Z.GetHashCode();
        return hash;
    }
}
+1  A: 

Usually NHibernate determines if the object is saved or not by its id.

So if you will implement Id attribute to return the same Id for all 'equal' objects it should be alright (I think).

So try if this makes your test pass (make appropriate changes to the mapping):

public class Point
{
    public virtual int Id { 
        get { this.GetHashCode() }
    }
// the rest

You might also use some other value for the Id as HashCode is not guaranteed to be unique this way.

Dmytrii Nagirniak
The code is wrong, hashcodes are not unique
Diego Mijelshon
@Diego, in this particular case HashCode is customly defined. But my example just demonstrates the implementation of Id returning the same Id for the same objects.
Dmytrii Nagirniak
The way to enforce that is to use a composite Id, as others have pointed out, which is a bad idea anyway.
Diego Mijelshon
+3  A: 

I believe you are tackling the problem from the wrong angle.

If this is your actual domain, you should be using Components or UserTypes for Points, not a separate table. Point clearly has value-type semantics.

Read http://nhforge.org/doc/nh/en/index.html#components and http://nhforge.org/doc/nh/en/index.html#mapping-types-custom

Diego Mijelshon
+1 I couldn't agree more! It's a shame I didn't read all of the answers before I write mine. ;P
Will Marcouiller
+2  A: 

NHibernate doesn't recognize the two Point instances in your example as being the same because they have different IDs. It looks like you're using GUIDs as the primary key in your Points table, and each Point you create will have a different GUID.

I think what you're searching for is called a composite ID, as described here. Please note, however, that the NHibernate manual says that composite keys only exist to support legacy databases, and they strongly recommend against composite keys:

"Unfortunately, this approach to composite identifiers means that a persistent object is its own identifier. There is no convenient "handle" other than the object itself. You must instantiate an instance of the persistent class itself and populate its identifier properties before you can load() the persistent state associated with a composite key."

Instead, the manual suggests you might use a component as a composite identifier.

Personally, I'd consider keeping the GUIDs the way they are, and then adding logic to the application layer that prevents duplicate Points, rather than enforcing it in the database; but it all depends on the individual needs of your application.

David Mills
+1 You're totally right.
Will Marcouiller
A: 

This means that you want all Points which have the (1, 1, 1) coordinates to be considered as only one. Despite it is not encouraged within the NHibernate practices, you may do so with composite-ids.

Following this link: Chapter 7. Component Mapping, while you scroll down to 7.4 where composite-id is explained. Then you're mapping should look like so:

<class="Point" tables="POINTS">
  <composite-id name="CompId" class="PointCoord">
    <key-property name="X" type="System.Double">
    <key-property name="Y" type="System.Double">
    <key-property name="Z" type="System.Double">
  </composite-id>
  <property name="Id" type="System.Guid">
</class>

And your class would need to get splitted like so:

public class PointCoord {
    public double X { get; set; }
    public double Y { get; set; }
    public double Z { get; set; }
}

public class Point : PointCoord {  
    public Guid Id { get; set; }
}

Doing that way, you won't need to keep your Guid as the Id of your class, since it is no longer the Id anymore. Your Id is now your coordinates X, Y and Z. Then, you would have to override the Equals() method accordingly:

public override bool Equals(object obj) {
    if (obj == null)
        return false;

    if (((Point)obj) == null) 
        return false;

    Point p = (Point)obj;

    return this.X == p.X && this.Y == p.Y && this.Z == p.Z
}

By the way, it is always good to have an overload of this method taking your classe type as the input parameter, this gives better performance:

public bool Equals(Point pt) {
    if (pt == null) 
        return false;

    return this.X == pt.X && this.Y == pt.Y && this.Z == pt.Z
}

However, this is normally not considered a good practice and NHibernate strongly recommends that every table have its own DB Id, and this Id must not be a significant domain value such as, for instance, an invoice number. You would have your invoice number, and your DB Id. This composite-id thing is kept for legacy compatibility.

Will Marcouiller