Patrón de objetos de transferencia de datos en Java: implementación y mapeo

En este tutorial, implementaremos el patrón de objetos de transferencia de datos en una aplicación Java Spring Boot. También cubriremos ejemplos de entidades de mapeo a DTO.

Introducción

Una aplicación empresarial es una solución de software creada para las necesidades de una organización. A menudo es un sistema escalable, de varios niveles y a gran escala. El software empresarial puede manejar una gran cantidad de datos complejos y es importante que este tipo de software tenga una buena arquitectura.

Los patrones de arquitectura de aplicaciones empresariales son soluciones estandarizadas para problemas comunes que se encuentran en sistemas grandes. Cultivan el pensamiento arquitectónico y ayudan a los desarrolladores a tener más confianza en la construcción de sistemas con confiabilidad comprobada.

Las aplicaciones empresariales pueden encargarse de manipular, mostrar o almacenar grandes cantidades de datos. Evitar el acoplamiento estrecho y garantizar la integridad/seguridad de los datos no debe ser una idea de último momento cuando se trabaja en estas aplicaciones.

Objeto de transferencia de datos

El Patrón de diseño de objetos de transferencia de datos es uno de los patrones de arquitectura de aplicaciones empresariales que exige el uso de objetos que agregan y encapsulan datos para la transferencia. Un Objeto de Transferencia de Datos es, esencialmente, como una estructura de datos. No debe contener ninguna lógica comercial, pero debe contener mecanismos de serialización y deserialización.

Los DTO pueden contener todos los datos de un origen o datos parciales. También pueden contener datos de fuentes únicas o múltiples. Cuando se implementan, los DTO se convierten en el medio de transporte de datos entre sistemas.

Martin Fowler describe el Objeto de transferencia de datos en su famoso libro Patrones de arquitectura de aplicaciones empresariales. Allí, la idea principal de los DTO es reducir la cantidad de llamadas remotas que son costosas.

Martin Fowler también define un objeto ensamblador, utilizado para convertir datos entre el DTO y cualquier objeto de entidad. Hoy en día, usamos mappers para ese propósito.

Lo que vale la pena señalar es que la aplicación del patrón Objeto de transferencia de datos puede convertirse en un antipatrón en los sistemas locales. Está destinado a ser utilizado en llamadas remotas para promover la seguridad y el acoplamiento flexible. Si se aplica a los sistemas locales, es solo un diseño excesivo de una característica simple.

Motivación

Supongamos que tenemos que desarrollar un sistema empresarial para una empresa. El sistema incluirá una base de datos con diversa información general sobre los empleados: salario, proyectos, certificados, datos personales (dirección, estado civil, número de teléfono, etc.).

La seguridad en la entrada de la empresa requiere acceso a nuestro sistema, para identificar al trabajador que quiere ingresar. Necesitan algunos datos rudimentarios, como el apellido y la foto del trabajador.

No queremos enviar otra información confidencial al sistema de seguridad, como información personal. Es redundante y expone el canal de comunicación entre los sistemas a los ataques. Solo proporcionaremos lo necesario y el alcance de los datos se definirá en un DTO.

En las aplicaciones Java, usamos clases de entidad para representar tablas en una base de datos relacional. Sin DTO, tendríamos que exponer todas las entidades a una interfaz remota. Esto provoca un fuerte acoplamiento entre una API y un modelo de persistencia.

Al usar un DTO para transferir solo la información requerida, aflojamos el acoplamiento entre la API y nuestro modelo, lo que nos permite mantener y escalar el servicio más fácilmente.

Implementación de un objeto de transferencia de datos

Hagamos una aplicación que se encargue del seguimiento de la ubicación de tus amigos. Construiremos una aplicación Spring Boot que expone una API REST. Utilizándolo, podremos recuperar las ubicaciones de los usuarios de una base de datos H2.

If you'd like to read on Integración de una base de datos H2 con Spring Boot, we've got you covered!

Configuración de Spring Boot

La forma más fácil de comenzar con una aplicación Spring Boot en blanco es usar Spring Initializr:

spring boot initializr

Alternativamente, también puede usar la CLI de arranque de primavera para arrancar la aplicación:

1
$ spring init --dependencies=h2 data-transfer-object-demo

Si ya tiene una aplicación Maven/Spring, agregue la dependencia a su archivo pom.xml:

1
2
3
4
5
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>${version}</version>
</dependency>

O si estás usando Gradle:

1
compile group: 'com.h2database', name: 'h2', version: '${version}'

Aplicación de demostración

Comencemos con el modelo User:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String firstName;
    private String lastName;
    private String password;
    private String email;
        
    @ManyToOne(fetch = FetchType.EAGER, optional = false)
    @JoinColumn(name = "location_id")
    private Location location;
        
    // Getters and Setters
}

Contiene información rudimentaria como nombre de usuario, nombre, correo electrónico, etc. También tiene una relación de muchos a uno con la entidad Ubicación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Entity
public class Location {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private double lat;
    private double lng;
    private String place;
    private String description;
    
        // Getters and Setters
}

Para las operaciones CRUD básicas, nos basaremos en el confiable CrudRepository proporcionado por Spring Boot:

1
2
@Repository
public interface UserRepository extends CrudRepository<User, Long>{}
1
2
@Repository
public interface LocationRepository extends CrudRepository<Location, Long> {}

Si no estás seguro de cómo funcionan, te sugerimos leer nuestra Guía de Spring Data JPA. En resumen, nos ayudarán con la funcionalidad CRUD básica para nuestros modelos.

En este punto, nos gustaría crear un controlador que maneje una solicitud GET y devuelva una lista de las ubicaciones de los usuarios. Sin embargo, si recuperamos los objetos Usuario y Ubicación de nuestra base de datos y simplemente imprimimos la información requerida, la otra información, como la contraseña, también estará contenida en ese objeto. No lo imprimiremos, pero estará allí.

Hagamos un Objeto de transferencia de datos para transferir solo la información requerida. Y mientras estamos en eso, agreguemos la información de ‘Usuario’ y ‘Ubicación’, para que los datos se transfieran juntos:

1
2
3
4
5
6
7
8
9
public class UserLocationDTO {
    private Long userId;
    private String username;
    private double lat;
    private double lng;
    private String place;
    
    // Getters and Setters
} 

Este objeto ahora contiene toda la información que queremos mostrar al usuario final. Ahora, necesitaremos una forma de asignar los objetos Usuario y Ubicación en un solo objeto UserLocationDTO. Esto normalmente se hace a través de herramientas de mapeo, como MapStruct o ModelMapper, que exploraremos en las últimas secciones.

Por ahora, realicemos la conversión manualmente. Dado que necesitaremos un servicio que llame a nuestro UserRepository, también asignaremos los resultados allí y devolveremos los DTO:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class MapService {

    @Autowired
    private UserRepository userRepository;

    public List<UserLocationDTO> getAllUsersLocation() {
        return ((List<User>) userRepository
                .findAll())
                .stream()
                .map(this::convertToUserLocationDTO)
                        .collect(Collectors.toList());
    }

    private UserLocationDTO convertToUserLocationDTO(User user) {
        UserLocationDTO userLocationDTO = new UserLocationDTO();
        userLocationDTO.setUserId(user.getId());
        userLocationDTO.setUsername(user.getUsername());
        Location location = user.getLocation();
        userLocationDTO.setLat(location.getLat());
        userLocationDTO.setLng(location.getLng());
        userLocationDTO.setPlace(location.getPlace());
        return userLocationDTO;
}

Al recuperar una lista de ‘Usuarios’, los convertimos directamente, junto con su información de ‘Ubicación’, en objetos ‘UserLocationDTO’. Al llamar a este servicio, recuperaremos esta lista de DTO.

Finalmente, hagamos un punto final /map para permitir que alguien recupere la ubicación de los usuarios:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@RestController
public class MapController {
  
    @Autowired
    private MapService mapService;

    @GetMapping("/map")
    @ResponseBody
    public List<UserLocationDTO> getAllUsersLocation() {
        List <UserLocationDTO> usersLocation = mapService.getAllUsersLocation();
        return usersLocation;
    }
}

Este punto final solo devuelve un @ResponseBody. Puede ser llamado por un usuario o por otro servicio que analiza los resultados.

Carguemos nuestra base de datos con información ficticia para fines de prueba:

1
2
3
insert into location(id, lat, lng, place, description) values (1, 49.8, 24.03 ,'Lviv', 'Lviv is one of the largest and the most beautiful cities of Ukraine.');
insert into user(id, username, first_name, last_name, password, location_id) values (1, 'Romeo', 'Romeo', 'Montagues' ,'gjt6lf2nt5os', 1);
insert into user(id, username, first_name, last_name, password, location_id) values (2, 'Juliet', 'Juliet', 'Capulets' ,'s894mjg03hd0', 1);

Ahora, para probar nuestro punto final, usaremos una herramienta como Postman para llegar a nuestros puntos finales:

postman results of spring boot rest endpoint

¡Excelente! Se devuelve una lista de nuestros usuarios con solo la información requerida, tanto transferida como mostrada.

Hemos escrito un método de mapeo dentro de nuestro MapService que agrega y convierte datos, sin embargo, este proceso se puede automatizar fácilmente.

Mapeo con ModelMapper

ModelMapper es una gran biblioteca de mapeo que nos permite mapear entre modelos y DTO. Facilita el mapeo de objetos al determinar automáticamente cómo un modelo de objeto se asigna a otro.

Para agregarlo a un proyecto Maven, agregaríamos la dependencia:

1
2
3
4
5
<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>${version}</version>
</dependency>

O, si estás usando Gradle:

1
compile group: 'org.modelmapper', name: 'modelmapper', version: '${version}'

Actualicemos nuestro ejemplo anterior con la biblioteca ModelMapper:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Service
public class MapService {

    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private ModelMapper modelMapper;

    public List<UserLocationDTO> getAllUsersLocation() {
       return ((List<User>) userRepository
                .findAll())
                .stream()
                .map(this::convertToUserLocationDTO)
                .collect(Collectors.toList());
    }

    private UserLocationDTO convertToUserLocationDTO(User user) { 
        modelMapper.getConfiguration()
                .setMatchingStrategy(MatchingStrategies.LOOSE);
        UserLocationDTO userLocationDTO = modelMapper
                .map(user, UserLocationDTO.class);  
        return userLocationDTO;
    }
}

Ahora, en lugar de todo el proceso de asignación que hemos tenido que hacer antes, simplemente ‘asignamos()’ un ‘usuario’ a ‘UserLocationDTO’. El método aplanará las propiedades de User dentro de UserLocationDTO y tanto la información del usuario como la ubicación estarán presentes.

Nota: Cuando se trabaja con objetos como propiedades, como nuestra Ubicación es una propiedad de Usuario, es posible que el comparador estándar de la biblioteca no pueda hacer coincidir todas las propiedades. Hemos establecido la estrategia de coincidencia en ‘SUELTO’ para que sea más fácil para la biblioteca ubicar y hacer coincidir las propiedades.

Mapeo con MapStruct

MapStruct es un generador de código basado en Java de código abierto que crea código para implementaciones de mapas.

Utiliza el procesamiento de anotaciones para generar implementaciones de clase de mapeador durante la compilación y reduce en gran medida la cantidad de código repetitivo que normalmente se escribiría a mano.

Si está utilizando Maven, instale MapStruct agregando la dependencia:

1
2
3
4
5
6
7
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>

Esta dependencia importará las anotaciones principales de MapStruct. Dado que MapStruct funciona en tiempo de compilación y está adjunto a constructores como Maven y Gradle, también tendremos que agregar un complemento a <build>:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Si está usando Gradle, instalar MapStruct es tan simple como:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
plugins {
    id 'net.ltgt.apt' version '0.20'
}

// Depending on your IDE
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'

dependencies {
    compile "org.mapstruct:mapstruct:${mapstructVersion}"
    annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}

Ya tenemos nuestras clases Usuario y Ubicación, así que hagamos un mapeador para ellas:

1
2
3
4
5
6
7
@Mapper
public interface UserLocationMapper {
    UserLocationMapper INSTANCE = Mappers.getMapper(UserLocationMapper.class);

    @Mapping(source = "user.id", target = "userId")
    UserLocationDTO toDto(User user, Location location);
}

Cuando construyas el proyecto, MapStruct tomará este @Mapper y generará una clase UserLocationMapperImpl con una implementación completamente funcional.

MapStruct tiene una amplia variedad de funcionalidades y un conjunto avanzado de funciones. Si está interesado en leer más al respecto, le sugerimos leer nuestra detallada Guía de MapStruct en Java .

Conclusión

En este artículo, revisamos el patrón de diseño de objetos de transferencia de datos con sus ventajas y desventajas. Este patrón está realmente dedicado solo para llamadas remotas porque la conversión desde y hacia DTO puede ser costosa.

Además, creamos una aplicación Spring Boot de demostración y exploramos dos mapeadores populares que se pueden usar para simplificar el proceso de mapeo entre modelos y DTO.

Puede encontrar todo el código del proyecto en GitHub. dto).