MapStruct con Spring Boot

mapstruct

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

Spring Boot AOP

Cómo funciona la inyección de dependencias en Spring

Cómo funciona BeanPostProcessor en Spring


1 pensamiento sobre “MapStruct con Spring Boot

  1. 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)

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *