Search code examples
javahibernatespring-datadtomapstruct

Single Item from Entity Collection into Nested Dto


I have a entity Author that has a collection of Books with property publishedDate.

Each Author has a collection books and they are mapped like so:

public class Author {
    @Id
    private Long id;

    private String name;

    @JsonBackReference
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "author")
    private List<Book> listBook = new ArrayList<>();

    // other fields
    // getters and setters

Book

public class Book {

    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.EAGER, optional = false)
    private Author author;    

    private LocalDateTime publishedDateTime;

I would like to get a Author By Id with the newest Book as a Dto, i.e.

@Data
public AuthorNewestBookDto {

    Long authorId;
    String name;
   
    Book newestBook;
  1. DtoProjection

Cast the results of the query directly into the Dto. No mapper required. Efficient transaction on database. Similar to here.

  1. Mapper

Use a mapstruct mapper to return an Author entity with a collection of books and use the @Before annotation to return only the single newest book from the collection. Similar to here.

Is there a better way to do this conversion to AuthorNewestBookDto where I need to retrieve the single newest member of a collection into a nested singular Dto.

I have considered if I could do it in hibernate stage with a DtoResultTransformer (Hibernate 6).

Hopefully this is not too opinionated for Stack Overflow.


Solution

  • MapStruct has multiple tools for situations like this:

    1. Qualifier

    We can provide custom mapping logic between Entity field type: List<Book> and DTO field type Book:

    interface BookingMapping {
        @Mapping(target = "newestBook", source = "listBook", qualifiedBy=NewestBookQualifier.class)
        //.. other fields
        AuthorNewestBookDto toDtoQualifiedBy(Author entity);
    }
    ...
    public class BookingMappingUtil { 
        @NewestBookQualifier
        BookDto toNewestBook(List<Book> listBook) {
            return listBook.stream()
                 .max(Comparator.comparing(Book::getPublishedDateTime))
                 .orElse(null)
    }
    ...
    @Qualifier
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface NewestBookQualifier{
    }
    

    Iterable to non-iterable example (official MapStruct-Example project).

    2. @Named

    In this case, we don't need to create a new annotation. Named-annotation documentation

    @Mapping(target = "newestBook", source = "listBook", qualifiedByName="toNewestBook" )
    //.. other fields
    AuthorNewestBookDto toDtoQualifiedByName(Author entity);
    
    @Named("toNewestBook")
    default BookDto toNewestBook(List<Book> listBook) {
        return listBook.stream()
                 .max(Comparator.comparing(Book::getPublishedDateTime))
                 .orElse(null)
    }
    

    3. @Mapping(.. expression=".." ..)

    We are able to store business logic as a String. This approach can be not readable but very easy to implement. expressions documentation

    @Mapping(target = "newestBook", 
              source = "listBook", 
              expression = "java( listBook.stream().max(java.util.Comparator.comparing(Book::getPublishedDateTime)).orElse(null) )" )
    //.. other fields
    AuthorNewestBookDto toDtoQualifiedByName(Author entity);