ModelMapper: round trip

    image

    For known reasons, the backend cannot give data from the repository as is. The most famous - the essential dependencies are taken from the base in a form not in which the front can understand them. Here you can add the difficulties with parsing enum (if the enum fields contain additional parameters), and many other difficulties that arise during the automatic casting of types (or the impossibility of their automatic casting). Hence the need to use a Data Transfer Object - DTO, understandable for both back and front.
    Converting an entity into a DTO can be solved in different ways. You can use the library, you can (if the project is small) nakolhozit something like this:

    @ComponentpublicclassItemMapperImplimplementsItemMapper{
        privatefinal OrderRepository orderRepository;
        @AutowiredpublicItemMapperImpl(OrderRepository orderRepository){
            this.orderRepository = orderRepository;
        }
        @Overridepublic Item toEntity(ItemDto dto){
            returnnew Item(
                    dto.getId(),
                    obtainOrder(dto.getOrderId()),
                    dto.getArticle(),
                    dto.getName(),
                    dto.getDisplayName(),
                    dto.getWeight(),
                    dto.getCost(),
                    dto.getEstimatedCost(),
                    dto.getQuantity(),
                    dto.getBarcode(),
                    dto.getType()
            );
        }
        @Overridepublic ItemDto toDto(Item item){
            returnnew ItemDto(
                    item.getId(),
                    obtainOrderId(item),
                    item.getArticle(),
                    item.getName(),
                    item.getDisplayName(),
                    item.getWeight(),
                    item.getCost(),
                    item.getEstimatedCost(),
                    item.getQuantity(),
                    item.getBarcode(),
                    item.getType()
            );
        }
        private Long obtainOrderId(Item item){
            return Objects.nonNull(item.getOrder()) ? item.getOrder().getId() : null;
        }
        private Order obtainOrder(Long orderId){
            return Objects.nonNull(orderId) ? orderRepository.findById(orderId).orElse(null) : null;
        }
    }

    Such samopisny mappers have obvious disadvantages:

    1. Do not scale.
    2. When you add / remove even the smallest field, you will have to edit the mapper.

    Therefore, the correct solution is to use the library-mapper. I know modelmapper and mapstruct. Since I worked with modelmapper, I will talk about it, but if you, my reader, are well acquainted with mapstruct and can tell you about all the intricacies of its application, write about this, please, an article, and I will first write it (I also have this very interesting, but there is no time to enter it).

    So, modelmapper.

    I just want to say that if something is not clear to you, you can download a finished project with a working test, a link at the end of the article.

    The first step is, of course, adding a dependency. I use gradle, but it’s easy for you to add a dependency to a maven project.

    compile group: 'org.modelmapper', name: 'modelmapper', version: '2.3.2'

    This is enough to make the mapper work. Next, we need to create a bin.

    @Beanpublic ModelMapper modelMapper(){
            ModelMapper mapper = new ModelMapper();
            mapper.getConfiguration()
                    .setMatchingStrategy(MatchingStrategies.STRICT)
                    .setFieldMatchingEnabled(true)
                    .setSkipNullEnabled(true)
                    .setFieldAccessLevel(PRIVATE);
            return mapper;
        }

    Usually it is enough just to return new ModelMapper, but it will be good to customize the mapper for our needs I set a strict matching strategy, enabled mapping of fields, skipping null fields, and set a private level of access to fields.

    Next, create the following entity structure. We will have a unicorn (Unicorn), which will have a certain number of droids (Droids), and each droid will have a certain number of cupcakes (Cupcake).

    Entities
    Абстрактный родитель:

    @MappedSuperclass@Setter@EqualsAndHashCode@NoArgsConstructor@AllArgsConstructorpublicabstractclassAbstractEntityimplementsSerializable{
        Long id;
        LocalDateTime created;
        LocalDateTime updated;
        @Id@GeneratedValuepublic Long getId(){
            return id;
        }
        @Column(name = "created", updatable = false)
        public LocalDateTime getCreated(){
            return created;
        }
        @Column(name = "updated", insertable = false)
        public LocalDateTime getUpdated(){
            return updated;
        }
        @PrePersistpublicvoidtoCreate(){
            setCreated(LocalDateTime.now());
        }
        @PreUpdatepublicvoidtoUpdate(){
            setUpdated(LocalDateTime.now());
        }
    }

    Unicorn:

    @Entity@Table(name = "unicorns")
    @EqualsAndHashCode(callSuper = false)
    @Setter@AllArgsConstructor@NoArgsConstructorpublicclassUnicornextendsAbstractEntity{
        private String name;
        private List<Droid> droids;
        private Color color;
        publicUnicorn(String name, Color color){
            this.name = name;
            this.color = color;
        }
        @Column(name = "name")
        public String getName(){
            return name;
        }
        @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "unicorn")
        public List<Droid> getDroids(){
            return droids;
        }
        @Column(name = "color")
        public Color getColor(){
            return color;
        }
    }

    Droid:

    @Setter@EqualsAndHashCode(callSuper = false)
    @Entity@Table(name = "droids")
    @AllArgsConstructor@NoArgsConstructorpublicclassDroidextendsAbstractEntity{
        private String name;
        private Unicorn unicorn;
        private List<Cupcake> cupcakes;
        private Boolean alive;
        publicDroid(String name, Unicorn unicorn, Boolean alive){
            this.name = name;
            this.unicorn = unicorn;
            this.alive = alive;
        }
        publicDroid(String name, Boolean alive){
            this.name = name;
            this.alive = alive;
        }
        @Column(name = "name")
        public String getName(){
            return name;
        }
        @ManyToOne(fetch = FetchType.EAGER)
        @JoinColumn(name = "unicorn_id")
        public Unicorn getUnicorn(){
            return unicorn;
        }
        @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "droid")
        public List<Cupcake> getCupcakes(){
            return cupcakes;
        }
        @Column(name = "alive")
        public Boolean getAlive(){
            return alive;
        }
    }

    Cupcake:

    @Entity@Table(name = "cupcakes")
    @Setter@EqualsAndHashCode(callSuper = false)
    @AllArgsConstructor@NoArgsConstructorpublicclassCupcakeextendsAbstractEntity{
        private Filling filling;
        private Droid droid;
        @Column(name = "filling")
        public Filling getFilling(){
            return filling;
        }
        @ManyToOne(fetch = FetchType.EAGER)
        @JoinColumn(name = "droid_id")
        public Droid getDroid(){
            return droid;
        }
        publicCupcake(Filling filling){
            this.filling = filling;
        }
    }


    We will convert these entities into DTO. There are at least two approaches to converting dependencies from an entity into a DTO. One implies saving only the ID instead of the entity, but then each entity from the dependency, if needed, will be pulled further by the ID. The second approach involves keeping the DTO dependent. So, in the first approach, we would convert the List droids to the List droids (in the new list we store only the ID), and in the second approach we will save to the List droids.

    DTO
    Абстрактный родитель:

    @DatapublicabstractclassAbstractDtoimplementsSerializable{
        private Long id;
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS")
        LocalDateTime created;
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS")
        LocalDateTime updated;
    }

    UnicornDto:

    @EqualsAndHashCode(callSuper = true)
    @Data@NoArgsConstructor@AllArgsConstructorpublicclassUnicornDtoextendsAbstractDto{
        private String name;
        private List<DroidDto> droids;
        private String color;
    }

    DroidDto:

    @EqualsAndHashCode(callSuper = true)
    @Data@NoArgsConstructor@AllArgsConstructorpublicclassDroidDtoextendsAbstractDto{
        private String name;
        private List<CupcakeDto> cupcakes;
        private UnicornDto unicorn;
        private Boolean alive;
    }

    CupcakeDto:

    @EqualsAndHashCode(callSuper = true)
    @Data@NoArgsConstructor@AllArgsConstructorpublicclassCupcakeDtoextendsAbstractDto{
        private String filling;
        private DroidDto droid;
    }


    To fine tune the mapper to our needs, we will need to create our own wrapper class and override the logic for mapping collections. To do this, we create a class-component UnicornMapper, autovayrim our mapper there and redefine the methods we need.

    The simplest version of the wrapper class looks like this:

    @ComponentpublicclassUnicornMapper{
        @Autowiredprivate ModelMapper mapper;
        @Overridepublic Unicorn toEntity(UnicornDto dto){
            return Objects.isNull(dto) ? null : mapper.map(dto, Unicorn.class);
        }
        @Overridepublic UnicornDto toDto(Unicorn entity){
            return Objects.isNull(entity) ? null : mapper.map(entity, UnicornDto.class);
        }
    }

    Now we just need to add our mapper to some service and pull on toDto and toEntity methods. Entities found in the object will be converted by the mapper into DTO, DTO - in essence.

    @ServicepublicclassUnicornServiceImplimplementsUnicornService{
        privatefinal UnicornRepository repository;
        privatefinal UnicornMapper mapper;
        @AutowiredpublicUnicornServiceImpl(UnicornRepository repository, UnicornMapper mapper){
            this.repository = repository;
            this.mapper = mapper;
        }
        @Overridepublic UnicornDto save(UnicornDto dto){
            return mapper.toDto(repository.save(mapper.toEntity(dto)));
        }
        @Overridepublic UnicornDto get(Long id){
            return mapper.toDto(repository.getOne(id));
        }
    }

    But if we try to convert something in this way, and then call, for example, toString, then we get a StackOverflowException, and here's why: UnicornDto contains the DroidDto list, which contains UnicornDto, where DroidDto is located, and so on. until the stack memory runs out. Therefore, for inverse dependencies, I usually use not UnicornDto unicorn, but Long unicornId. Thus, we retain a connection with Unicorn, but we cut off cyclical dependency. Let's fix our DTOs in such a way that instead of inverse DTOs they store the ID of their dependencies.

    @EqualsAndHashCode(callSuper = true)
    @Data@NoArgsConstructor@AllArgsConstructorpublicclassDroidDtoextendsAbstractDto{
    ...
        //private UnicornDto unicorn;private Long unicornId;
    ...
    }

    and so on.

    But now, if we call DroidMapper, we get unicornId == null. This is because ModelMapper cannot determine exactly what Long is. And just does not fail him. And we will have to tackle the necessary mappers to teach them how to map entities to ID.

    Recall that with each bin after its initialization, you can work manually.

    @PostConstructpublicvoidsetupMapper(){
            mapper.createTypeMap(Droid.class, DroidDto.class)
                    .addMappings(m -> m.skip(DroidDto::setUnicornId)).setPostConverter(toDtoConverter());
            mapper.createTypeMap(DroidDto.class, Droid.class)
                    .addMappings(m -> m.skip(Droid::setUnicorn)).setPostConverter(toEntityConverter());
        }

    In @PostConstruct we will set the rules in which we will indicate which fields should not be touched by the mapper, because for them we will define the logic ourselves. In our case, this is both the definition of unicornId in the DTO, and the definition of Unicorn in essence (because the mapper does not know what to do with Long unicornId).

    TypeMap is the rule in which we specify all the nuances of the mapping, and also we set the converter. We indicated that for converting from Droid to DroidDto we skip setUnicornId, and when converting backwards - setUnicorn. We will convert everything in the converter toDtoConverter () for UnicornDto and in toEntityConverter () for Unicorn. We need to describe these converters in our component.

    The easiest postconverter looks like this:

    Converter<UnicornDto, Unicorn> toEntityConverter(){
            return MappingContext::getDestination;
        }

    We need to expand its functionality:

    public Converter<UnicornDto, Unicorn> toEntityConverter(){
            return context -> {
                UnicornDto source = context.getSource();
                Unicorn destination = context.getDestination();
                mapSpecificFields(source, destination);
                return context.getDestination();
            };
        }

    We do the same with the reverse converter:

    public Converter<Unicorn, UnicornDto> toDtoConverter(){
            return context -> {
                Unicorn source = context.getSource();
                UnicornDto destination = context.getDestination();
                mapSpecificFields(source, destination);
                return context.getDestination();
            };
        }

    In fact, we simply insert an additional method into each post-converter, in which we will write our own logic for the missing fields.

    publicvoidmapSpecificFields(Droid source, DroidDto destination){
            destination.setUnicornId(Objects.isNull(source) || Objects.isNull(source.getId()) ? null : source.getUnicorn().getId());
        }
        voidmapSpecificFields(DroidDto source, Droid destination){
            destination.setUnicorn(unicornRepository.findById(source.getUnicornId()).orElse(null));
        }

    When mapping in DTO, we refer to the entity ID. When mapping in DTO, we retrieve the entity from the repository by ID.

    And that's all.

    I showed the necessary minimum to start working with modelmapper and didn’t particularly refactor the code. If you, the reader, have something to add to my article, I will be glad to hear constructive criticism.

    The project can be viewed here:
    The project on GitHub.

    Fans of clean code probably already saw the opportunity to drive many components of the code into abstraction. If you are one of them, I suggest under the cat.

    We increase the level of abstraction
    Для начала, определим интерфейс для основных методов класса-обёртки.

    publicinterfaceMapper<EextendsAbstractEntity, DextendsAbstractDto> {
        E toEntity(D dto);
        D toDto(E entity);
    }

    Унаследуем от него абстрактный класс.

    publicabstractclassAbstractMapper<EextendsAbstractEntity, DextendsAbstractDto> implementsMapper<E, D> {
        @Autowired
        ModelMapper mapper;
        private Class<E> entityClass;
        private Class<D> dtoClass;
        AbstractMapper(Class<E> entityClass, Class<D> dtoClass) {
            this.entityClass = entityClass;
            this.dtoClass = dtoClass;
        }
        @Overridepublic E toEntity(D dto){
            return Objects.isNull(dto)
                    ? null
                    : mapper.map(dto, entityClass);
        }
        @Overridepublic D toDto(E entity){
            return Objects.isNull(entity)
                    ? null
                    : mapper.map(entity, dtoClass);
        }
        Converter<E, D> toDtoConverter(){
            return context -> {
                E source = context.getSource();
                D destination = context.getDestination();
                mapSpecificFields(source, destination);
                return context.getDestination();
            };
        }
        Converter<D, E> toEntityConverter(){
            return context -> {
                D source = context.getSource();
                E destination = context.getDestination();
                mapSpecificFields(source, destination);
                return context.getDestination();
            };
        }
        voidmapSpecificFields(E source, D destination){
        }
        voidmapSpecificFields(D source, E destination){
        }
    }

    Постконвертеры и методы заполнения специфичных полей смело отправляем туда. Также, создаём два объекта типа Class и конструктор для их инициализации:

    private Class<E> entityClass;
        private Class<D> dtoClass;
        AbstractMapper(Class<E> entityClass, Class<D> dtoClass) {
            this.entityClass = entityClass;
            this.dtoClass = dtoClass;
        }

    Теперь количество кода в DroidMapper сокращается до следующего:

    @ComponentpublicclassDroidMapperextendsAbstractMapper<Droid, DroidDto> {
        privatefinal ModelMapper mapper;
        privatefinal UnicornRepository unicornRepository;
        @AutowiredpublicDroidMapper(ModelMapper mapper, UnicornRepository unicornRepository){
            super(Droid.class, DroidDto.class);
            this.mapper = mapper;
            this.unicornRepository = unicornRepository;
        }
        @PostConstructpublicvoidsetupMapper(){
            mapper.createTypeMap(Droid.class, DroidDto.class)
                    .addMappings(m -> m.skip(DroidDto::setUnicornId)).setPostConverter(toDtoConverter());
            mapper.createTypeMap(DroidDto.class, Droid.class)
                    .addMappings(m -> m.skip(Droid::setUnicorn)).setPostConverter(toEntityConverter());
        }
        @OverridepublicvoidmapSpecificFields(Droid source, DroidDto destination){
            destination.setUnicornId(getId(source));
        }
        private Long getId(Droid source){
            return Objects.isNull(source) || Objects.isNull(source.getId()) ? null : source.getUnicorn().getId();
        }
        @OverridevoidmapSpecificFields(DroidDto source, Droid destination){
            destination.setUnicorn(unicornRepository.findById(source.getUnicornId()).orElse(null));
        }
    }

    Маппер без специфичных полей выглядит вообще просто:

    @ComponentpublicclassUnicornMapperextendsAbstractMapper<Unicorn, UnicornDto> {
        @AutowiredpublicUnicornMapper(){
            super(Unicorn.class, UnicornDto.class);
        }
    }


    Also popular now: