I am using MapStruct to map from a JPA entity to a POJO DTO, in a Spring app with dependency injection.
I have added some additional processing of the DTO to a method in a decorator as specified in the doc.
It works fine for mapping a single entity. But I also have a mapping for a collection (set) of these entities and the method is called automatically when a collection of those entities is found in a relationship.
However the generated collection mapping method does not use the decorated method to map each entity, is just uses the "vanilla" generated method on the delegate. Here is the code of the generated method :
@Override
public Set<DimensionItemTreeDTO> missionSetToTreeDtoSet(Set<Mission> set) {
return delegate.missionSetToTreeDtoSet( set );
}
The delegate method itself is not aware of the decorator and calls the individual item mapping method on itself :
@Override
public Set<DimensionItemTreeDTO> missionSetToTreeDtoSet(Set<Mission> set) {
if ( set == null ) {
return null;
}
Set<DimensionItemTreeDTO> set__ = new HashSet<DimensionItemTreeDTO>();
for ( Mission mission : set ) {
set__.add( missionToTreeDto( mission ) ); //here the decorator is not called !
}
return set__;
}
...and the decorated method is never called for the items inside the collection.
Is there a way I can make Mapstruct use the decorator method in the collection mappings, short of writing the collection method manually in my decorator (which works but is verbose and defeats the purpose of having MapStruct in the first place which is not to have to write this kind of code) ?
In general, mapping collections with MapStruct works the same way as for simple types. Basically, we have to create a simple interface or abstract class, and declare the mapping methods. Based on our declarations, MapStruct will generate the mapping code automatically.
Annotation Type MappingTargetDeclares a parameter of a mapping method to be the target of the mapping. Not more than one parameter can be declared as MappingTarget . NOTE: The parameter passed as a mapping target must not be null .
Enclosing class: MappingConstants public static final class MappingConstants.ComponentModel extends Object. Specifies the component model constants to which the generated mapper should adhere. It can be used with the annotation Mapper.componentModel() or MapperConfig.componentModel()
We can ignore unmapped properties in several mappers by setting the unmappedTargetPolicy via @MapperConfig to share a setting across several mappers. We should note that this is a simple example showing the minimal usage of @MapperConfig, which might not seem much better than setting the policy on each mapper.
I found the solution to my problem : actually my use case was better suited for the MapStruct @AfterMapping methods, I used it and it is now working fine for all cases :
@Mapper
public abstract class ConstraintsPostProcessor {
@Inject
private UserService userService; // can use normal Spring DI here
@AfterMapping
public void setConstraintsOnMissionTreeDTO(Mission mission, @MappingTarget MissionDTO dto){ // do not forget the @MappingTarget annotation or it will not work
dto.setUser(userService.getCurrentUser()); // can do any additional logic here, using services etc.
}
}
And in the main mapper :
@Mapper(uses = {ConstraintsPostProcessor.class}) // just add the previous class here in the uses attribute
public interface DimensionMapper {
...
}
You should create another decorator for sub object. Suppose you have Book class which has one Author as sub object.
public class Book {
private int id;
private Author author;
getters and setters
}
public class BookDto {
private int bookId;
private AuthorDto author;
getters and setters
}
public class Author {
private int id;
private String name;
getters and setters
}
public class AuthorDto {
private int authorId;
private String name;
getters and setters
}
And we create BookMapper and BookMapperDecorator
@Mapper(componentModel = "spring", uses = AuthorMapper.class)
@DecoratedWith(BookMapperDecorator.class)
public interface BookMapper {
@Mapping(target = "bookId", ignore = true)
BookDto toDto(Book book);
}
public abstract class BookMapperDecorator implements BookMapper{
@Autowired
@Qualifier("delegate")
private BookMapper delegate;
@Override
public BookDto toDto(Book book) {
BookDto bookDto = delegate.toDto(book);
bookDto.setBookId(book.getId());
return bookDto;
}
}
And also AuthorMapper and AuthorMapperDecorator
@Mapper(componentModel = "spring")
@DecoratedWith(AuthorMapperDecorator.class)
public interface AuthorMapper {
@Mapping(target = "authorId", source = "id")
@Mapping(target = "name", ignore = true)
AuthorDto toDto(Author author);
}
public abstract class AuthorMapperDecorator implements AuthorMapper{
@Autowired
@Qualifier("delegate")
private AuthorMapper delegate;
@Override
public AuthorDto toDto(Author author) {
AuthorDto authorDto = delegate.toDto(author);
authorDto.setName(author.getName().toUpperCase());
return authorDto;
}
}
The important parts here are uses = AuthorMapper.class (which says that one mapper uses another one and you don't have to include Author parsing method in BookMapper)
componentModel = "spring" (Which is responsible for bean injection and understands the @Qualifier("delegate") )
You can also ignore one field in mapper and set it in decorator as we did with bookId
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With