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
@AfterMappingmethod:
@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
setBiDirectionalReviewsinBookand 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?