4. DTOs y Mapeo de Datos
🎯 Objetivos
En esta sección aprenderás a: - Entender qué son los DTOs y por qué son importantes - Crear DTOs para cada entidad del sistema - Implementar métodos de conversión entre entidades y DTOs - Aplicar mejores prácticas en el mapeo de datos - Separar la capa de presentación de la capa de persistencia
📋 Prerrequisitos
- Entidades JPA creadas (sección anterior)
- Conocimientos básicos de Java
- Comprensión de patrones de diseño
🤔 ¿Qué son los DTOs?
Definición
DTO (Data Transfer Object) es un patrón de diseño que se utiliza para transferir datos entre diferentes capas de una aplicación o entre diferentes sistemas.
¿Por qué usar DTOs?
1. Separación de Responsabilidades
// ❌ Malo: Exponer entidad directamente
@GetMapping
public List<User> getUsers() {
return userService.findAll(); // Expone password, relaciones, etc.
}
// ✅ Bueno: Usar DTO
@GetMapping
public List<UserDTO> getUsers() {
return userService.findAll().stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
2. Control de Datos Expuestos
- Entidad: Contiene todos los campos, incluyendo sensibles
- DTO: Solo contiene campos que deben ser expuestos
3. Evitar Referencias Circulares
- Las entidades pueden tener relaciones bidireccionales
- Los DTOs usan IDs en lugar de objetos completos
4. Versionado de API
- Cambios en entidades no afectan la API
- Múltiples DTOs para diferentes versiones
5. Validación Específica
- Validaciones diferentes para creación vs actualización
- Campos obligatorios según el contexto
👤 UserDTO (Usuario)
Crea el archivo src/main/java/com/example/pib2/models/dtos/UserDTO.java:
package com.example.pib2.models.dtos;
import lombok.Data;
@Data
public class UserDTO {
private Long id;
private String username;
private String email;
// Nota: NO incluimos password por seguridad
// Nota: NO incluimos loans para evitar referencias circulares
}
🔍 Análisis del UserDTO
Campos Incluidos
- id: Identificador único
- username: Nombre de usuario público
- email: Email del usuario
Campos Excluidos
- password: Información sensible que nunca debe exponerse
- role: Podría incluirse según los requerimientos
- loans: Lista de préstamos (evita referencias circulares)
Ventajas
// La respuesta JSON será limpia:
{
"id": 1,
"username": "john_doe",
"email": "john@example.com"
}
// Sin password, sin relaciones complejas
📦 ItemDTO (Artículo)
Crea el archivo src/main/java/com/example/pib2/models/dtos/ItemDTO.java:
package com.example.pib2.models.dtos;
import lombok.Data;
@Data
public class ItemDTO {
private Long id;
private String name;
private String description;
private int quantity;
// Nota: NO incluimos loans para evitar referencias circulares
}
🔍 Análisis del ItemDTO
Campos Incluidos
- id: Identificador único
- name: Nombre del artículo
- description: Descripción detallada
- quantity: Cantidad disponible
Campos Excluidos
- loans: Lista de préstamos (evita complejidad)
Ejemplo de Uso
{
"id": 1,
"name": "Laptop Dell",
"description": "Laptop para desarrollo",
"quantity": 5
}
📋 LoanDTO (Préstamo)
Crea el archivo src/main/java/com/example/pib2/models/dtos/LoanDTO.java:
package com.example.pib2.models.dtos;
import lombok.Data;
import java.time.LocalDate;
@Data
public class LoanDTO {
private Long id;
private Long itemId; // ID en lugar del objeto completo
private Long userId; // ID en lugar del objeto completo
private LocalDate loanDate;
private LocalDate returnDate;
private boolean returned;
// Nota: NO incluimos histories para evitar complejidad
}
🔍 Análisis del LoanDTO
Uso de IDs en lugar de Objetos
// ❌ En la entidad (relaciones completas)
private User user;
private Item item;
// ✅ En el DTO (solo IDs)
private Long userId;
private Long itemId;
Ventajas de usar IDs
- Simplicidad: JSON más limpio
- Performance: No carga objetos relacionados
- Flexibilidad: El cliente decide si necesita más datos
- Evita ciclos: No hay referencias circulares
Ejemplo de JSON
{
"id": 1,
"itemId": 5,
"userId": 3,
"loanDate": "2024-01-15",
"returnDate": "2024-01-30",
"returned": false
}
📊 LoanHistoryDTO (Historial de Préstamos)
Crea el archivo src/main/java/com/example/pib2/models/dtos/LoanHistoryDTO.java:
package com.example.pib2.models.dtos;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class LoanHistoryDTO {
private Long id;
private Long loanId; // ID del préstamo relacionado
private LocalDateTime actionDate;
private String action; // e.g., "CREATED", "RETURNED"
}
🔍 Análisis del LoanHistoryDTO
Campos de Auditoría
- actionDate: Timestamp completo con hora
- action: Tipo de acción realizada
Ejemplo de JSON
{
"id": 1,
"loanId": 5,
"actionDate": "2024-01-15T10:30:00",
"action": "CREATED"
}
🔄 Métodos de Conversión
Patrón de Mapeo Manual
En cada controlador, implementamos métodos para convertir entre entidades y DTOs:
UserController - Métodos de Conversión
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
// Convertir de Entidad a DTO
private UserDTO toDTO(User user) {
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setUsername(user.getUsername());
dto.setEmail(user.getEmail());
// NO incluimos password ni loans
return dto;
}
// Convertir de DTO a Entidad
private User toEntity(UserDTO dto) {
User user = new User();
user.setId(dto.getId());
user.setUsername(dto.getUsername());
user.setEmail(dto.getEmail());
// password y role se manejan por separado
return user;
}
// Uso en endpoints
@GetMapping
public List<UserDTO> getAll() {
return userService.findAll().stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
@PostMapping
public UserDTO create(@RequestBody UserDTO userDTO) {
User user = toEntity(userDTO);
User saved = userService.save(user);
return toDTO(saved);
}
}
ItemController - Métodos de Conversión
@RestController
@RequestMapping("/api/items")
public class ItemController {
@Autowired
private ItemService itemService;
private ItemDTO toDTO(Item item) {
ItemDTO dto = new ItemDTO();
dto.setId(item.getId());
dto.setName(item.getName());
dto.setDescription(item.getDescription());
dto.setQuantity(item.getQuantity());
return dto;
}
private Item toEntity(ItemDTO dto) {
Item item = new Item();
item.setId(dto.getId());
item.setName(dto.getName());
item.setDescription(dto.getDescription());
item.setQuantity(dto.getQuantity());
return item;
}
@GetMapping
public List<ItemDTO> getAll() {
return itemService.findAll().stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
}
LoanController - Métodos de Conversión Complejos
@RestController
@RequestMapping("/api/loans")
public class LoanController {
@Autowired
private LoanService loanService;
@Autowired
private ItemService itemService;
@Autowired
private UserService userService;
private LoanDTO toDTO(Loan loan) {
LoanDTO dto = new LoanDTO();
dto.setId(loan.getId());
// Convertir objetos a IDs
dto.setItemId(loan.getItem() != null ? loan.getItem().getId() : null);
dto.setUserId(loan.getUser() != null ? loan.getUser().getId() : null);
dto.setLoanDate(loan.getLoanDate());
dto.setReturnDate(loan.getReturnDate());
dto.setReturned(loan.isReturned());
return dto;
}
private Loan toEntity(LoanDTO dto) {
Loan loan = new Loan();
loan.setId(dto.getId());
// Convertir IDs a objetos (con validación)
if (dto.getItemId() != null) {
Optional<Item> item = itemService.findById(dto.getItemId());
item.ifPresent(loan::setItem);
}
if (dto.getUserId() != null) {
Optional<User> user = userService.findById(dto.getUserId());
user.ifPresent(loan::setUser);
}
loan.setLoanDate(dto.getLoanDate());
loan.setReturnDate(dto.getReturnDate());
loan.setReturned(dto.isReturned());
return loan;
}
}
LoanHistoryController - Métodos de Conversión
@RestController
@RequestMapping("/api/loanhistories")
public class LoanHistoryController {
@Autowired
private LoanHistoryService loanHistoryService;
@Autowired
private LoanService loanService;
private LoanHistoryDTO toDTO(LoanHistory history) {
LoanHistoryDTO dto = new LoanHistoryDTO();
dto.setId(history.getId());
dto.setLoanId(history.getLoan() != null ? history.getLoan().getId() : null);
dto.setActionDate(history.getActionDate());
dto.setAction(history.getAction());
return dto;
}
private LoanHistory toEntity(LoanHistoryDTO dto) {
LoanHistory history = new LoanHistory();
history.setId(dto.getId());
if (dto.getLoanId() != null) {
Optional<Loan> loan = loanService.findById(dto.getLoanId());
loan.ifPresent(history::setLoan);
}
history.setActionDate(dto.getActionDate());
history.setAction(dto.getAction());
return history;
}
}
📁 Estructura de Directorios
Organiza tus DTOs de la siguiente manera:
src/main/java/com/example/pib2/
├── models/
│ ├── entities/
│ │ ├── User.java
│ │ ├── Item.java
│ │ ├── Loan.java
│ │ └── LoanHistory.java
│ └── dtos/
│ ├── UserDTO.java
│ ├── ItemDTO.java
│ ├── LoanDTO.java
│ └── LoanHistoryDTO.java
├── controllers/
├── services/
└── repositories/
🎨 Mejores Prácticas
1. Nomenclatura Consistente
✅ Bueno:
// Entidad
public class User { }
// DTO correspondiente
public class UserDTO { }
// Métodos de conversión
private UserDTO toDTO(User user) { }
private User toEntity(UserDTO dto) { }
2. Validación en DTOs
@Data
public class UserDTO {
private Long id;
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50)
private String username;
@Email(message = "Email should be valid")
@NotBlank(message = "Email is required")
private String email;
}
3. DTOs Específicos por Operación
// Para creación (sin ID)
public class CreateUserDTO {
@NotBlank
private String username;
@Email
private String email;
@NotBlank
@Size(min = 6)
private String password;
}
// Para actualización (con ID)
public class UpdateUserDTO {
@NotNull
private Long id;
private String username; // Opcional
private String email; // Opcional
}
// Para respuesta (sin password)
public class UserResponseDTO {
private Long id;
private String username;
private String email;
private LocalDateTime createdAt;
}
4. Manejo de Nulos
private LoanDTO toDTO(Loan loan) {
if (loan == null) {
return null;
}
LoanDTO dto = new LoanDTO();
dto.setId(loan.getId());
// Verificar nulos antes de acceder a propiedades
dto.setItemId(loan.getItem() != null ? loan.getItem().getId() : null);
dto.setUserId(loan.getUser() != null ? loan.getUser().getId() : null);
return dto;
}
5. Uso de Optional
private Loan toEntity(LoanDTO dto) {
Loan loan = new Loan();
// Usar Optional para manejo seguro
Optional.ofNullable(dto.getItemId())
.flatMap(itemService::findById)
.ifPresent(loan::setItem);
Optional.ofNullable(dto.getUserId())
.flatMap(userService::findById)
.ifPresent(loan::setUser);
return loan;
}
🔧 Alternativas de Mapeo
1. MapStruct (Recomendado para proyectos grandes)
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "password", ignore = true)
@Mapping(target = "loans", ignore = true)
UserDTO toDTO(User user);
@Mapping(target = "password", ignore = true)
@Mapping(target = "loans", ignore = true)
User toEntity(UserDTO dto);
List<UserDTO> toDTOList(List<User> users);
}
2. ModelMapper
@Service
public class MappingService {
private final ModelMapper modelMapper;
public MappingService() {
this.modelMapper = new ModelMapper();
configureMapper();
}
private void configureMapper() {
// Configurar mapeos específicos
modelMapper.typeMap(User.class, UserDTO.class)
.addMappings(mapper -> {
mapper.skip(UserDTO::setPassword);
mapper.skip(UserDTO::setLoans);
});
}
public UserDTO toDTO(User user) {
return modelMapper.map(user, UserDTO.class);
}
}
3. Mapeo Manual (Usado en nuestro proyecto)
Ventajas: - Control total sobre el mapeo - Sin dependencias adicionales - Fácil debugging - Flexibilidad máxima
Desventajas: - Más código para mantener - Propenso a errores manuales - Repetitivo para entidades simples
✅ Verificación del Mapeo
1. Prueba de Endpoints
# Crear un usuario
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{
"username": "john_doe",
"email": "john@example.com"
}'
# Respuesta esperada (sin password)
{
"id": 1,
"username": "john_doe",
"email": "john@example.com"
}
2. Verificar Referencias
# Crear un préstamo
curl -X POST http://localhost:8080/api/loans \
-H "Content-Type: application/json" \
-d '{
"itemId": 1,
"userId": 1,
"loanDate": "2024-01-15",
"returnDate": "2024-01-30",
"returned": false
}'
# Respuesta esperada (con IDs, no objetos completos)
{
"id": 1,
"itemId": 1,
"userId": 1,
"loanDate": "2024-01-15",
"returnDate": "2024-01-30",
"returned": false
}
🚨 Problemas Comunes y Soluciones
Error: "StackOverflowError" en JSON
Causa: Referencias circulares en entidades
Solución: Usar DTOs con IDs
// ❌ Malo: Exponer entidad con relaciones
@GetMapping
public List<User> getUsers() {
return userService.findAll();
}
// ✅ Bueno: Usar DTO
@GetMapping
public List<UserDTO> getUsers() {
return userService.findAll().stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
Error: "NullPointerException" en conversión
Causa: No validar nulos
Solución: Verificar antes de acceder
private LoanDTO toDTO(Loan loan) {
if (loan == null) return null;
LoanDTO dto = new LoanDTO();
dto.setItemId(loan.getItem() != null ? loan.getItem().getId() : null);
return dto;
}
Error: "Entity not found" al convertir DTO a Entity
Causa: ID referenciado no existe
Solución: Usar Optional y manejar casos
if (dto.getItemId() != null) {
Optional<Item> item = itemService.findById(dto.getItemId());
if (item.isPresent()) {
loan.setItem(item.get());
} else {
throw new EntityNotFoundException("Item not found: " + dto.getItemId());
}
}
Error: "Validation failed" en DTOs
Causa: Datos inválidos en DTO
Solución: Agregar validaciones apropiadas
@Data
public class UserDTO {
@NotBlank(message = "Username cannot be blank")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@Email(message = "Email must be valid")
@NotBlank(message = "Email cannot be blank")
private String email;
}
📚 Conceptos Clave Aprendidos
- DTOs: Objetos para transferir datos entre capas
- Separación de responsabilidades: Entidades vs DTOs
- Mapeo manual: Control total sobre la conversión
- Referencias por ID: Evitar ciclos y complejidad
- Validación: Datos seguros en la capa de presentación
- Manejo de nulos: Código robusto y seguro
- Patrones de conversión: toDTO() y toEntity()
🎯 Próximos Pasos
En la siguiente sección aprenderás a: - Crear repositorios JPA - Implementar consultas personalizadas - Usar Spring Data JPA - Manejar transacciones
← Anterior: Entidades y Modelos | Volver al Índice | Siguiente: Repositorios →