I am writing a Song app using Java SE and Hibernate 6.2.7.Final and I am running into a puzzling ConcurrentModificationException. I've seen other people run into this exception while removing elements from a persistence-managed collection, but this does not seem to be the case in my example.
In this application, a song can be written by multiple authors, and an author can write one and only one song. I am using a bidirectional Many-to-One mapping from Author to Song to represent the relationship. The Author entity is the owning side of the relationship.
@Entity
public class Song {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "song", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private Set<Author> authors = new HashSet<>();
// Getters and setters omitted for brevity
}
@Entity
public class Author {
@EmbeddedId
private SongAuthorId id;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("songId")
private Song song;
@Override
public boolean equals(Object other) {
if (other == this)
return true;
if (!(other instanceof Author otherAuthor))
return false;
return Objects.equals(id, otherAuthor.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
// Getters and setters omitted for brevity
}
@Embeddable
public class SongAuthorId implements Serializable {
private static final long serialVersionUID = 5903710918709877615L;
@Column(name = "song_id")
private Long songId;
@Column(name = "author_name")
private String authorName;
@Override
public boolean equals(Object other) {
if (other == this)
return true;
if (!(other instanceof SongAuthorId otherId))
return false;
return Objects.equals(songId, otherId.songId)
&& Objects.equals(authorName, otherId.authorName);
}
@Override
public int hashCode() {
return Objects.hash(songId, authorName);
}
// Getters and setters omitted for brevity
}
Since an Author cannot exist without a Song, I use both the author name and the song id as primary keys for the Author entity.
To update an existing song and its authors, I wrote the following :
private void simpleUpdate() {
// get EntityManagerFactory from context
EntityManager em = AppContext.getContext().getEntityManagerFactory().createEntityManager();
long existingSongId = 1L;
Song newSong = new Song();
newSong.setId(existingSongId);
// Setting first author
var authorId1 = new SongAuthorId();
authorId1.setAuthorName("John");
authorId1.setSongId(existingSongId); // not sure if necessary with @MapsId
var songAuthor1 = new Author();
songAuthor1.setId(authorId1);
songAuthor1.setSong(newSong);
// Setting second author
var authorId2 = new SongAuthorId();
authorId2.setAuthorName("Marc");
authorId2.setSongId(existingSongId); // not sure if necessary with @MapsId
var songAuthor2 = new Author();
songAuthor2.setId(authorId2);
songAuthor2.setSong(newSong);
newSong.getAuthors().add(songAuthor1);
newSong.getAuthors().add(songAuthor2);
em.getTransaction().begin();
Song oldSong = em.find(Song.class, newSong.getId());
if (null == oldSong) {
return;
}
Set<Author> newAuthors = newSong.getAuthors();
oldSong.getAuthors().retainAll(newAuthors);
oldSong.getAuthors().addAll(newAuthors);
oldSong.getAuthors().forEach(author -> author.getId().setSongId(oldSong.getId())); // --> works
oldSong.getAuthors().forEach(author -> author.setSong(oldSong)); // --> does not work
em.getTransaction().commit();
em.close();
}
When setting the reference from Author to Song, I run into a ConcurrentModificationException.
This only occurs when some of the updated song's authors primary key already exist, i.e : if Marc or John are already linked to the song with id = 1L.
I have tried using an iterable instead of a forEach() but the problem remains.
If I skip oldSong.getAuthors().forEach(author -> author.setSong(oldSong)); altogether, the entity is updated properly but, from what I've read, omitting the reference from Author to Song does not seem to be recommended in a bidirectionnal mapping as it may leave the entities in an inconsistent state.
By the way, I know I could use a unidirectional One-to-Many for this application, but according to the blog post https://vladmihalcea.com/the-best-way-to-map-a-onetomany-association-with-jpa-and-hibernate/ , bidirectional many-to-one may be better in terms of performance.