DEV Community

Cover image for Cómo mapear objetos en Java con MapStruct
Jordi Ayala
Jordi Ayala

Posted on • Originally published at asjordi.dev

Cómo mapear objetos en Java con MapStruct

MapStruct es un generador de código que simplifica la implementación de mapeos entre objetos de Java permitiendo realizar este proceso de forma rápida y sencilla. Comúnmente en aplicaciones multicapa de Java se suele requerir mapear diferentes modelos de objetos, por ejemplo, entidades y DTO. Si se realiza de una manera manual puede ser tedioso y propenso a errores. Este es el propósito de MapStruct, simplificar el proceso de mapeo entre objetos. A diferencia de otras librerías, MapStruct genera los mapeos en tiempo de compilación, lo que permite un mejor rendimiento y una comprobación de errores más rápida.

MapStruct al ser un procesador de anotaciones se puede utilizar desde la línea de comandos usando Maven o Gradle, así como desde un IDE como IntelliJ IDEA o Eclipse.

Instalación

Para este ejemplo vamos a utilizar Maven, por lo que es necesario agregar lo siguiente al archivo pom.xml.

<properties>
    <org.mapstruct.version>1.6.3</org.mapstruct.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source> <!-- Dependiendo de tu proyecto -->
                <target>1.8</target> <!-- Dependiendo de tu proyecto -->
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <!-- Otras dependencias de procesadores de anotaciones -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>
Enter fullscreen mode Exit fullscreen mode

En caso de utilizar otra herramienta para la construcción de proyectos, se puede consultar la documentación oficial.

Configurar con Lombok

MapStruct se puede combinar con Lombok para reducir la cantidad de código que se escribe. Para ello, es necesario agregar las dependencias de lombok y lombok-mapstruct-binding, esta última es necesaria para que MapStruct pueda reconocer las anotaciones de Lombok.

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.36</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok-mapstruct-binding</artifactId>
    <version>0.2.0</version>
    <scope>provided</scope>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Ahora en el apartado de annotationProcessorPaths del maven-compiler-plugin se deben configurar estas dependencias.

<path>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.36</version>
</path>
<path>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok-mapstruct-binding</artifactId>
    <version>0.2.0</version>
</path>
Enter fullscreen mode Exit fullscreen mode

Al final, nuestro archivo pom.xml queda de la siguiente manera.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dev.asjordi</groupId>
    <artifactId>MapStruct</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <org.mapstruct.version>1.6.3</org.mapstruct.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.36</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-mapstruct-binding</artifactId>
            <version>0.2.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>21</source>
                    <target>21</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.36</version>
                        </path>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok-mapstruct-binding</artifactId>
                            <version>0.2.0</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
            </plugin>
        </plugins>
    </build>

</project>
Enter fullscreen mode Exit fullscreen mode

Configurar JUnit

Para realizar pruebas unitarias y verificar que los mapeos se están realizando correctamente, es necesario agregar la dependencia de JUnit al archivo pom.xml (este paso es completamente opcional).

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.11.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit</groupId>
    <artifactId>junit-bom</artifactId>
    <version>5.11.3</version>
    <type>pom</type>
    <scope>test</scope>
</dependency>
Enter fullscreen mode Exit fullscreen mode

En el apartado de build se debe agregar el plugin maven-surefire-plugin para ejecutar las pruebas unitarias.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version>
</plugin>
Enter fullscreen mode Exit fullscreen mode

Crear clases de ejemplo

Supongamos que tenemos una clase User con los atributos id, name y email que nos servirá como entidad base para mapearla a un DTO (Data Transfer Object).

import lombok.*;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private String name;
    private String email;
}
Enter fullscreen mode Exit fullscreen mode

Ahora definimos una clase UserDTO con los atributos name y email.

import lombok.*;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
    private String name;
    private String email;
}
Enter fullscreen mode Exit fullscreen mode

MapStruct también funciona con records, en general, este puede ser un mejor enfoque dado que la principal función de un DTO es almacenar y transferir datos de un punto a otro, donde la inmutabilidad es una característica deseable. Creamos un record UserRecord con los atributos name y email, en este caso no es necesario utilizar Lombok.

Si tienes dudas sobre los records, puedes consultar dentro del blog al respecto.

public record UserRecord(String name, String email) { }
Enter fullscreen mode Exit fullscreen mode

Crear un mapper

Para crear un mapper con MapStruct, se debe crear una interfaz con el nombre UserMapper (el nombre depende de cada uno) y anotarla con @Mapper. En esta interfaz se deben definir los métodos de mapeo entre las clases User, UserDTO y UserRecord.

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper
public interface UserMapper {
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

    UserDTO userToUserDTO(User user);
    UserRecord userToUserRecord(User user);
    User userDTOToUser(UserDTO userDTO);
    User userRecordToUser(UserRecord userRecord);
}
Enter fullscreen mode Exit fullscreen mode

La interfaz UserMapper tiene las siguientes características:

  • La anotación @Mapper indica que esta interfaz es un mapper.
  • La constante INSTANCE es un objeto que se utiliza para obtener una instancia del mapper.
  • Los métodos userToUserDTO y userToUserRecord mapean un objeto User a un objeto UserDTO y UserRecord respectivamente.
  • Los métodos userDTOToUser y userRecordToUser mapean un objeto UserDTO y UserRecord a un objeto User respectivamente.

Si tenemos atributos con diferente nombre, tanto en la entidad (source) como en el DTO (target), se puede utilizar la anotación @Mapping para indicar el nombre del atributo en la entidad y el nombre del atributo en el DTO.

@Mapping(source = "name", target = "name")
@Mapping(source = "email", target = "email")
UserDTO userToUserDTO(User user);
Enter fullscreen mode Exit fullscreen mode

En este punto ya tenemos tanto la entidad a mapear como el DTO, así como el mapper que se encargará de realizar el mapeo entre ambos.

Realizar el mapeo

Para realizar el mapeo de una entidad, basta con utilizar el atributo INSTANCE de la interfaz UserMapper y llamar al método correspondiente, tanto para mapear de la entidad al DTO como del DTO a la entidad.

public class Main {
    public static void main(String[] args) {
        // Mapeo de User a UserDTO y UserRecord

        User user = new User(1L, "John Doe", "jonh@gmail.com");
        UserDTO userDTO = UserMapper.INSTANCE.userToUserDTO(user);
        UserRecord userRecord = UserMapper.INSTANCE.userToUserRecord(user);

        System.out.println("User: " + user); // User: User(id=1, name=John Doe, email=jonh@gmail.com)
        System.out.println("UserDTO: " + userDTO); // UserDTO: UserDTO(name=John Doe, email=jonh@gmail.com)
        System.out.println("UserRecord: " + userRecord); // UserRecord: UserRecord[name=John Doe, email=jonh@gmail.com]

        // Mapeo inverso de UserDTO a User y UserRecord a User

        User userFromDTO = UserMapper.INSTANCE.userDTOToUser(userDTO);
        User userFromRecord = UserMapper.INSTANCE.userRecordToUser(userRecord);

        System.out.println("User from DTO: " + userFromDTO); // User from DTO: User(id=null, name=John Doe, email=jonh@gmail.com)
        System.out.println("User from Record: " + userFromRecord); // User from Record: User(id=null, name=John Doe, email=jonh@gmail.com)
    }
}
Enter fullscreen mode Exit fullscreen mode

En el ejemplo anterior, se crea un objeto User con los atributos id, name y email, luego se mapea a un objeto UserDTO y UserRecord. Posteriormente, se realiza el mapeo inverso de un objeto UserDTO y UserRecord a un objeto User.

Test unitarios

Para verificar que los mapeos entre objetos se están realizando correctamente, se pueden realizar pruebas unitarias con JUnit. En este caso, se crea una clase MapstructTest con diferentes pruebas unitarias para verificar los mapeos entre objetos.

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class MapstructTest {

    private static User user;

    @BeforeAll
    public static void setup() {
        user = new User(1L, "John Doe", "jonh@gmail.com");
    }

    @Test
    @DisplayName("Test User to UserDTO")
    void testUserToUserDTO() {
        UserDTO userDTO = UserMapper.INSTANCE.userToUserDTO(user);
        assertEquals(user.getName(), userDTO.getName());
        assertEquals(user.getEmail(), userDTO.getEmail());
    }

    @Test
    @DisplayName("Test User to UserRecord")
    void testUserToUserRecord() {
        UserRecord userRecord = UserMapper.INSTANCE.userToUserRecord(user);
        assertEquals(user.getName(), userRecord.name());
        assertEquals(user.getEmail(), userRecord.email());
    }

    @Test
    @DisplayName("Test UserDTO to User")
    void testUserDTOToUser() {
        UserDTO userDTO = UserMapper.INSTANCE.userToUserDTO(user);
        User userFromDTO = UserMapper.INSTANCE.userDTOToUser(userDTO);
        assertEquals(userDTO.getName(), userFromDTO.getName());
        assertEquals(userDTO.getEmail(), userFromDTO.getEmail());
    }

    @Test
    @DisplayName("Test UserRecord to User")
    void testUserRecordToUser() {
        UserRecord userRecord = UserMapper.INSTANCE.userToUserRecord(user);
        User userFromRecord = UserMapper.INSTANCE.userRecordToUser(userRecord);
        assertEquals(userRecord.name(), userFromRecord.getName());
        assertEquals(userRecord.email(), userFromRecord.getEmail());
    }

}
Enter fullscreen mode Exit fullscreen mode

Se pueden ejecutar los tests desde el propio IDE o usando el comando mvn test desde la línea de comandos.

Conclusiones

Como hemos visto crear mapeos entre objetos es muy sencillo utilizando MapStruct. Además, al ejecutarse en tiempo de compilación proporciona un mejor rendimiento y una comprobación de errores más rápida. En este ejemplo hemos visto cómo mapear una entidad a un DTO y viceversa, así como realizar pruebas unitarias para verificar que los mapeos se están realizando correctamente, puede que los mapeos del ejemplo sean simples, pero funciona de la misma manera con mapeos más complejos.

Puedes consultar el código fuente de este ejemplo en mi repositorio de GitHub.

Top comments (0)