Hi Hibernate community,
I usually use Vlad Mihalcea, and Thorben Janssen blogs for reference and Hibernate tips. While both have a blog explaining how bidirectional is done. The examples ( How to synchronize bidirectional entity associations with JPA and Hibernate - Vlad Mihalcea , and Hibernate Tips: How to map a bidirectional many-to-one association ) suggest bi-syncing at the call site (syncing where calling the main setter). While this work, it becomes a requirement which is easy to forget about, or newer devs coming to the project may not know it (which happened to me when I started working with Hiberante). So it’s better to keep this syncing logic at the entity level. Hibernate enhancement tool used to do this, but it’s now deprecated (I mean the bi-sync feature), probably for good reasons. It also caused some trouble in some cases, like the following: OneToMany bi-directional save throws java.lang.IncompatibleClassChangeError: Class org.hibernate.collection.internal.PersistentMap does not implement the requested interface java.util.Collection
When implementing entity-level bi-sycning, I faced various bugs (unexpected Hibernate exceptions) like throwing an exception when a collection is marked with orphanRemoval = true, or loading an entire collection just to set the entity syncing.
Anyway, I made many versions, and each time I ask AI tools about it, they start praising it as if I created a patent, then shortly, I find that it’s missing something or buggy. Anyway, this is so far my final version after so long. I just want to ask Hibernate veterans here if this pattern is good or if it’s overly complex and over-engineered. Also, side question, many (on reddit) suggest avoiding bi-directional mappings and use separate repositories to fetch/insert items to avoid a lot of the bi-directional quirks, while I see that the Hiberante team is investing a lot in it with various non-JPA annotations to enhance and make bi-directional less source of problems.
Anyway, this is how I implement bi-directional sync :
@Entity
public class Serie {
@Id
@GeneratedValue
private UUID id;
private String name;
@OneToMany(mappedBy="serie", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private Set<Episode> episodes = new HashSet<>();
public Serie setEpisodes(Set<Episode> episodes) {
if (episodes == null) {
for (var e : new HashSet<>(this.episodes)) {
e.setSerie(null);
}
this.episodes.clear();
return this;
}
new HashSet<>(this.episodes).stream()
.filter(e -> !episodes.contain(e))
.forEach(this::removeEpisode);
episodes.forEach(this::addEpisode);
return this;
}
public Serie addEpisode(@Nonnull Episode episode) {
Assert.notNull(episode, "episode must not be null");
episode.setSerie(this);
return this;
}
public Serie removeEpisode(@Nonnull Episode episode) {
Assert.notNull(episode, "episode must not be null");
episode.setSerie(null);
return this;
}
// This implementation suggested by Georgii Vlasov and auto generated by JPA Buddy plugin
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
if (thisEffectiveClass != oEffectiveClass) return false;
Serie serie= (Serie) o;
return getId() != null && Objects.equals(getId(), serie.getId());
}
@Override
public final int hashCode() {
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
}
}
@Entity
public class Episode {
@Id
@GeneratedValue
private UUID id;
@ManyToOne(fetch = FetchType.LAZY)
private Serie serie;
public Episode setSerie(Serie serie) {
if (this.serie == serie) return this;
if (this.serie != null && Hibernate.isInitialized(this.serie.getEpisodes()))
this.serie.getEpisodes().remove(this);
this.serie = serie;
if (this.serie != null && Hibernate.isInitialized(this.serie.getEpisodes()))
this.serie.getEpisodes().add(this);
return this;
}
// same equals/hashCode implementation
}