Hibernate version:
hibernate-3.2.5.ga
hibernate-annotations-3.3.0.ga
I have a Map<Entity, Entity> where the key entity uses a business key to implement equals()/hashCode() (as per
http://www.hibernate.org/109.html best practices).
However, when the Map is initialized the key entity does not yet have its fields loaded (only the id is available) and thus the equals/hashCode methods return the same values for all keys resulting in a map that contains a single entry. If the id is used then the code works fine. Is this intended behaviour and what is the best approach if so?
Test Case:
Code:
@Entity
public class Reviewer implements Serializable {
private Long id;
private Map<Document, Review> reviews;
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
//@CollectionOfElements
@ManyToMany
@MapKeyManyToMany
public Map<Document, Review> getReviews() { return reviews; }
public void setReviews(Map<Document, Review> reviews) { this.reviews = reviews; }
public void addReview(Review review) {
if (reviews == null) reviews = new HashMap<Document, Review>();
reviews.put(review.getDocument(), review);
}
}
Code:
@Entity
public class Document implements Serializable {
private Long id;
private String url;
public Document() {}
public Document(String url) { this.url = url; }
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Document)) return false;
return new EqualsBuilder().append(url, ((Document) obj).url).isEquals();
// works:
//return new EqualsBuilder().append(id, ((Document) obj).id).isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(url).toHashCode();
//return new HashCodeBuilder().append(id).toHashCode();
}
}
Code:
@Entity
public class Review implements Serializable {
private Long id;
private Document document;
public Review() {}
public Review(Document document) {
this.document = document;
}
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
@ManyToOne
public Document getDocument() { return document; }
public void setDocument(Document document) { this.document = document; }
}
Code:
public class TestAddToMap extends TestCase {
public void testAddToMap() {
SessionFactory sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory();
Session session = sessionFactory.getCurrentSession();
session.beginTransaction();
Document doc1 = new Document("http://myurl/document/1");
session.save(doc1);
Document doc2 = new Document("http://myurl/document/2");
session.save(doc2);
Reviewer reviewer = new Reviewer();
session.save(reviewer);
Review review = new Review(doc1);
session.save(review);
reviewer.addReview(review);
review = new Review(doc2);
session.save(review);
reviewer.addReview(review);
assertEquals(2, reviewer.getReviews().size());
session.getTransaction().commit();
session = sessionFactory.getCurrentSession();
session.beginTransaction();
Reviewer reviewer2 = (Reviewer) session.get(Reviewer.class, new Long(reviewer.getId()));
assertEquals(2, reviewer2.getReviews().size()); // fails
session.getTransaction().commit();
}
}
Stack trace at point where hashCode() is called with only id availableCode:
TestAddToMap [JUnit]
org.eclipse.jdt.internal.junit.runner.RemoteTestRunner at localhost:3109
Thread [main] (Suspended (breakpoint at line 38 in Document))
Document.hashCode() line: 38
HashMap<K,V>.put(K, V) line: 418
PersistentMap.readFrom(ResultSet, CollectionPersister, CollectionAliases, Object) line: 259
BasicCollectionLoader(Loader).readCollectionElement(Object, Serializable, CollectionPersister, CollectionAliases, ResultSet, SessionImplementor) line: 1008
BasicCollectionLoader(Loader).readCollectionElements(Object[], ResultSet, SessionImplementor) line: 646
BasicCollectionLoader(Loader).getRowFromResultSet(ResultSet, SessionImplementor, QueryParameters, LockMode[], EntityKey, List, EntityKey[], boolean) line: 591
BasicCollectionLoader(Loader).doQuery(SessionImplementor, QueryParameters, boolean) line: 701
BasicCollectionLoader(Loader).doQueryAndInitializeNonLazyCollections(SessionImplementor, QueryParameters, boolean) line: 236
BasicCollectionLoader(Loader).loadCollection(SessionImplementor, Serializable, Type) line: 1994
BasicCollectionLoader(CollectionLoader).initialize(Serializable, SessionImplementor) line: 36
BasicCollectionPersister(AbstractCollectionPersister).initialize(Serializable, SessionImplementor) line: 565
DefaultInitializeCollectionEventListener.onInitializeCollection(InitializeCollectionEvent) line: 60
SessionImpl.initializeCollection(PersistentCollection, boolean) line: 1716
PersistentMap(AbstractPersistentCollection).initialize(boolean) line: 344
PersistentMap(AbstractPersistentCollection).read() line: 86
PersistentMap(AbstractPersistentCollection).readSize() line: 109
PersistentMap.size() line: 114
TestAddToMap.testAddToMap() line: 38
NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]
NativeMethodAccessorImpl.invoke(Object, Object[]) line: 39
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25
Method.invoke(Object, Object...) line: 585
TestAddToMap(TestCase).runTest() line: 164
TestAddToMap(TestCase).runBare() line: 130
TestResult$1.protect() line: 106
TestResult.runProtected(Test, Protectable) line: 124
TestResult.run(TestCase) line: 109
TestAddToMap(TestCase).run(TestResult) line: 120
TestSuite.runTest(Test, TestResult) line: 230
TestSuite.run(TestResult) line: 225
JUnit3TestReference.run(TestExecution) line: 128
TestExecution.run(ITestReference[]) line: 38
RemoteTestRunner.runTests(String[], String, TestExecution) line: 460
RemoteTestRunner.runTests(TestExecution) line: 673
RemoteTestRunner.run() line: 386
RemoteTestRunner.main(String[]) line: 196