I want to override equals to be based on the business key instead of the object identity, but i cannot make it work with hibernate.
I know that similar questions has been asked and reading the answers (like this) helps understanding not breaking equivalence relation, use instanceof instead of getClass, use getters instead of field refs. etc., but my implementation of equals/hashCode still breaks hibernates population of the map.
I do not get an error, hibernate saves all 3 entities in a map correctly in the database, but it only reads 1 instead of all 3 when I read the entity with the map again. The sql logged by hibernate selects all 3 entities correct when it is executed in mysql.
The project is based on Spring 3.2.1, Hibernate 4.1.5 and JPA annotations
Update
When debugging hibernate it seems to call hashCode for each of the 3 map entities, but all values are null except the hibernate generated id. The hashCode calculated by the overridden method is therefore always the same. I guess that is why only one entity is added to the map. But i don't understand why hibernate calculates the hashCode before loading data from the database?
Example
In the example below I have reproduced the error in a project with 3 simple entities (all with hibernate generated ids):
- Item
- Language
- LanguageDetails
Item has a Map of LanguageDetails where Language is the key:
@OneToMany(mappedBy="item", orphanRemoval=true, cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@MapKey(name="language")
private Map<Language, LanguageDetails> languageDetails = new TreeMap<Language, LanguageDetails>();
The business key in Language is a String called code. My best guess of overriding equals has been to generate equals/hashCode using eclipse (and replace getClass with instanceof and field refs. with getters):
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getCode() == null) ? 0 : getCode().hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof Language))
return false;
Language other = (Language) obj;
if (getCode() == null) {
if (other.getCode() != null)
return false;
} else if (!getCode().equals(other.getCode()))
return false;
return true;
}
The following integration test fails with the above equals/hashCode and succeeds without:
Item item = new Item();
Language english = languageService.getByCode("en");
Language german = languageService.getByCode("de");
Language spanish = languageService.getByCode("es");
item.getLanguageDetails().put(english, new LanguageDetails(item, english, "ice cream"););
item.getLanguageDetails().put(spanish, new LanguageDetails(item, spanish, "helado"););
item.getLanguageDetails().put(german, new LanguageDetails(item, german, "eis"););
//save - stores all 3 languageDetails correct in mysql
int id = itemService.create(item);
//read - loads only 1 languageDetails (de)
Item readItem = itemService.getById(id);
//java.lang.AssertionError: expected:<3> but was:<1>
Assert.assertEquals(item.getLanguageDetails().size(), readItem.getLanguageDetails().size());
What am I doing wrong?