javaspringhibernatejpaentitymanager

EntityManager - ManyToMany child entity persistence in Join table


I have a Book entity with two child entities Author and Genre, each with a ManyToMany relationship and the corresponding join tables. Book class definitions follows:

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq")
    @GenericGenerator(name = "book_seq",
            strategy = "com.example.bookappjpa.dao.StringPrefixedSequenceIdGenerator",
            parameters = {
                    @org.hibernate.annotations.Parameter(name = StringPrefixedSequenceIdGenerator.INCREMENT_PARAM, value = "1"),
                    @org.hibernate.annotations.Parameter(name = StringPrefixedSequenceIdGenerator.VALUE_PREFIX_PARAMETER, value = "B"),
                    @org.hibernate.annotations.Parameter(name = StringPrefixedSequenceIdGenerator.NUMBER_FORMAT_PARAMETER, value = "%08d"),
            })
    private String id;

    @NaturalId
    private String title;

    @Column(name = "rank_no")
    private int rank;

    private int listId;

    private Double rating;

    private int pages;

    private LocalDate publishedOn;

    @ManyToMany()
    @JoinTable(name = "BOOK_AUTHOR", joinColumns = @JoinColumn(name = "BOOK_ID"), inverseJoinColumns = @JoinColumn(name = "AUTHOR_ID")
    )
    private Set<Author> authors = new HashSet<>();

    @ManyToMany
    @JoinTable(name = "BOOK_GENRE", joinColumns = @JoinColumn(name = "BOOK_ID"), inverseJoinColumns = @JoinColumn(name = "GENRE_ID"))
    private Set<Genre> genres = new HashSet<>();

    public void setTitle(String title) {
        this.title = title;
    }

    public void setRankNo(int rankNo) {
        this.rank = rankNo;
    }

    public void setListId(int listId) {
        this.listId = listId;
    }

    public void setRating(Double rating) {
        this.rating = rating;
    }

    public void setPages(int pages) {
        this.pages = pages;
    }

    public void setPublishedOn(LocalDate publishedOn) {
        this.publishedOn = publishedOn;
    }

    public boolean addAuthor(Author author) {
        return this.authors.add(author);
    }

    public boolean addGenre(Genre genre) {
        return this.genres.add(genre);
    }

}

Persistence method(looking up each entity to by its natural id to see if it already exists in the DB, only proceeding to persist if not. Excuse the half-baked code, yet to improve):

 @Transactional
    public void addBook(BookDTO book) {
        Session session = entityManager.unwrap(Session.class);
        Book b = session.bySimpleNaturalId(Book.class).load(book.getTitle());

        if (null == b) {
            Book bookToDB = new Book();
            bookToDB.setTitle(book.getTitle());
            bookToDB.setPages(book.getPages());
            bookToDB.setRating(book.getRating());
            bookToDB.setRankNo(book.getRank());
            bookToDB.setPublishedOn(book.getPublishedOn());

            book.getAuthors().forEach(a -> {
                Author authorToDB = new Author(a.getName(), a.getAge(), a.getNationality());
                if(null == session.bySimpleNaturalId(Author.class).load(a.getName())) {
                    authorToDB.addBook(bookToDB);
                    entityManager.persist(authorToDB);
                } 
            });

            book.getGenres().forEach(g -> {
                if(null == session.bySimpleNaturalId(Genre.class).load(g.getName())) {
                    Genre genreToDB = new Genre(g.getName());
                    genreToDB.addBook(bookToDB);
                    entityManager.persist(genreToDB);
                }
            });

            entityManager.persist(bookToDB);

        } else {
            String message = "Book with title " + book.getTitle() + " already exists!";
            log.info(message);
            throw new RuntimeException(message);
        }

        session.close();
    }

The problem with this approach is, if for instance, a certain book is persisted with a particular Author and Genre(s), and later on, we have another Book with the same Author and Genre(s), the persistence for the child entities will be skipped as per the code above and nothing populated in the join tables, indicating no relationship between the parent Book and child Author/Genre(s). Should I really be including the check above for the Author and Genre entities? If not, I'll end up with multiple entities in the corresponding entity tables with the same Author and Genre name, albeit with different ID sequence numbers(primary key). Not sure if that's good practice. Wanted to know if there's a sophisticated/effii approach to handle such a scenario in Production-grade code. Thanks in advance and apologies if its a rookie question

Two Book objects with the same author(the second one not making it to the join table BOOK_AUTHOR, as the author already exists in DB, and my DAO method will not allow persistence again in such a scenario):

{
    "rank": 8,
    "title": "Clap When You Land",
    "authors": [
      {
        "name": "Elizabeth Acevedo",
        "age": 0,
        "nationality": null
      }
    ],
    "rating": 0,
    "genres": [
      {

        "name": "Young Adult"
      },
      {

        "name": "Poetry"
      },
      {

        "name": "Contemporary"
      },
      {

        "name": "Fiction"
      },
      {

        "name": "Audiobook"
      },
      {

        "name": "LGBT"
      },
      {

        "name": "Realistic Fiction"
      }
    ],
    "pages": 432,
    "publishedOn": "2020-05-05",
    "listId": 1
  }


{
    "rank": 18,
    "title": "The Poet X",
    "authors": [
      {

        "name": "Elizabeth Acevedo",
        "age": 0,
        "nationality": null
      }
    ],
    "rating": 0,
    "genres": [
      {

        "name": "Poetry"
      },
      {

        "name": "Young Adult"
      },
      {

        "name": "Contemporary"
      },
      {

        "name": "Fiction"
      },
      {

        "name": "Audiobook"
      },
      {

        "name": "Realistic Fiction"
      },
      {

        "name": "Romance"
      }
    ],
    "pages": 368,
    "publishedOn": "2018-03-06",
    "listId": 1
  }

Solution

  • To keep relationships consistent and ensure that existing genres and authors are added to new books:

    Fetch the genre(s) and author(s) by their natural ids and do the following

    Your Genre and Author entities should include the following bidirectional relationship to books

    @Entity
    public class Genre {
    ...
       @ManyToMany(mappedBy = "genres")
       private Set<Book> books = new HashSet<>();
    ...
    }
    
    @Entity
    public class Author {
    ...
       @ManyToMany(mappedBy = "authors")
       private Set<Book> books = new HashSet<>();
    ...
    }
    

    and expand your addAuthor(...) and addGenre(...) methods in Book entity to ensure the bidirectional relationship is in sync when you add them

    @Entity
    public class Book {
    ...
        public boolean addAuthor(Author author) {
            author.getBooks().add(this); // ensures both side of relationship
            return this.authors.add(author);
        }
    
        public boolean addGenre(Genre genre) {
            genre.getBooks().add(this); // ensures both side of relationship
            return this.genres.add(genre);
        }
    ...
    }