What is the best way to perform MapStruct mapping from a Data Transfer Object (DTO) to a Hibernate entity with a bi-directional one-to-many association?
Assume we have a BookDto
with multiple reviews of type ReviewDto
linked to it:
public class BookDto {
private List<ReviewDto> reviews;
// getter and setters...
}
The corresponding Hibernate entity Book
has a one-to-many association to Review
:
@Entity
public class Book {
@OneToMany(mappedBy = "book", orphanRemoval = true, cascade = CascadeType.ALL)
private List<Review> reviews = new ArrayList<>();
public void addReview(Review review) {
this.reviews.add(review);
review.setBook(this);
}
//...
}
@Entity
public class Review {
@ManyToOne(fetch = FetchType.LAZY)
private Book book;
public void setBook(Book book) {
this.book = book;
}
//...
}
Note that the book's addReview
method sets the association bi-directionally by also calling review.setBook(this)
as recommended by Hibernate experts (e.g., 'Hibernate Tips: How to map a bi-directional many-to-one association' by Thorben Janssen or 'How to synchronize bidirectional entity associations with JPA and Hibernate' by Vlad Mihalcea) in order to ensure Domain Model relationship consistency.
Now, we want a MapStruct mapper that automatically links the review back to the book. There are multiple options that I have found so far, each of which has some drawbacks:
- Custom mapping method:
@Mapper
public interface BookMapper {
default Book mapBookDtoToBook(BookDto bookDto) {
//...
for (ReviewDto reviewDto : bookDto.getReviews()) {
book.addReview(mapReviewDtoToReview(reviewDto));
}
//...
}
//...
}
This gets cumbersome if the book has many other fields to map. [Update: This can be simplified as suggested by Ben's answer.]
- Make the relationship bi-directional in an
@AfterMapping
method:
@Mapper
public interface BookMapper {
Book mapBookDtoToBook(Book book); // Implementation generated by MapStruct
@AfterMapping
void linkReviewsToBook(@MappingTarget Book book) {
for (Review review : book.getReviews()) {
review.setBook(book);
}
}
//...
}
This approach allows MapStruct to generate all other field mappings; but by decoupling the auto-generated setReviews
from the setBook
operation in the after-mapping, we lose cohesion.
- Add a method
setBiDirectionalReviews
inBook
and instruct MapStruct to use it as target:
@Entity
public class Book {
//...
public void setBiDirectionalReviews(List<Review> reviews) {
this.reviews = reviews;
for (Review review : this.reviews) {
review.setBook(this);
}
}
}
@Mapper
public class BookMapper {
@Mapping(source = "reviews", target = "biDirectionalReviews")
Book mapBookDtoToBook(Book book);
}
Now we have re-established cohesion, but (1) we might still need the additional method addReview
if we wanted to modify the existing reviews somewhere else, and (2) it feels somewhat hacky to abuse MapStruct's accessor naming strategy by pretending there were a field named "biDirectionalReviews".
Anyway, this is the best approach that I could find so far.
Is there a better solution to mapping bi-directional associations in MapStruct?