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:

    @Component
    public class ItemMapperImpl implements ItemMapper {
        private final OrderRepository orderRepository;
        @Autowired
        public ItemMapperImpl(OrderRepository orderRepository) {
            this.orderRepository = orderRepository;
        }
        @Override
        public Item toEntity(ItemDto dto) {
            return new Item(
                    dto.getId(),
                    obtainOrder(dto.getOrderId()),
                    dto.getArticle(),
                    dto.getName(),
                    dto.getDisplayName(),
                    dto.getWeight(),
                    dto.getCost(),
                    dto.getEstimatedCost(),
                    dto.getQuantity(),
                    dto.getBarcode(),
                    dto.getType()
            );
        }
        @Override
        public ItemDto toDto(Item item) {
            return new 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.

    @Bean
        public 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
    @AllArgsConstructor
    public abstract class AbstractEntity implements Serializable {
        Long id;
        LocalDateTime created;
        LocalDateTime updated;
        @Id
        @GeneratedValue
        public Long getId() {
            return id;
        }
        @Column(name = "created", updatable = false)
        public LocalDateTime getCreated() {
            return created;
        }
        @Column(name = "updated", insertable = false)
        public LocalDateTime getUpdated() {
            return updated;
        }
        @PrePersist
        public void toCreate() {
            setCreated(LocalDateTime.now());
        }
        @PreUpdate
        public void toUpdate() {
            setUpdated(LocalDateTime.now());
        }
    }

    Unicorn:

    @Entity
    @Table(name = "unicorns")
    @EqualsAndHashCode(callSuper = false)
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    public class Unicorn extends AbstractEntity {
        private String name;
        private List<Droid> droids;
        private Color color;
        public Unicorn(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
    @NoArgsConstructor
    public class Droid extends AbstractEntity {
        private String name;
        private Unicorn unicorn;
        private List<Cupcake> cupcakes;
        private Boolean alive;
        public Droid(String name, Unicorn unicorn, Boolean alive) {
            this.name = name;
            this.unicorn = unicorn;
            this.alive = alive;
        }
        public Droid(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
    @NoArgsConstructor
    public class Cupcake extends AbstractEntity {
        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;
        }
        public Cupcake(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
    Абстрактный родитель:

    @Data
    public abstract class AbstractDto implements Serializable {
        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
    @AllArgsConstructor
    public class UnicornDto extends AbstractDto {
        private String name;
        private List<DroidDto> droids;
        private String color;
    }

    DroidDto:

    @EqualsAndHashCode(callSuper = true)
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class DroidDto extends AbstractDto {
        private String name;
        private List<CupcakeDto> cupcakes;
        private UnicornDto unicorn;
        private Boolean alive;
    }

    CupcakeDto:

    @EqualsAndHashCode(callSuper = true)
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class CupcakeDto extends AbstractDto {
        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:

    @Component
    public class UnicornMapper {
        @Autowired
        private ModelMapper mapper;
        @Override
        public Unicorn toEntity(UnicornDto dto) {
            return Objects.isNull(dto) ? null : mapper.map(dto, Unicorn.class);
        }
        @Override
        public 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.

    @Service
    public class UnicornServiceImpl implements UnicornService {
        private final UnicornRepository repository;
        private final UnicornMapper mapper;
        @Autowired
        public UnicornServiceImpl(UnicornRepository repository, UnicornMapper mapper) {
            this.repository = repository;
            this.mapper = mapper;
        }
        @Override
        public UnicornDto save(UnicornDto dto) {
            return mapper.toDto(repository.save(mapper.toEntity(dto)));
        }
        @Override
        public 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
    @AllArgsConstructor
    public class DroidDto extends AbstractDto {
    ...
        //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.

        @PostConstruct
        public void setupMapper() {
            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.

        public void mapSpecificFields(Droid source, DroidDto destination) {
            destination.setUnicornId(Objects.isNull(source) || Objects.isNull(source.getId()) ? null : source.getUnicorn().getId());
        }
        void mapSpecificFields(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
    Для начала, определим интерфейс для основных методов класса-обёртки.

    public interface Mapper<E extends AbstractEntity, D extends AbstractDto> {
        E toEntity(D dto);
        D toDto(E entity);
    }

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

    public abstract class AbstractMapper<E extends AbstractEntity, D extends AbstractDto> implements Mapper<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;
        }
        @Override
        public E toEntity(D dto) {
            return Objects.isNull(dto)
                    ? null
                    : mapper.map(dto, entityClass);
        }
        @Override
        public 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();
            };
        }
        void mapSpecificFields(E source, D destination) {
        }
        void mapSpecificFields(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 сокращается до следующего:

    @Component
    public class DroidMapper extends AbstractMapper<Droid, DroidDto> {
        private final ModelMapper mapper;
        private final UnicornRepository unicornRepository;
        @Autowired
        public DroidMapper(ModelMapper mapper, UnicornRepository unicornRepository) {
            super(Droid.class, DroidDto.class);
            this.mapper = mapper;
            this.unicornRepository = unicornRepository;
        }
        @PostConstruct
        public void setupMapper() {
            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());
        }
        @Override
        public void mapSpecificFields(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();
        }
        @Override
        void mapSpecificFields(DroidDto source, Droid destination) {
            destination.setUnicorn(unicornRepository.findById(source.getUnicornId()).orElse(null));
        }
    }

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

    @Component
    public class UnicornMapper extends AbstractMapper<Unicorn, UnicornDto> {
        @Autowired
        public UnicornMapper() {
            super(Unicorn.class, UnicornDto.class);
        }
    }


    Also popular now: