ModelMapper: ida e volta

imagem

Por razões conhecidas, o back-end não pode retornar dados do repositório como está. O mais famoso - as dependências essenciais não são retiradas da base na forma em que a frente as entende. Aqui você pode adicionar dificuldades ao analisar enum (se os campos enum contiverem parâmetros adicionais) e muitas outras dificuldades decorrentes da conversão automática de tipo (ou da incapacidade de convertê-los automaticamente). Isso implica na necessidade de usar um Data Transfer Object - DTO, que seja compreensível tanto para trás quanto para frente.
A conversão de uma entidade em um DTO pode ser feita de várias maneiras diferentes. Você pode usar a biblioteca, você pode (se o projeto for pequeno) montar algo parecido com isto:

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

Esses mapeadores auto-escritos têm desvantagens óbvias:

  1. Não escalar.
  2. Ao adicionar / remover até o campo mais insignificante, você precisará editar o mapeador.

Portanto, a solução correta é usar uma biblioteca de mapeadores. Conheço modelmapper e mapstruct. Como trabalhei com modelmapper, falarei sobre isso, mas se você, meu leitor, conhece bem o mapstruct e pode contar sobre todas as sutilezas de sua aplicação, escreva um artigo sobre ele e eu serei o primeiro a escrever para mim (esta biblioteca também é muito interessante, mas ainda não há tempo para inseri-lo).

Então modelmapper.

Quero dizer imediatamente que, se algo não estiver claro para você, você pode fazer o download do projeto finalizado com um teste de trabalho, um link no final do artigo.

O primeiro passo é, obviamente, adicionar uma dependência. Eu uso o gradle, mas é fácil adicionar uma dependência ao seu projeto maven.

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

Isso é suficiente para o mapeador funcionar. Em seguida, precisamos criar uma lixeira.

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

Normalmente, basta simplesmente retornar o novo ModelMapper, mas não será supérfluo configurar o mapeador para nossas necessidades. Defino uma estratégia de correspondência rigorosa, habilitei o mapeamento de campos, ignorando campos nulos e defino um nível privado de acesso aos campos.

Em seguida, crie a seguinte estrutura de entidade. Teremos um unicórnio, que terá um certo número de dróides subordinados, e cada dróide terá um certo número de cupcakes.

Entidades
Pai abstrato:

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

Unicórnio:

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


Converteremos essas entidades em DTO. Existem pelo menos duas abordagens para converter dependências de uma entidade em um DTO. Um implica salvar apenas o ID em vez da entidade, mas cada entidade da dependência, se necessário, extrairemos o ID adicionalmente. A segunda abordagem envolve manter o DTO dependente. Portanto, na primeira abordagem, converteríamos os dróides da lista em dróides da lista (só armazenamos IDs na nova lista) e, na segunda abordagem, salvaremos nos dróides da lista.

DTO
Pai abstrato:

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


Para ajustar o mapeador às nossas necessidades, precisaremos criar nossa própria classe de wrapper e redefinir a lógica para mapear coleções. Para fazer isso, criamos uma classe de componente UnicornMapper, mapeamos automaticamente nosso mapeador para lá e redefinimos os métodos que precisamos.

A versão mais simples da classe wrapper é assim:

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

Agora é o suficiente para conectar automaticamente nosso mapeador a algum serviço e utilizá-lo usando os métodos toDto e toEntity. O mapeador transformará as entidades encontradas no objeto em DTO, DTO - em entidades.

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

Mas se tentarmos converter algo dessa maneira e chamar, por exemplo, toString, obteremos uma StackOverflowException, e eis o porquê: UnicornDto contém a lista DroidDto, que contém UnicornDto, que contém DroidDto e assim por diante até aquele momento até a memória da pilha acabar. Portanto, para dependências inversas, geralmente não uso UnicornDto unicorn, mas Long unicornId. Dessa forma, mantemos contato com o Unicorn, mas eliminamos a dependência cíclica. Vamos corrigir nossos DTOs para que, em vez de DTOs reversos, eles armazenem os IDs de suas dependências.

 @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class DroidDto extends AbstractDto { ... //private UnicornDto unicorn; private Long unicornId; ... } 

e assim por diante.

Mas agora, se chamarmos DroidMapper, obteremos unicornId == null. Isso ocorre porque o ModelMapper não pode determinar exatamente o que é Long. E simplesmente não o incomoda. E teremos que ajustar os mapeadores necessários para ensiná-los a mapear entidades em IDs.

Lembramos que com cada compartimento após sua inicialização, você pode trabalhar manualmente.

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

No @PostConstruct, definiremos as regras nas quais indicamos quais campos o mapeador não deve tocar, porque para eles determinaremos a lógica por conta própria. No nosso caso, essa é a definição de unicornId no DTO e a definição de Unicorn em essência (uma vez que o mapeador também não sabe o que fazer com o Long unicornId).

TypeMap - esta é a regra na qual especificamos todas as nuances do mapeamento e também definimos o conversor. Observamos que, para converter de Droid para DroidDto, pulamos setUnicornId e, na conversão reversa, passamos para setUnicorn. Todos nós converteremos no conversor toDtoConverter () para UnicornDto e em toEntityConverter () para Unicorn. Devemos descrever esses conversores em nosso componente.

O pós-conversor mais simples é assim:

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

Precisamos expandir sua funcionalidade:

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

Fazemos o mesmo com o conversor inverso:

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

De fato, simplesmente inserimos um método adicional em cada pós-conversor, no qual escrevemos nossa própria lógica para os campos ausentes.

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

Ao mapear no DTO, definimos o ID da entidade. Ao mapear no DTO, obtemos a entidade do repositório por ID.

E é isso.

Mostrei o mínimo necessário para começar a trabalhar com o modelmapper e não refatorei particularmente o código. Se você, leitor, tem algo a acrescentar ao meu artigo, ficarei feliz em ouvir críticas construtivas.

O projeto pode ser visto aqui:
Projeto no GitHub.

Os fãs de código limpo provavelmente já viram a oportunidade de direcionar muitos dos componentes de código para uma abstração. Se você é um deles, sugiro em gato.

Aumente o nível de abstração
Para começar, definimos uma interface para os métodos básicos da classe wrapper.

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

Herdamos dela uma classe abstrata.

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

Pós-conversores e métodos para preencher campos específicos podem ser enviados com segurança para lá. Além disso, crie dois objetos do tipo Class e um construtor para inicializá-los:

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

Agora, a quantidade de código no DroidMapper é reduzida para o seguinte:

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

Um mapeador sem campos específicos geralmente parece simples:

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

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


All Articles