3. Entidades y Modelos de Datos
🎯 Objetivos
En esta sección aprenderás a: - Diseñar el modelo de datos del sistema de inventario - Crear entidades JPA con anotaciones - Definir relaciones entre entidades - Usar Lombok para reducir código boilerplate - Manejar referencias circulares con Jackson - Implementar buenas prácticas en el diseño de entidades
📋 Prerrequisitos
- Proyecto Spring Boot configurado
- Base de datos PostgreSQL configurada
- Conocimientos básicos de JPA/Hibernate
- Comprensión de relaciones de base de datos
🏗️ Diseño del Modelo de Datos
Diagrama de Entidades
Nuestro sistema de inventario tendrá las siguientes entidades:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User │ │ Item │ │ Loan │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ id (PK) │ │ id (PK) │ │ id (PK) │
│ username │ │ name │ │ item_id(FK) │
│ email │ │ description │ │ user_id(FK) │
│ password │ │ quantity │ │ loanDate │
│ role │ │ │ │ returnDate │
└─────────────┘ └─────────────┘ │ returned │
│ │ └─────────────┘
│ │ │
│ │ │
└───────────────────────┼─────────────────────┘
│
│
┌─────────────┐
│ LoanHistory │
├─────────────┤
│ id (PK) │
│ loan_id(FK) │
│ actionDate │
│ action │
└─────────────┘
Relaciones
- User ↔ Loan: Un usuario puede tener múltiples préstamos (1:N)
- Item ↔ Loan: Un item puede estar en múltiples préstamos (1:N)
- Loan ↔ LoanHistory: Un préstamo puede tener múltiples registros de historial (1:N)
👤 Entidad User (Usuario)
Crea el archivo src/main/java/com/example/pib2/models/entities/User.java:
package com.example.pib2.models.entities;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import lombok.Data;
import java.util.List;
@Entity
@Data
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
private String password;
private String role;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private List<Loan> loans;
}
🔍 Explicación de la Entidad User
Anotaciones de Clase
@Entity
@Data
toString()
- Métodos equals() y hashCode()
- Constructor sin argumentos
@Table(name = "users")
Anotaciones de Campo
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private List<Loan> loans;
📦 Entidad Item (Artículo)
Crea el archivo src/main/java/com/example/pib2/models/entities/Item.java:
package com.example.pib2.models.entities;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import lombok.Data;
import java.util.List;
@Entity
@Data
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private int quantity;
@OneToMany(mappedBy = "item", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private List<Loan> loans;
}
🔍 Explicación de la Entidad Item
Campos de Negocio
private String name;
private String description;
private int quantity;
Relación con Loan
@OneToMany(mappedBy = "item", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private List<Loan> loans;
📋 Entidad Loan (Préstamo)
Crea el archivo src/main/java/com/example/pib2/models/entities/Loan.java:
package com.example.pib2.models.entities;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
@Entity
@Data
public class Loan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "item_id")
@JsonBackReference
private Item item;
@ManyToOne
@JoinColumn(name = "user_id")
@JsonBackReference
private User user;
private LocalDate loanDate;
private LocalDate returnDate;
private boolean returned;
@OneToMany(mappedBy = "loan", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private List<LoanHistory> histories;
}
🔍 Explicación de la Entidad Loan
Relaciones Many-to-One
@ManyToOne
@JoinColumn(name = "item_id")
@JsonBackReference
private Item item;
@ManyToOne
@JoinColumn(name = "user_id")
@JsonBackReference
private User user;
Campos de Negocio
private LocalDate loanDate;
private LocalDate returnDate;
private boolean returned;
Relación con LoanHistory
@OneToMany(mappedBy = "loan", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private List<LoanHistory> histories;
📊 Entidad LoanHistory (Historial de Préstamos)
Crea el archivo src/main/java/com/example/pib2/models/entities/LoanHistory.java:
package com.example.pib2.models.entities;
import com.fasterxml.jackson.annotation.JsonBackReference;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
@Entity
@Data
public class LoanHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "loan_id")
@JsonBackReference
private Loan loan;
private LocalDateTime actionDate;
private String action; // e.g., "CREATED", "RETURNED"
}
🔍 Explicación de la Entidad LoanHistory
Campos de Auditoría
private LocalDateTime actionDate;
private String action;
Relación con Loan
@ManyToOne
@JoinColumn(name = "loan_id")
@JsonBackReference
private Loan loan;
🔄 Manejo de Referencias Circulares
Problema de Referencias Circulares
Sin las anotaciones de Jackson, tendríamos:
User → Loan → User → Loan → ... (infinito)
Solución con Jackson
// En el lado "padre" (User, Item)
@JsonManagedReference
private List<Loan> loans;
// En el lado "hijo" (Loan)
@JsonBackReference
private User user;
@JsonBackReference
private Item item;
Resultado en JSON:
{
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"loans": [
{
"id": 1,
"loanDate": "2024-01-15",
"returned": false
// user e item no aparecen aquí
}
]
}
📁 Estructura de Directorios
Organiza tus entidades de la siguiente manera:
src/main/java/com/example/pib2/
├── models/
│ └── entities/
│ ├── User.java
│ ├── Item.java
│ ├── Loan.java
│ └── LoanHistory.java
├── controllers/
├── services/
├── repositories/
└── Pib2Application.java
✅ Verificación de las Entidades
1. Compilar el Proyecto
./mvnw clean compile
2. Ejecutar la Aplicación
./mvnw spring-boot:run
3. Verificar Creación de Tablas
En los logs deberías ver:
Hibernate: create table users (
id bigserial not null,
email varchar(255),
password varchar(255),
role varchar(255),
username varchar(255),
primary key (id)
)
Hibernate: create table item (
id bigserial not null,
description varchar(255),
name varchar(255),
quantity integer not null,
primary key (id)
)
Hibernate: create table loan (
id bigserial not null,
loan_date date,
return_date date,
returned boolean not null,
item_id bigint,
user_id bigint,
primary key (id)
)
Hibernate: create table loan_history (
id bigserial not null,
action varchar(255),
action_date timestamp(6),
loan_id bigint,
primary key (id)
)
🎨 Mejores Prácticas
1. Nomenclatura de Entidades
✅ Bueno:
@Entity
@Table(name = "users") // Plural, snake_case
public class User { // Singular, PascalCase
❌ Malo:
@Entity
public class user { // Minúscula
2. Uso de Lombok
✅ Bueno:
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
// campos
}
❌ Malo:
@Entity
public class User {
// Escribir manualmente todos los getters/setters
}
3. Relaciones Bidireccionales
✅ Bueno:
// Lado padre
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private List<Loan> loans;
// Lado hijo
@ManyToOne
@JoinColumn(name = "user_id")
@JsonBackReference
private User user;
4. Tipos de Datos
✅ Bueno:
private LocalDate loanDate; // Para fechas
private LocalDateTime actionDate; // Para fecha y hora
private BigDecimal price; // Para dinero
❌ Malo:
private Date loanDate; // Deprecated
private float price; // Impreciso para dinero
🚨 Problemas Comunes y Soluciones
Error: "Table 'user' doesn't exist"
Causa: "user" es palabra reservada en PostgreSQL
Solución:
@Entity
@Table(name = "users") // Usar nombre diferente
public class User {
Error: "StackOverflowError" en JSON
Causa: Referencias circulares
Solución:
@JsonManagedReference // En el lado padre
@JsonBackReference // En el lado hijo
Error: "LazyInitializationException"
Causa: Acceso a relaciones lazy fuera de transacción
Solución:
@OneToMany(fetch = FetchType.EAGER) // Solo si es necesario
// O usar @Transactional en el servicio
Error: "Detached entity passed to persist"
Causa: Intentar guardar entidad con ID ya asignado
Solución:
// Usar merge() en lugar de save() para entidades existentes
entityManager.merge(entity);
🔧 Configuraciones Adicionales
Auditoría Automática
Para agregar campos de auditoría automática:
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
// campos existentes...
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
Y en la clase principal:
@SpringBootApplication
@EnableJpaAuditing
public class Pib2Application {
// ...
}
Validaciones
Agregar validaciones a los campos:
@Entity
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@Email(message = "Email should be valid")
@NotBlank(message = "Email is required")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
@NotBlank(message = "Role is required")
private String role;
}
📚 Conceptos Clave Aprendidos
- Entidades JPA: Clases que representan tablas de base de datos
- Anotaciones de mapeo: @Entity, @Table, @Id, @GeneratedValue
- Relaciones: @OneToMany, @ManyToOne, @JoinColumn
- Lombok: Reducción de código boilerplate
- Jackson: Manejo de referencias circulares en JSON
- Cascade: Propagación de operaciones a entidades relacionadas
- Orphan removal: Eliminación automática de entidades huérfanas
🎯 Próximos Pasos
En la siguiente sección aprenderás a: - Crear DTOs (Data Transfer Objects) - Implementar mapeo entre entidades y DTOs - Separar la capa de presentación de la capa de datos - Validar datos de entrada
← Anterior: Configuración de Base de Datos | Volver al Índice | Siguiente: DTOs y Mapeo →