Publicado el 2025-04-19
Escrito por C. V. Charco
Supongamos que tenemos una aplicación cualquiera en Spring que recibe un DTO del controlador (un CreateAccountDto) y necesitamos transformarlo a la entidad correspondiente Account para poder almacenarlo en base de datos. Veamos la implementación de estas dos clases rápidamente.
// accounts/Account.java
// Nota: estamos usando Hibernate 6+ que ofrece mayor compatibilidad con Java records
@Entity
@Table(name = "accounts")
public record Account (
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id,
@Column(name = "username", nullable = false, unique = true) String username,
@Column(name = "password", nullable = false) String password,
@Column(name = "email", nullable = false, updatable = true) String email,
@CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) LocalDateTime createdAt,
@UpdateTimestamp @Column(name = "updated_at", nullable = false) LocalDateTime updatedAt
) {}
// accounts/dto/CreateAccountDto.java
public record CreateAccountDto (
@NotBlank String username,
@NotBlank @Size(min = 8, max = 40) String password,
@NotBlank @Email String email
) {}
Cuando mandamos el DTO del controlador al servicio, este último debe convertirlo en un objeto de entidad para poder trabajar e interactuar con la lógica de negocio de nuestra aplicación. Por ejemplo, para almacenar el nuevo registro en la base de datos. Para hacer esto necesitamos escribir manualmente la lógica para convertir entre ambos tipos.
// accounts/AccountService.java
public Account createAccount(CreateaAccountDto createAccountDto) {
Account account = new Account(
null, // id
createAccountDto.username,
createAccountDto.password, // En aplicación real debemos cifrar contraseña (Ej: Bcrypt)
createAccountDto.email,
null, // createdAt
null // updatedAt
);
// Guardar Account en base de datos
Account response = accountRepository.save(account);
return response;
}
Si bien este ejemplo es muy sencillo ya que la entidad tiene muy pocos campos, se puede comenzar a entender los problemas que conlleva hacerlo así.
Una solución simple consistiría en crear en el DTO (o la entidad) funciones de conversión entre estos, de la siguiente manera:
// accounts/dto/CreateAccountDto.java
public record CreateAccountDto (
@NotBlank String username,
@NotBlank @Size(min = 8, max = 40) String password,
@NotBlank @Email String email
) {
public Account toAccount() {
// Aquí podemos incluso cifrar la contraseña
String hashedPassword = encriptar(this.password());
return new Account(
null, // id
this.username(),
hashedPassword,
this.email(),
null, // createdAt
null // updatedAt
);
}
}
Entonces en nuestro servicio simplemente haríamos lo siguiente:
// accounts/AccountService.java
public Account createAccount(CreateaAccountDto createAccountDto) {
Account account = createAccountDto.toAccount();
// Guardar Account en base de datos
Account response = accountRepository.save(account);
return response;
}
Este enfoque es mucho más mantenible y limpio, ya que centralizamos en un único lugar la conversión entre DTOs y entidades. Sin embargo, todavía tenemos que implementar manualmente la lógica de conversión en cada DTO (o centralizarla en un mapper) y actualizar esta lógica cada vez que agregamos campos a la entidad y al DTO. Entonces, ¿cómo puedo simplificarlo todavía más y promover una mayor mantenibilidad?
MapStruct es una biblioteca de Java que permite mapear automáticamente objetos entre clases, especialmente útil para convertir entre entidades y DTOs, usando generación de código en tiempo de compilación, lo que lo hace muy rápido y sin reflexión.
Para poder continuar, debemos agregar las siguientes dependencias al pom.xml de nuestro proyecto.
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>
<!-- Compatibilidad con Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
<scope>provided</scope>
</dependency>
Con MapStruct nosotros simplemente crearemos una interfaz que defina qué conversiones queremos que se implementen automáticamente y entre qué DTOs y entidades. Veamos un ejemplo. Crearemos una clase accounts/AccountsMapper.java con el siguiente código.
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
// Con @Mapper le decimos a MapStruct que debe usar esta interfaz para generar el código de conversión
// Es importante usar `component = spring` para que Spring pueda inyectarlo como un componente en otras clases
@Mapper(componentModel = "spring")
public interface UserMapper {
// Indicamos los campos que no deben mapearse (están presentes en la entidad, pero no en el DTO)
@Mappings({
@Mapping(target = "id", ignore = true),
@Mapping(target = "createdAt", ignore = true),
@Mapping(target = "updatedAt", ignore = true)
})
Account createDtoToEntity(CreateAccountDto dto);
ResponsAccountDto entityToResponseDto(Account account);
// Actualiza la entidad con los datos no nulos del dto (útil para el update)
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateAccountDtoToEntity(UpdateAccountDto dto, @MappingTarget Account account);
}
Si observas, solo hemos declarado firmas de los métodos, no los hemos implementado, de esto se encarga MapStruct. Esto nos provee dos beneficios:
Ahora para usar este mapper en nuestro servicio el procedimiento es muy parecido.
// accounts/AccountService.java
@Autowired
private final AccountMapper accountMapper; // Inyectamos el mapper
public Account createAccount(CreateaAccountDto createAccountDto) {
Account account = accountMapper.createDtoToEntity(createAccountDto);
// Guardar Account en base de datos
Account response = accountRepository.save(account);
return response;
}
Hasta aquí hemos visto el funcionamiento básico de MapStruct, pero que ya nos va a permitir trabajar en una gran variedad de escenarios distintos y que nos va a ayudar a mantener un código más limpio, mantenible y optimizado. Una vez que domines todo esto, te recomiendo que eches un ojo a la documentación de MapStruct para ver todas las posibilidades que ofrece. ¡Nos leemos!