DTO
(DTO — Data Transfer Object)
Un DTO (Data Transfer Object) es una clase ligera usada para transportar datos entre capas (por ejemplo, entre la capa de persistencia y la capa de presentación). Su objetivo es:
- Evitar exponer entidades JPA directamente en la API.
- Reducir la cantidad de datos transferidos (seleccionar sólo lo necesario).
- Desacoplar la representación externa (API) del modelo de persistencia.
Cuándo usar DTOs:
- En respuestas de API públicas (REST) para no devolver entidades gestionadas.
- Cuando la vista necesita una combinación de campos de varias entidades.
- Para mejorar seguridad y control sobre los campos expuestos.
Dependencia (Maven)
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.1</version>
</dependency>
Ejemplo simple
1) Entidad User
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
// getters/setters
}
2) DTO UserDto
public class UserDto {
private Long id;
private String name;
// no incluimos email por ejemplo
// getters/setters
}
3) Bean de configuración de ModelMapper (Spring)
@Configuration
public class ModelMapperConfig {
@Bean
public ModelMapper modelMapper() {
ModelMapper mapper = new ModelMapper();
// configuración opcional, ej. ignorar nulls, políticas de matching, etc.
return mapper;
}
}
4) Uso en un Service para mapear entidad -> DTO
@Service
public class UserService {
private final UserRepository repo;
private final ModelMapper mapper;
public UserService(UserRepository repo, ModelMapper mapper) {
this.repo = repo;
this.mapper = mapper;
}
public UserDto findById(Long id) {
User user = repo.findById(id).orElseThrow();
return mapper.map(user, UserDto.class);
}
public List<UserDto> findAll() {
return repo.findAll()
.stream()
.map(u -> mapper.map(u, UserDto.class))
.collect(Collectors.toList());
}
}
Mapeo de colecciones
ModelMapper puede mapear listas con streams (como en el ejemplo). También se puede usar TypeToken para mapear listas directamente:
Type listType = new TypeToken<List<UserDto>>() {}.getType();
List<UserDto> dtos = mapper.map(userList, listType);
Mapeos personalizados (campos anidados o nombres distintos)
Si el DTO y la entidad no tienen nombres compatibles, se puede usar PropertyMap:
PropertyMap<User, UserDto> userMap = new PropertyMap<>() {
@Override
protected void configure() {
map().setName(source.getName());
// map nested: map().setCity(source.getAddress().getCity());
}
};
mapper.addMappings(userMap);
O usar TypeMap para reglas más sofisticadas:
TypeMap<User, UserDto> typeMap = mapper.createTypeMap(User.class, UserDto.class);
typeMap.addMappings(m -> m.map(src -> src.getName(), UserDto::setName));
Mapear sobre un objeto existente (actualizar DTO -> entidad o viceversa)
Ignorar nulos
Para no sobrescribir campos con null al mapear sobre un objeto existente:
Notas prácticas y buenas prácticas
- Mantener los DTOs lo más simples posible: únicamente campos que la vista necesite.
- Usar mapeos personalizados para casos donde la estructura difiere (campos anidados, renombrados, formatos).
- Para mapeos complejos o mucho tráfico, considerar escribir conversiones manuales (mayor control y rendimiento).
- No exponer entidades JPA en APIs públicas; usar DTOs para evitar lazy-loading y problemas de serialización.
Si quieres, añado ejemplos concretos adaptados a las entidades que ya tienes en el proyecto (por ejemplo Post/Comment o Student/Course).