Reputation: 18780
I'm trying to figure out what's wrong with this approach, given my particular usage patterns:
@Entity
public class DomainObject {
@Id // + sequence generator
private Long id;
@Override
public boolean equals(Object o) {
// bunch of other checks omitted for clarity
if (id != null) {
return id.equals(o.getId());
}
return super.equals(o);
}
@Override
public int hashCode() {
if (id != null) {
return id.hashCode();
}
return super.hashCode();
}
I've read several posts on the subject and it sounds like you don't want to use a DB-generated sequence value in equals/hashCode because they won't be set until the objects are persisted and you don't want disparate transient instances to all be equal, or the persistence layer itself might break.
But is there anything wrong with falling back to the default Object equals/hashCode (instance equality) for transient objects and then using the generated @Id when you have it?
The worst thing I can think of is, a transient object can't ever be equal to a persistent object, which is fine in my use case - the only time I'm ever putting objects in collections and want contains
to work, all the objects are already persistent and all have IDs.
However, I feel like there's something else wrong in a really subtle, non-obvious way deep in the persistence layer but I can't quite figure out what.
The other options don't seem that appealing either:
doing nothing and living with instance equality (default Object.equals): works great for most of my entities, but getting tired of workarounds for the handful of cases when I want a collection with a mix of detached entities (e.g., session scope) and "live" ones from current transaction
using a business key: I have clear natural keys but they're mutable, and this would have some of the same problems as above (hashCode stability if object changes)
using a UUID - I know that will work, but feels wrong to pollute the DB with artifacts to support java.util collections.
See also:
Upvotes: 4
Views: 4590
Reputation: 153960
Yes, you can! But you have to be careful that the hashCode
implementation always returns the same constant value as explained in this post:
@Entity
public class Book implements Identifiable<Long> {
@Id
@GeneratedValue
private Long id;
private String title;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book)) return false;
Book book = (Book) o;
return Objects.equals(getId(), book.getId());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
//Getters and setters omitted for brevity
}
This is the only way you can ensure that equals and hashCode are consistent across all entity state transitions.
Upvotes: 2
Reputation: 4952
If you're sure you don't ever need to add an unpersisted entity to a Set or Map key, you can use the ID to test for equality and as the hash code. But if you do this, you could enforce it by throwing an Exception for an unpersisted object:
@Entity
public class DomainObject {
@Id // + sequence generator
private Long id;
@Override
public boolean equals(Object that) {
// bunch of other checks omitted for clarity
if (id != null) {
throw new IllegalStateException("equals() before persisting");
}
if (this == that) {
return true;
}
if (that instanceof DomainObject) {
return id.equals(((DomainObject)that).id);
}
}
@Override
public int hashCode() {
if (id != null) {
throw new IllegalStateException("hashCode() before persisting");
}
return id;
}
}
If you do this, you may see surprise exceptions, where you didn't realize you were relying on these methods on unpersisted objects. You may find this helpful in debugging. You might also find it makes your existing code unusable. Either way, you'll be clearer about how your code works.
One thing you should never do is return a constant for the hash code.
public int hashCode() { return 5; } // Don't ever do this!
Technically, it fulfills the contract, but it's a terrible implementation. Just read the javadocs for Object.hashCode(): …producing distinct integer results for unequal objects may improve the performance of hash tables. (The word "may" here is a serious understatement.)
Upvotes: 0
Reputation: 70574
The javadoc of Map writes:
Note: great care must be exercised if mutable objects are used as map keys. The behavior of a map is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is a key in the map.
Whenever an object is persisted, your implementation changes the meaning of equals. As such, any Collections containing that object need no longer work right. In particular, changing the hashcode of an object used as key in a HashMap (or contained in HashSet) is likely to result in future lookups on that Map (Set) not finding the object, and adding that object again to the Map (Set) is likely to succeed, even though under ordinary circumstances, a Map may contain at most one mapping for every given key, and a Set contain every object at most once.
Since it is common to store entities in collections (to express ToMany-associations), that flaw is likely to result in actual hard-to-find bugs.
I therefore strongly recommend against implementing hashcode based on database-generated identifiers.
Upvotes: 2