martes, 21 de diciembre de 2021

Mapear objetos con MapStruct

Recientemente he estado migrando un proyecto muy antiguo que hacía copias de los valores de un objeto persistente a un objeto de la capa de vista o DTO. La migración contaba con varios problemas asociados a dicha lógica:

  • Había objetos que no tenían los mismos atributos. 
  • Con la migración y actualización del proyecto, podía ocurrir que las clases persistentes, ahora entidades JPA, utilizarán clases más modernas como java.time.LocalDate y los objetos DTO siguiesen usando APIs y clases más anticuadas, como java.util.Date.
  • Hacían uso de frameworks más básicos y obsoletos como commons-beanutils.
Actualmente hay varios frameworks que nos pueden ayudar en esta tarea como Orika, ModelMapper o JMapper. Pero nosotros nos centraremos en MapStruct. Y para ello utilizaremos la versión 1.4.2.Final. 

Aparte de que a nivel de rendimiento es una de las mejores librerías otro punto a favor es que con el uso de anotaciones se configura fácilmente el conversor. Pero esto conlleva que tengamos que añadir un mínimo de configuración en el plugin de compilación. En el deberemos indicar el procesador de anotaciones a utilizar, en este caso el propio de mapstruct

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

Pero si usamos lombok, tenemos que tener en cuenta que esta librería tiene su propio procesador de anotaciones y por tanto deberemos indicar ambas en el plugin:

...
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<!-- This is needed when using Lombok 1.18.16 and above -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
<!-- Mapstruct should follow the lombok path(s) -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
...

Las diferentes cualidades las veremos a través de la conversión de dos clases. Por un lado las clases de persistencia:


Y por otro lado las clases DTO:


El funcionamiento es muy sencillo. Crearemos una interface que tenga la anotación @Mapper y un atributo del mismo tipo generado con la clase de utilidad de mapStruct, Mappers. Y para utilizarla, crearemos métodos que conviertan un tipo de objeto en otro. 

@Mapper
public interface BookConverter {
	BookConverter MAPPER = Mappers.getMapper(BookConverter.class);
}

Si queremos una conversión básica, donde los atributos de ambas clases aunque no coincidan en el tipo, si coinciden en el nombre, no hace falta ninguna configuración extra. En la documentación oficial se indican además las conversiones automáticas, las puedes ver aquí. Incluso, tampoco hay que hacer ninguna configuración si una propiedad no existe o es nula. La conversión se realizará sin lanzar ninguna excepción como ocurre con otros mapeadores. 

SagaDto convertSaga(Saga sourceSaga);

En el caso donde haya una correspondencia que queremos que se realice de forma automática pero en la cual los nombres no coinciden, necesitaremos una configuración extra. Esta se puede realizar a través de la anotación @Mapping, en la cual indicaremos que atributos queremos que se mapeen de forma automatizada. 

@Mapping(source = "saga", target = "serie")
BookDto convertBook(Book sourceBook);

También podemos crear un objeto en función de otros dos. Y mapear atributos concretos de clases concretas a través del uso de expresiones y puntos. Como podemos ver a continuación.

@Mapping(source = "sourceBook.name", target = "bookName")
@Mapping(source = "sourceSaga.name", target = "sagaName")
BookResumeDto convertResume(Book sourceBook, Saga sourceSaga);

A través de la anotación @InheritInverseConfiguration, podremos hacer el mapeo a la inversa sin necesidad de configurar nada si ya lo hemos hecho en otro método. 

@InheritInverseConfiguration
Book convertBookDto(BookDto sourceBook);

Por terminar con el post, también se puede generar métodos propios de conversión dentro de la interfaz y que sean utilizados por la instancia que realiza los mapeos. O incluso crear clases que hagan la conversión de forma concreta para determinados objetos y asociarlos en la anotación principal:

@Mapper(uses=CustomDateMapper.class)

Y aparte de estas, también hay otras características que pueden ayudar a la conversión. Todas de ellas igual de sencillas de utilizar. 

No hay comentarios:

Publicar un comentario