Reputation: 580
So I have an entity Book
public class Book {
private String id;
private String name;
private String description;
private Image coverImage;
private Set<Chapter> chapters;
//Sets & Gets
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book)) return false;
Book book = (Book) o;
return Objects.equals(name, book.name) &&
Objects.equals(description, book.description) &&
Objects.equals(image, book.image) &&
Objects.equals(chapters, book.chapters);
}
@Override
public int hashCode() {
return Objects.hash(name, description, image, chapters);
}
}
an entity Chapter
public class Chapter {
private String id;
private String title;
private String number;
private LocalDate releaseDate;
private Set<Distributor> distributors;
//Sets & Gets
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Chapter)) return false;
Chapter chapter = (Chapter) o;
return Objects.equals(title, chapter.title) &&
Objects.equals(number, chapter.number) &&
Objects.equals(releaseDate, chapter.releaseDate) &&
Objects.equals(distributors, chapter.distributors);
}
@Override
public int hashCode() {
return Objects.hash(title, number, releaseDate, distributors);
}
}
and a Distributor entity
public class Distributor {
private String id;
private String name;
private Image logoImage;
//Sets & Gets
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Distributor)) return false;
Distributor that = (Distributor) o;
return Objects.equals(name, that.name) &&
Objects.equals(logoImage, that.logoImage);
}
@Override
public int hashCode() {
return Objects.hash(name, logoImage);
}
}
I have a List<Chapter>
of old and new chapters and I have to add only the new ones to the Book.
Hibernate fetches and populates the Set<Chapter>
with all the chapters in the database using its custom implementation PersistentSet
.
The problem I'm trying to solve is add only those chapters from the List that are not present in the PersistentSet. For this I though, as Chapter does not uses the id field to calculate the hashCode/equals I could just add all the chapters from the List to the PersistentSet and the result should be a Set that excluded those from the list that already exists and included those that are not in the set. Well... this is not happening.
Hibernate's PersistentSet
is not using the hashCode/equals function I defined for the Distributor
entity but some internal implementation of it, resulting in the Chapters from the List and Set having different hashCodes and been not equals. Lets call a chapter from the List lChapter
and a chapter from the PersistentSet psChapter
and assume they are equals except for the Id.
If I do
lChapter.equals(psChapter); //True
but If I do
psChapter.equals(lChapter); //False
And if I do
book.getChapters().addAll(chapters);
Being book
an attached entity with 20 chapters and chapters
the List with 21 chapters the result is a set with 41 chapters.
I'm I doing something wrong here? I find it been a very trivial problem yet I haven't found any solution that doesn't involves me going through the List and check if its contained before adding. Its an unnecessary extra step that I can't afford.
Edit 1: Image is a custom implementation and does implements hashCode/equals and already proved its not the problem. Even if I remove from the entities the above experiments results doesn't change.
Edit 2: I debugged the code and when doing lChapter.equals(psChapter);
if you go into Objects.equals(distributors, chapter.distributors)
of the Chapter's equals function, it goes to the Distributor equals function whereas on the psChapter.equals(lChapter);
it goes into the PersistentSet one.
Upvotes: 2
Views: 2017
Reputation: 8257
UPDATE
After going through the hibernate JIRA PersistentSet does not honor hashcode/equals contract when loaded eagerly it is a known issue.
The collection loaded eagerly in some situations calls hashcode on its items before their field values are populated and thus the other methods like contain(), remove() etc are impacted.
The fix is planned for 6.0.0 Alpha.
And as per one of the suggestions in the JIRA as a workaround, it's much better to stick to LAZY collections. EAGER fetching is bad for performance, can lead to Cartesian Products, and you can't paginate the collection.
And that should explain why
Chapter.equals(psChapter);
returns true
since it uses normal Set.equals
psChapter.equals(lChapter);
returns false
.
This goes via PersistentSet while violates the hashcode/equals contract and thus it is not guaranteed to return true even if the element is present in the Set. And further it results in allowing adding duplicate elements to the Set as well.
Upvotes: 1
Reputation: 344
A workaround for this issue is to add an @OrderColumn
to the relationship between Book and Chapter like this:
public class Book {
private String id;
private String name;
private String description;
private Image coverImage;
@OrderColumn
private Set<Chapter> chapters;
...
}
This will get rid of the PersistentSet
and instead use the PersistentSortedSet
of Hibernate which does not violate the equals/hashCode contract.
By the way I was wondering why you don't have any @OneToMany
/ @ManyToMany
annotations? Seems Hibernate does it automatically (which I find creepy). How does Hibernate decide whether it's a one-to-many or many-to-many relationship?
Upvotes: 2
Reputation: 1722
I tried your code with main method, and it is working as expected, as below
Could you please try with your code ?
public static void main(String[] args) {
LocalDate now = LocalDate.now();
Distributor db = new Distributor();
db.setId("1");
db.setLogoImage(null);
db.setName("Name");
Set<Distributor> dbs = new HashSet<>();
dbs.add(db);
Chapter c= new Chapter();
c.setDistributors(dbs);
c.setId("1");
c.setNumber("123");
c.setReleaseDate(now);
c.setTitle("10:30");
Set<Chapter> chapters = new HashSet<>();
chapters.add(c);
Book b= new Book();
b.setChapters(chapters);
b.setCoverImage(null);
b.setDescription("Description");
b.setId("1");
b.setName("Name");
Set<Distributor> dbs1 = new HashSet<>();
Distributor db1 = new Distributor();
db1.setId("1");
db1.setLogoImage(null);
db1.setName("Name");
dbs1.add(db1);
Chapter c1 = new Chapter();
c1.setDistributors(dbs1);
c1.setId("1");
c1.setNumber("123");
c1.setReleaseDate(now);
c1.setTitle("10:30");
System.out.println(chapters.add(c1));
System.out.println(chapters.size());
}
Upvotes: 0