ModelMapper: ida y vuelta

imagen

Por razones bien conocidas, el backend no puede devolver datos del repositorio tal como está. El más famoso: las dependencias esenciales no se toman de la base en la forma en que el frente puede entenderlas. Aquí puede agregar dificultades para analizar la enumeración (si los campos de enumeración contienen parámetros adicionales) y muchas otras dificultades derivadas de la conversión automática de tipos (o la imposibilidad de convertirlas automáticamente). Esto implica la necesidad de utilizar un Objeto de transferencia de datos - DTO, que es comprensible tanto para la parte posterior como para la frontal.
La conversión de una entidad a un DTO se puede hacer de muchas maneras. Puedes usar la biblioteca, puedes (si el proyecto es pequeño) armar algo como esto:

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

Tales mapeadores auto-escritos tienen desventajas obvias:

  1. No escalar.
  2. Al agregar / eliminar incluso el campo más insignificante, tendrá que editar el mapeador.

Por lo tanto, la solución correcta es usar una biblioteca de mapeador. Sé modelmapper y mapstruct. Como trabajé con modelmapper, hablaré sobre eso, pero si usted, mi lector, conoce bien maptruct y puede contar todas las sutilezas de su aplicación, escriba un artículo al respecto, y seré el primero en escribirme (esta biblioteca también es muy interesante, pero aún no hay tiempo para ingresarlo).

Entonces modelmapper.

Quiero decir de inmediato que si algo no está claro para usted, puede descargar el proyecto terminado con una prueba de trabajo, un enlace al final del artículo.

El primer paso es, por supuesto, agregar una dependencia. Yo uso Gradle, pero es fácil para usted agregar una dependencia a su proyecto Maven.

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

Esto es suficiente para que el mapeador funcione. A continuación, necesitamos crear un contenedor.

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

Por lo general, es suficiente simplemente devolver el nuevo ModelMapper, pero no será superfluo configurar el mapeador para nuestras necesidades. Establecí una estrategia estricta de correspondencia, habilité el mapeo de campos, salté los campos nulos y establecí un nivel privado de acceso a los campos.

A continuación, cree la siguiente estructura de entidad. Tendremos un Unicornio, que tendrá una cierta cantidad de droides subordinados, y cada droide tendrá una cierta cantidad de Cupcakes.

Entidades
Resumen padre:

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

Unicornio

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

Droide:

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

Magdalena

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


Convertiremos estas entidades a DTO. Existen al menos dos enfoques para convertir las dependencias de una entidad a un DTO. Una implica guardar solo la ID en lugar de la entidad, pero luego cada entidad de la dependencia, si es necesario, extraeremos la ID adicionalmente. El segundo enfoque implica mantener el DTO dependiente. Entonces, en el primer enfoque, convertiríamos droides de lista en droides de lista (solo almacenamos ID en la nueva lista), y en el segundo enfoque, guardaremos en droides de lista.

DTO
Resumen padre:

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

Unicornio:

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

MagdalenaDto:

 @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class CupcakeDto extends AbstractDto { private String filling; private DroidDto droid; } 


Para ajustar el mapeador a nuestras necesidades, necesitaremos crear nuestra propia clase de envoltura y redefinir la lógica para las colecciones de mapeo. Para hacer esto, creamos una clase de componente UnicornMapper, mapeamos automáticamente nuestro mapeador allí y redefinimos los métodos que necesitamos.

La versión más simple de la clase wrapper se ve así:

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

Ahora es suficiente para nosotros conectar automáticamente nuestro mapeador a algún servicio y utilizarlo con los métodos toDto y toEntity. El asignador convertirá las entidades encontradas en el objeto en DTO, DTO - en 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)); } } 

Pero si intentamos convertir algo de esta manera, y luego llamamos, por ejemplo, a String, obtendremos una StackOverflowException, y he aquí por qué: UnicornDto contiene la lista DroidDto, que contiene UnicornDto, que contiene DroidDto, y así hasta ese momento hasta que se agote la memoria de la pila. Por lo tanto, para dependencias inversas, generalmente no uso UnicornDto unicornio, sino Long unicornId. De esta manera, nos mantenemos en contacto con Unicornio, pero cortamos la dependencia cíclica. Arreglemos nuestros DTO para que, en lugar de los DTO inversos, almacenen los ID de sus dependencias.

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

Y así sucesivamente.

Pero ahora, si llamamos a DroidMapper, obtenemos unicornId == null. Esto se debe a que ModelMapper no puede determinar exactamente qué es Long. Y simplemente no le molesta. Y tendremos que ajustar los mapeadores necesarios para enseñarles cómo mapear entidades en ID.

Recordamos que con cada contenedor después de su inicialización, puede trabajar 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()); } 

En @PostConstruct estableceremos las reglas en las que indicaremos qué campos no debe tocar el asignador, porque para ellos determinaremos la lógica por nuestra cuenta. En nuestro caso, esta es la definición de unicornId en DTO y la definición de Unicorn en esencia (ya que el mapeador tampoco sabe qué hacer con Long unicornId).

TypeMap: esta es la regla en la que especificamos todos los matices de la asignación y también establecemos el convertidor. Señalamos que para convertir de Droid a DroidDto, omitimos setUnicornId, y en la conversión inversa, pasamos setUnicorn. Todos convertiremos en el convertidor toDtoConverter () para UnicornDto y en toEntityConverter () para Unicorn. Debemos describir estos convertidores en nuestro componente.

El post-convertidor más simple se ve así:

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

Necesitamos ampliar su funcionalidad:

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

Hacemos lo mismo con el convertidor inverso:

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

De hecho, simplemente insertamos un método adicional en cada convertidor posterior, en el que escribimos nuestra propia lógica para los campos faltantes.

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

Al mapear en DTO, establecemos la ID de la entidad. Al mapear en DTO, obtenemos la entidad del repositorio por ID.

Y eso es todo.

Mostré el mínimo necesario para comenzar a trabajar con modelmapper y no refactoricé particularmente el código. Si usted, lector, tiene algo que agregar a mi artículo, me complacerá escuchar críticas constructivas.

El proyecto se puede ver aquí:
Proyecto en GitHub.

Los fanáticos del código limpio probablemente ya vieron la oportunidad de conducir muchos de los componentes del código a una abstracción. Si eres uno de ellos, te sugiero debajo del gato.

Elevar el nivel de abstracción
Para comenzar, definimos una interfaz para los métodos básicos de la clase wrapper.

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

Heredamos de él una clase abstracta.

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

Los posconvertidores y los métodos para rellenar campos específicos se pueden enviar de forma segura allí. Además, cree dos objetos de tipo Class y un constructor para inicializarlos:

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

Ahora la cantidad de código en DroidMapper se reduce a lo siguiente:

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

Un mapeador sin campos específicos parece generalmente simple:

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

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


All Articles