How to Map to Generic Type?
Asked Answered
C

2

9

PageInfoDto<T>:

public class PageInfoDto<T> {
    private int currentPageNum;
    private int totalPageNum;
    private int perPageNum;
    private int totalItemNum;
    private List<T> list;
}

Page<T>:

public class Page<T> {
    private int current;
    private int total;
    private int size;
    private int items;
    private List<T> list;
}

I have a school list and a student list.

I want to map Page<School> to PageInfoDto<SchoolDto> and map Page<Student> to PageInfoDto<StudentDto>

This is how my mapper looks like:

@Mapper(config = CentralConfig.class, uses = {StudentMapper.class, SchoolMapper.class}, componentModel = "spring")
public interface PageInfoMapper {
    @Mappings({
        @Mapping(target = "list", source = "pageInfo.records"),
        @Mapping(target = "currentPageNum", source = "pageInfo.current"),
        @Mapping(target = "totalPageNum", source = "pageInfo.pages"),
        @Mapping(target = "perPageNum", source = "pageInfo.size"),
        @Mapping(target = "totalItemNum", source = "pageInfo.total"),
    })
    PageInfoDto<SchoolDto> toPageInfoDto1(Page<School> pageInfo);

    @Mappings({
        @Mapping(target = "list", source = "pageInfo.records"),
        @Mapping(target = "currentPageNum", source = "pageInfo.current"),
        @Mapping(target = "totalPageNum", source = "pageInfo.pages"),
        @Mapping(target = "perPageNum", source = "pageInfo.size"),
        @Mapping(target = "totalItemNum", source = "pageInfo.total"),
    })
    PageInfoDto<StudentDto> toPageInfoDto2(Page<Student> pageInfo);
}

This is obviously not a clever way to do, having to repeat the same code. Is there a better way to do this?

Calumnious answered 5/8, 2019 at 3:30 Comment(0)
G
13

Mapstruct is a code generator. So it needs to know which types to construct in order to generate a method implementation. Having said that, you could do this smarter by using a base mapping method on which you define all the @Mapping annotations and ignore the generic type mapping. You still have the methods above but you just specify @InheritConfiguration

Alternatively you could consider playing around with an objectfactory using @TargetType to construct the proper generic type. I'm not sure whether that works with a generic mapping method signature. I'm not in the position to check it, but let me know if that works

E.g.

    public interface BasePageMapper<S, DTO> {

        @Mapping(target = "list", source = "records"),
        @Mapping(target = "currentPageNum", source = "current"),
        @Mapping(target = "totalPageNum", source = "pages"),
        @Mapping(target = "perPageNum", source = "size"),
        @Mapping(target = "totalItemNum", source = "total"),
        PageInfoDto<DTO> toPageInfoDto(Page<S> pageInfo);

        DTO toDto(S source);
    }

@Mapper( config = CentralConfig.class, uses = StudentMapper.class, componentModel = "spring")
    public interface StudentMapper extends BasePageMapper<Student, StudentDto> {

    }

@Mapper( config = CentralConfig.class, uses = SchoolMapper.class, componentModel = "spring")
    public interface SchoolMapper extends BasePageMapper<School, SchoolDto> {

    }
Geminate answered 5/8, 2019 at 16:29 Comment(5)
thanks。@InheritConfiguration makes the mapper look much better. but i still have to define lots of method. toPageInfoDto1 toPageInfoDto2 toPageInfoDto3 toPageInfoDto4…… how sad. And about the @TargetType,i dont know exactly how to do.Calumnious
We have a unit test that covers generic factories. Have a look here: github.com/mapstruct/mapstruct/blob/master/processor/src/test/…. it's not quite your scenario, and to be honest, I'm not sure at ask whether mapstruct can generate a method when it's signature contains type variables. It can select those, so you might go the other way around, so that might be an option as well.Geminate
So with the other way around I mean that you implement a generic default method with the signature you desire and then from that method call a non parameterized mapstruct method with all the mappings you desire, but ignoring the listGeminate
I've edited the answer and provided an example (from what I thought @Geminate was thinking)Soria
What if the toPageInfoDto of SchoolMapper need to add one more \@Mapping (School has on more field than others). how to achieve that since the Mapping annotation is not inheritable ?Require
Y
0

The best I could do is something like this for a base generic mapper:

public interface ParamMapper<E, D> {

    D toDto(E entity);
    List<D> listToDtoList(List<E> list);
    E toEntity(D dto);
    E updateEntity(@MappingTarget E entity, D dto);

}

And then your implementation of a concrete mapper should be as simple as this:

@Mapper
public interface ConcreteMapper extends ParamMapper<ConcreteEntity, ConcreteDTO> {

}

You can base on those to implement your case afterwards:

@Mapper(uses = ConcreteMapper.class)
public interface PageInfoMapper extends ParamMapper<Page<School>, PageInfo<School>> {
    
}

You'll need to specialize each variant fo your T generic for it to work. (I did not test the above example completely, but I'm using this scheme in my projects)

Also some caveats:

  • You can have @Mapping annotations on the generic mapper ParamMapper<E, D> but you should add @Mapper annotation on the concrete interface.
  • Also any imports, even if you don't override the method in the concrete interface should be added to the concrete interface
  • If you override any method (you can) you'll need to write all @Mapping annotations again in the concrete interface.
Yeti answered 17/5, 2024 at 12:29 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.