It turns out that the issue was not in the Hibernate annotations. Instead, the issue resided in how I was accessing the collections in the annotated methods.
As you can see in the question, the Collection setters clear the collection and then add any items in the new collection. The updated code (which works!) is shown here:
@ManyToMany(targetEntity = AbstractEntity.class,
fetch = FetchType.LAZY)
@JoinTable(name = "conflict_affected_entity",
joinColumns = { @JoinColumn(name = "conflict_id", referencedColumnName = "id") },
inverseJoinColumns = { @JoinColumn(name = "affected_entity_id", referencedColumnName = "id") })
public Set<AbstractEntity> getAffectedEntities()
{
Hibernate.initialize(affectedEntities);
return affectedEntities;
}
public void setAffectedEntities(Set<AbstractEntity> affectedEntities)
{
this.affectedEntities = affectedEntities;
}
and
@ManyToMany(targetEntity = Conflict.class,
fetch = FetchType.LAZY)
@JoinTable(name = "conflict_affected_entity",
joinColumns = { @JoinColumn(name = "affected_entity_id", referencedColumnName = "id") },
inverseJoinColumns = { @JoinColumn(name = "conflict_id", referencedColumnName = "id") })
public Set<Conflict> getConflicts()
{
Hibernate.initialize(conflicts);
return conflicts;
}
public void setConflicts(Set<Conflict> conflicts)
{
this.conflicts = conflicts;
}
For future viewers, this Hibernate configuration (mapping each side as a ManyToMany) creates two uni-directional associations: from Conflict -> AbstractEntities and from AbstractEntity -> Conflicts. This means that if you decide to use this configuration, you will have to be careful when adding or deleting items from the collections to make sure the joining table entries get updated to avoid foreign key constraint violations. For example, when deleting a Conflict, we can't just say ConflictDAO.getInstance.delete(toDelete)
. Instead, we have to make sure the Conflict doesn't retain any associations:
for (AbstractEntity affectedEntity : toDelete.getAffectedEntities()) {
notifications.add(Notification.forUsersWithAccess(ActionType.UPDATE, affectedEntity));
// Forcefully remove the associations from the affectedEntity to the Conflict, since we don't want to risk using CascadeType.DELETE
affectedEntity.getConflicts().remove(toDelete);
}
ConflictDAO.getInstance().delete(toDelete);