📜 ⬆️ ⬇️

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 not be superfluous to configure 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
Abstract parent:

 @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
Abstract parent:

 @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 override 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 the mapper should not touch, 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 found here:
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
First, we define the interface for the basic methods of the wrapper class.

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

We inherit from it the abstract class.

 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) { } } 

Postconverters and methods for filling in specific fields are safely sent there. Also, we create two objects of type Class and a constructor for their initialization:

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

Now the amount of code in DroidMapper is reduced to the following:

 @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)); } } 

Mapper without specific fields looks generally simple:

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

Source: https://habr.com/ru/post/438808/