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
}
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
if they exist, add them to book
if they don't exist, create them and add them to book
book.getAuthors().forEach(a -> {
Author author = session.bySimpleNaturalId(Author.class).load(a.getName());
if (author == null) {
author = new Author(a.getName(), a.getAge(), a.getNationality());
em.persist(author);
}
bookToDB.addAuthor(author);
});
book.getGenres().forEach(g -> {
Genre genre = session.bySimpleNaturalId(Genre.class).load(g.getName());
if (genre == null) {
genre = new Genre(g.getName());
em.persist(genre);
}
bookToDB.addGenre(genre);
});
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);
}
...
}