MapStruct con Spring Boot
En este artículo vamos a ver como usar MapStruct con Spring Boot, el cual es un simple Mapper para convertir de un objeto a otro.
El API de MapStruct contiene las funcionalidades necesarias para convertir entre dos Bean de Java.
¿Por qué usar MapStruct?
MapStruct lo usaremos siempre que queramos hacer algún tipo de conversión entre objetos de Java, por ejemplo, si tenemos un DTO y queremos transformarlo en una Entidad.
Además MapStruct nos facilita su uso con tan solo crear una interfaz y definir los métodos de conversión.
¿Cómo usar MapStruct?
Vamos a comenzar añadiendo su dependencia Maven, hay que tener en cuenta que nosotros usamos Lombok por lo que también lo incluiremos:
Maven
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${mapstruct.version}</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${mapstruct.version}</version> </dependency>
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-compiler-plugin.version}</version> <configuration> <release>${java.version}</release> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${mapstruct.version}</version> </path> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin>
Conversión básica
Vamos a empezar con el caso más básico de mapper
@Getter @Setter public class CarEntity { private String color; private String model; } @Getter @Setter public class CarDto { private String color; private String model; }
Vamos a crear la interfaz que se encargará de mapstruct:
@Mapper(componentModel = "spring") public interface CarMapper { CarEntity toEntity(CarDto source); CarDto toDto(CarEntity target); }
Para poder hacer uso de Spring IoC, tenemos que añadir el componentModel con la etiqueta «spring«.
La interfaz que hemos creado, realizará una implementación para los métodos creados.
Conversión con diferentes campos
Hay veces que tendremos diferentes nombre en los campos entre los dos objetos para ello usaremos una nueva propiedad de mapstruct @Mapping
@Getter @Setter public class CarEntity { private String colorCode; private String name; } @Getter @Setter public class CarDto { private String color; private String model; }
La nueva interfaz hacer uso de @Mapping para mapear los campos:
@Mapper public interface CarMapper { @Mappings({ @Mapping(target="color", source="colorCode"), @Mapping(target="model", source="name") }) CarDto Dto(CarEntity entity); @Mappings({ @Mapping(target="colorCode", source="color"), @Mapping(target="model", source="name") }) CarEntity toEntity(CarDto dto); }
Conversión con referencia a otros objetos
En algunas ocasiones tendremos que convertir objetos con objetos dentro, vamos a verlo con un ejemplo:
@Getter @Setter public class CarEntity { private String color; private String model; private UserEntity user; } @Getter @Setter public class CarDto { private String color; private String model; private UserDto user; } @Getter @Setter public class UserEntity { private String name; } @Getter @Setter public class UserDto { private String name; }
Vamos a crear un nuevo mapper y el mapper padre lo referenciará:
@Mapper(componentModel = "spring") public interface UserMapper { UserEntity toEntity(UserDto source); UserDto toDto(UserEntity target); } @Mapper(componentModel = "spring", uses = UserMapper.class)) public interface CarMapper { CarEntity toEntity(CarDto source); CarDto toDto(CarEntity target); }
Como vemos en el ejemplo anterior hemos añadido en el mapper padre la sentencia uses = UserMapper.class para que pueda mapear las clases del tipo User.
Conversión de Tipos con MapStruct
MapStruct nos ofrece conversión implicita de tipos, por lo general la más usada suele ser la conversión de un string a tipo date, vamos a ver lo mejor con un ejemplo:
@Getter @Setter public class CarEntity { private String colorCode; private String name; private String date; } @Getter @Setter public class CarDto { private String color; private String model; private LocalDate date; }
Modificamos nuestra interfaz para incorporar esta conversión de tipos:
@Mapper(componentModel = "spring") public interface CarMapper { @Mappings({ @Mapping(target="color", source="colorCode"), @Mapping(target="model", source="name"), @Mapping(target="date", source = "date", dateFormat = "dd-MM-yyyy") }) CarDto Dto(CarEntity entity); @Mappings({ @Mapping(target="colorCode", source="color"), @Mapping(target="model", source="name"), @Mapping(target="date", source="date", dateFormat="dd-MM-yyyy") }) CarEntity toEntity(CarDto dto); }
Ignorar un campo con MapStruct
Hay veces que necesitamos ignorar un campo, para ello vamos a usar la propiedad ignore.
@Getter @Setter public class CarEntity { private String color; private String model; private boolean valueToIgnore; } @Getter @Setter public class CarDto { private String color; private String model; private boolean valueToIgnore; }
@Mapper(componentModel = "spring") public interface CarMapper { @Mapping(ignore = true, target = "valueToIgnore") CarEntity toEntity(CarDto source); @Mapping(ignore = true, target = "valueToIgnore") CarDto toDto(CarEntity target); }
En el ejemplo anterior lo que hemos logrado es que el campo valueToIgnore, sea ignorado tanto al convertir a entidad como al convertir a dto.
El uso de ignore dentro de mapstruct va a ser de vital importancia cuando estamos trabajando con Hibernate y JPA y haciendo uso de FetchType.LAZY, ya que si no tenemos en cuenta la carga «perezosa» mapstruct nos hará un get y nos traerá toda la información. Este es uno de los errores típicos que suele cometer cuando se trabaja con mapstruct e Hibernate.
Anotaciones BeforeMapping y AfterMapping en Mapstruct
En muchas ocasiones vamos a necesitar realizar algún tipo de mapeo antes o después de la conversión, para estos casos hacemos uso de las anotaciones @BeforeMappging y @AfterMapping, por ejemplo en función de un tipo de objeto, popular un campo.
Haciendo uso de @BeforeMapping en Mapstruct vamos a conseguir ejecutar el código que creemos justo antes de que se produzca el mapping.
El uso de @AfterMapping en Mapstruct se ejecutará despues de la ejecución del mapping, de manera que podremos asignar valores una vez finalizada el mapeo.
@Getter @Setter public class CarEntity { private String colorCode; private String name; } public class PickUpCar extends Car { } public class SedanCar extends Car { } @Getter @Setter public class CarDto { private String color; private String model; private CarType type; } public enum CarType { PICKUP, SEDAN }
@Mapper(componentModel = "spring") public interface CarMapper { CarEntity toEntity(CarDto dto); CarDto Dto(CarEntity entity); @BeforeMapping default void setTypeCar(Car car, @MappingTarget CarDTO carDto) { if (car instanceof SedanCar) { carDto.setType(CarType.SEDAN); } if (car instanceof PickupCar) { carDto.setType(CarType.PICKUP); } } @AfterMapping default void changeColor(@MappingTarget CarDTO carDto) { carDto.setColor("yellow"); } }
Crear mapper custom con @Named
En algunos casos necesitamos crear un mapper para solventar alguna situación específica que directamente no permite el mapeo. Para esos casos podemos hacer uso de @Named. Vamos a verlo con un ejemplo.
Tenemos un caso en el que necesitamos convertir de millas a kilómetros, y tenemos los siguientes objetos para convertir:
public class CityValuesDTO { private int miles; private String city; //getters setters etc... }
public class CityValues { private double km; private String city; //getters setters etc... }
Para poder realizar el mapeo de miles a kilómetros y viceversa vamos a hacer uso de un mapper y la anotación @named.
@Mapper public interface CityValuesValuesMapper { @Named("milesToKm") public static double milesToKilometers(int miles) { return miles * 1.6; } //... }
Ahora terminamos el mapper con los mapeos que necesitamos:
@Mapper public interface CityValuesMapper { CityValuesValuesMapper INSTANCE = Mappers.getMapper(CityValuesValuesMapper.class); @Mapping(source = "miles", target = "km", qualifiedByName = "milesToKm") public CityValues toDomains(CityValuesDTO dto); @Named("milesToKm") public static double milesToKilometers(int miles) { return miles * 1.6; } }
Conclusión
En esta entrada hemos visto como usar MapStruct con Spring Boot, viendo algunos ejemplos. Aunque hay otras librerías que nos van ayudar también para realizar un mapper entre nuestros beans, quizás sea este el que mejor se adapta a Spring facilitando muchísimo el trabajo y el tiempo de desarrollo.
Otros artículos que te pueden interesar
Cómo funciona la inyección de dependencias en Spring
Cómo funciona BeanPostProcessor en Spring
Gracias Noel, he hecho una búsqueda en Google y me ha salido esta página la primera. Enhorabuena chato (y no lo digo porque seamos amigos)