Hibernate throwing ConcurrentModificationException on bidirectional mapping

23 Views Asked by At

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.

0

There are 0 best solutions below