Hibernate Books

All times are UTC - 5 hours [ DST ]



Post new topic Reply to topic  [ 147 posts ]  Go to page 1, 2, 3, 4, 5 ... 10  Next
Author Message
 Post subject: equals() / hashCode(): Is there *any* non-broken approach?
PostPosted: Thu Feb 19, 2004 4:34 pm 
Beginner
Beginner

Joined: Thu Nov 06, 2003 7:27 pm
Posts: 30
Location: Minneapolis, MN, USA
We're grappling with how to implement equals()/hashCode() on our persistent objects. The Hibernate page on the subject recommends using a "semi-unique key" for equals, instead of ID. That doesn't work -- very few of our objects actually have such a key, and IMO it's terrible advice anyway: "Use some other ID that is not the ID as your ID for the purpose of equals()." Come on, guys! I'm sorry, but our PK is our PK; the ID is the ID. Period.

<rehash of well-known discussion>

Our strategy has thus been to use ID when present, but fall back to instance equality if the ID is not set. Here's the problem with that approach:

Code:
Thinger t = new Thinger(); // t has no ID yet
Set s = new HashSet();
s.add(t);
s.contains(t); // returns true
session.save(t); // t now has an ID
s.contains(t); // returns false, because t's hash code changed

I hasten to point out that the problem here is not hashCode() -- it is behaving correctly: after being saved, t compares equal to objects it wouldn't have before (namely, anything with the same ID), so its hashCode must change. And no, making hashCode() always return a constant is not acceptable to us, thank you.

The real, fundamental problem here is this: objects used in a set/map must be equals-immutable (meaning that their equals() methods can't depend on mutable fields) -- the Collections javadoc is very clear on this point -- and our objects use a mutable field (ID) in equals().

</rehash of well-known discussion>

There are really only two real solutions to this problem:
    (1) Use instance-based equality (that is, don't override equals()).
    (2) Assign the ID when the object is created, not when it is persisted.

So, my questions are:

On (1): What's wrong with instance-based equality? Doesn't Hibernate guarantee instance uniqueness within a session? (It had darned well better!) Does this break caches, or what? This should, in theory, be a perfectly valid approach.

On (2): Can we ask Hibernate to assign object IDs on instantiation? (I'm presuming the answer is no; I've never heard of such a feature.)


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 19, 2004 4:38 pm 
Hibernate Team
Hibernate Team

Joined: Tue Sep 09, 2003 2:10 pm
Posts: 3246
Location: Passau, Germany
Quote:
On (1): What's wrong with instance-based equality? Doesn't Hibernate guarantee instance uniqueness within a session? (It had darned well better!) Does this break caches, or what? This should, in theory, be a perfectly valid approach.


Theoretically nothing, Hibernate sure does not have a problem with that. You must remeber however that in this case two equal objects are not .equals() if loaded in different sessions.

Quote:
On (2): Can we ask Hibernate to assign object IDs on instantiation? (I'm presuming the answer is no; I've never heard of such a feature.)


No, thats not possible - how should Hibernate know when you create a new object.


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 19, 2004 4:48 pm 
Beginner
Beginner

Joined: Thu Nov 06, 2003 7:27 pm
Posts: 30
Location: Minneapolis, MN, USA
Quote:
Quote:
On (1): What's wrong with instance-based equality? Doesn't Hibernate guarantee instance uniqueness within a session? (It had darned well better!) Does this break caches, or what? This should, in theory, be a perfectly valid approach.

Theoretically nothing, Hibernate sure does not have a problem with that.

That's good news! Do you know anything about its effect on global cache plug-ins?

Quote:
You must remeber however that in this case two equal objects are not .equals() if loaded in different sessions.

That seems desirable to me. Our app uses SSBs pretty much exclusively, and each SSB call has its own private session. So objects from different sessions should never play together, except in a global cache.

Quote:
Quote:
On (2): Can we ask Hibernate to assign object IDs on instantiation? (I'm presuming the answer is no; I've never heard of such a feature.)

No, thats not possible - how should Hibernate know when you create a new object.

Well, obviously we'd have to call some hook -- something like:
Code:
session.getNextId(getClass());

But the first solution seems preferable to me.


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 19, 2004 4:50 pm 
Hibernate Team
Hibernate Team

Joined: Mon Aug 25, 2003 9:11 pm
Posts: 4592
Location: Switzerland
You only need to implement equals()/hashCode() on your persistent classes if

- it is a used as a composite primary key class

- instances of that class, loaded in different Hibernate Sessions, are in the same Set (or should be compared)

You don't need it if your Session's never share data. Remember that a Session is a single unit-of-work, a short scope. This also explains your problem with the persistent identifer for an implementation of equals()/hashCode(). The persistent identifier has the same scope as a persistent object, that is: as long as an instance is associated with a Session, i.e. it starts with session.save(o) and it ends with session.close().

Yes, we had recommended the usage of a persistent identifier for equals()/hashCode() at some point, but I think we always had some hints about this problem on the same page. We no longer use this approach (for obvious reasons) at all and I'd like to know if we missed some sentences somewhere.

_________________
JAVA PERSISTENCE WITH HIBERNATE
http://jpwh.org
Get the book, training, and consulting for your Hibernate team.


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 19, 2004 4:51 pm 
Hibernate Team
Hibernate Team

Joined: Mon Aug 25, 2003 9:11 pm
Posts: 4592
Location: Switzerland
The global (second level) cache handles this transparently, it is a cache of data, not actual persistent instances (this is the first level Session cache).

_________________
JAVA PERSISTENCE WITH HIBERNATE
http://jpwh.org
Get the book, training, and consulting for your Hibernate team.


Top
 Profile  
 
 Post subject: Awesome answers
PostPosted: Thu Feb 19, 2004 5:06 pm 
Beginner
Beginner

Joined: Thu Nov 06, 2003 7:27 pm
Posts: 30
Location: Minneapolis, MN, USA
That clears it all up for us.

The page I was looking at (http://hibernate.org/109.html) doesn't mention any of this. It would be useful to summarize some of this discussion there, particularly this text of Christian's:
Quote:
You only need to implement equals()/hashCode() on your persistent classes if

- it is a used as a composite primary key class

- instances of that class, loaded in different Hibernate Sessions, are in the same Set (or should be compared)

You don't need it if your Sessions never share data.

Thanks for the speedy & excellent answers. You put commercial support to shame. :)


Top
 Profile  
 
 Post subject:
PostPosted: Thu Feb 19, 2004 5:13 pm 
Hibernate Team
Hibernate Team

Joined: Mon Aug 25, 2003 9:11 pm
Posts: 4592
Location: Switzerland
Oh, it is actually

- if your class is used in any <composite-*> mapping

- instances of that class, loaded in different Hibernate Sessions, are in the same Set (or should be compared)

I'll add it to the Wiki page.

_________________
JAVA PERSISTENCE WITH HIBERNATE
http://jpwh.org
Get the book, training, and consulting for your Hibernate team.


Top
 Profile  
 
 Post subject: OK, now throw proxies into the mix
PostPosted: Tue Feb 24, 2004 12:58 pm 
Beginner
Beginner

Joined: Thu Nov 06, 2003 7:27 pm
Posts: 30
Location: Minneapolis, MN, USA
So what happens to all our discussion above when we start using proxies?

Generally speaking, we need the following to be true for some persistent object o and its proxies p, p2:
    (1) p.equals(p)
    (2) p.equals(p2)
    (3) p.equals(o)
    (4) o.equals(p)
Furthermore, we need (1) and (2) not to incur a hit to the database. And to top it all off, the discussion above still applies!

It seems that, if we don't override equals(), at least (2) and (4) fail and possibly the others. We're rather at a loss here! Any ideas?


Top
 Profile  
 
 Post subject:
PostPosted: Tue Feb 24, 2004 5:44 pm 
Expert
Expert

Joined: Thu Jan 08, 2004 6:17 pm
Posts: 278
christian wrote:
Yes, we had recommended the usage of a persistent identifier for equals()/hashCode() at some point, but I think we always had some hints about this problem on the same page. We no longer use this approach (for obvious reasons) at all and I'd like to know if we missed some sentences somewhere.

Well, I don't know about the documentation, but I do know that my hbm2java generated classes ALL contain methods like this:

Code:
    public boolean equals(Object other) {
        if ( !(other instanceof Client) ) return false;
        Client castOther = (Client) other;
        return new EqualsBuilder()
            .append(this.getId(), castOther.getId())
            .isEquals();
    }

    public int hashCode() {
        return new HashCodeBuilder()
            .append(getId())
            .toHashCode();
    }

Which, of course, is a use of a persistent identifier for equals() AND hashCode()....

So is it the case that hbm2java is not following your own recommended best practice for object identity???

Cheers,
Rob


Top
 Profile  
 
 Post subject: Re: OK, now throw proxies into the mix
PostPosted: Tue Feb 24, 2004 5:46 pm 
Expert
Expert

Joined: Thu Jan 08, 2004 6:17 pm
Posts: 278
melquiades wrote:
Generally speaking, we need the following to be true for some persistent object o and its proxies p, p2:
    (1) p.equals(p)
    (2) p.equals(p2)
    (3) p.equals(o)
    (4) o.equals(p)
Furthermore, we need (1) and (2) not to incur a hit to the database. And to top it all off, the discussion above still applies!

When do you have two proxies (p and p2) for the same object (o) in the same session? Wouldn't that be a Hibernate bug?

Seems to me that the only way to get (3) would be to use the persistent identifier as equality, FOR PROXIES ONLY. (since proxies will never NOT have a persistent identifier, and since proxies will never equal any object which DOESN'T have a persistent identifier.)

Would that cover all your cases? Seems like it might.....
Cheers!
Rob


Top
 Profile  
 
 Post subject:
PostPosted: Tue Feb 24, 2004 5:51 pm 
Beginner
Beginner

Joined: Thu Nov 06, 2003 7:27 pm
Posts: 30
Location: Minneapolis, MN, USA
(1) p.equals(p)
(2) p.equals(p2)
(3) p.equals(o)
(4) o.equals(p)

Quote:
When do you have two proxies (p and p2) for the same object (o) in the same session? Wouldn't that be a Hibernate bug?


I don't know how Hibernate creates proxies. If it always proxies the same ID with the same proxy object, then clearly (2) is not a case we have to deal with. But does Hibernate actually guarantee this?

Quote:
Seems to me that the only way to get (3) would be to use the persistent identifier as equality, FOR PROXIES ONLY. (since proxies will never NOT have a persistent identifier, and since proxies will never equal any object which DOESN'T have a persistent identifier.)

Would that cover all your cases? Seems like it might.....


This approach breaks (4): o does not use ID equality, and thus does not think it's equal to the proxy object.


Top
 Profile  
 
 Post subject:
PostPosted: Tue Feb 24, 2004 5:57 pm 
Expert
Expert

Joined: Thu Jan 08, 2004 6:17 pm
Posts: 278
If o.equals() was of the form

public boolean equals (Object other) {
if (other instanceof OProxy) {
// use persistent id equality
} else return super.equals(other);
}

then o.equals() would use ID equality ONLY IF other was a proxy, and not otherwise.

Cheers,
Rob


Top
 Profile  
 
 Post subject:
PostPosted: Tue Feb 24, 2004 6:05 pm 
Beginner
Beginner

Joined: Thu Nov 06, 2003 7:27 pm
Posts: 30
Location: Minneapolis, MN, USA
Quote:
Quote:
Seems to me that the only way to get (3) would be to use the persistent identifier as equality, FOR PROXIES ONLY. (since proxies will never NOT have a persistent identifier, and since proxies will never equal any object which DOESN'T have a persistent identifier.)

Would that cover all your cases? Seems like it might.....


This approach breaks (4): o does not use ID equality, and thus does not think it's equal to the proxy object.


Oh, wait, maybe I'm misunderstanding. You mean that my object figures out whether it's being compared to a proxy, and if so use the ID? Something like this?
Code:
    public final boolean equals(Object that) {
        if(that instanceof CatImpl)
            return this == that;
        else if(that instanceof Cat) // proxy; compare using ID
            return this.id.equals(((Cat) that).getId());
        else
            return false;
    }

OK, but how do I implement hashCode()? Now my object has to be hashed differently depending on what it's being compared to.....


Top
 Profile  
 
 Post subject:
PostPosted: Tue Feb 24, 2004 6:26 pm 
Expert
Expert

Joined: Thu Jan 08, 2004 6:17 pm
Posts: 278
Hm, you're right. This equals() contract may not be able to apply to hashCode().

Let's step back and look at the constraints and their implications:

[list=]An object should be equal to (and hash to the same code as) its proxy. This implies that Java == MUST NOT be the basis for equals() and hashCode() FOR PERSISTENT OBJECTS, and that persistent identity MUST be the basis for equals() and hashCode() FOR PERSISTENT OBJECTS. This will work with proxies since proxies necessarily only exist for persistent objects.

Whether an object has been persisted or not should not affect its equality or its hashcode. This implies that persistent identity MUST NOT be the basis for equals() and hashCode() for possibly-non-persistent objects, and that Java == MUST be the basis for equals() and hashCode() for possibly non-persistent objects.[/list]

This makes it pretty clear that you can't have everything you want.

Either:

1) you use Object.equals() and Object.hashCode() (i.e. Java object identity) semantics, and you give up o.equals(p);

or

2) you use persistent identity semantics, and you give up "Object newO = new O(); HashSet set = new HashSet(); set.add(newO); session.save(newO); assert(set.contains(newO));";

or

3) you change semantics as soon as you save your objects, and give up equals-immutability. (Which you also have to give up in option (2), and then you have even less consistency.)

That's it. Those are your only choices.

Way back, you asked whether Hibernate could assign object IDs on instantiation. Hibernate may not be able to enforce this itself. But you could possibly do:

Code:
public class O {
    private O () {
    }
    public static final O create () {
        O newO = new O();
        ThreadLocalSession.save(newO);
        return newO;
    }
}

Thereby ensuring that all O's will always have been saved before you can start working with them.

The ONLY other choice would be to assign some kind of random number or other UUID on creation, which would get saved. But then you're basically doing your own ID assignment. Maybe that's what you *want* in this case...???

Personally I'm not clear on how much equals-immutability makes sense in a persistently mapped world....
Cheers,
Rob


Top
 Profile  
 
 Post subject:
PostPosted: Tue Feb 24, 2004 8:07 pm 
Beginner
Beginner

Joined: Thu Nov 06, 2003 7:27 pm
Posts: 30
Location: Minneapolis, MN, USA
RobJellinghaus wrote:
(...much good reasoning...) This makes it pretty clear that you can't have everything you want.[\quote]

You may be right.

Quote:
The ONLY other choice would be to assign some kind of random number or other UUID on creation, which would get saved. But then you're basically doing your own ID assignment. Maybe that's what you *want* in this case...???


That's the obvious solution that's been lurking around this while time. I've never particularly liked it. But perhaps it's the only way in this case!

Quote:
Personally I'm not clear on how much equals-immutability makes sense in a persistently mapped world....


I think it's objects that don't make sense in a relationally mapped world -- instance identity just doesn't have any relational equivalent!

I do see one potential loophole in your reasoning above: if I create an object in a session, I'll never get a proxy for it in that same session (I hope); conversely, if I read an object and get a proxy, that means I won't be creating it again. Therefore, your contradictory constraints are not ever in play in the same session. We can rephrase them:

Quote:
An object should be equal to (and hash to the same code as) its proxy. This implies that persistent identity MUST be the basis for equals() and hashCode() for persistent object which entered the session through a read.

Whether an object has been persisted or not should not affect its equality or its hashcode. This implies that Java == must be the basis for equals() and hashCode() for objects which entered the session through a save, and had equals() or hashCode() called before being saved.


So here's a hack: the first time anybody calls equals() or hashCode(), use the ID if we have it, then remember what we used that first time so that the answer remains consistent:
Code:
public CatImpl extends Cat {
    private Long id;
    private Object equalityObject;
   
    private Object getEqualityObject() {
        if(equalityObject == null)
           equalityObject = (id == null) ? this : id;
        return equalityObject;
    }
   
    public boolean equals(Object that) {
        if(that instanceof CatImpl) {
            return ((CatImpl) that).getEqualityObject().equals(getEqualityObject());
        } else if (that instanceof Cat) {
            if(getEqualityObject() == this)
                throw new IllegalArgumentException(
                    "Unexpected proxy object:" + that.getClass());
            return ((Cat) that).getId().equals(getId());
        } else {
            return false;
        }
    }
   
    public int hashCode() {
        return getEqualityObject().hashCode();
    }
}


This hinges on the presumption that Hibernate will never return a proxy if the real object is already in the session. It also presumes that a proxy will just blithely use ID-based equality, whatever our equals() method does. Are these presumptions correct?


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 147 posts ]  Go to page 1, 2, 3, 4, 5 ... 10  Next

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.