9. Hibernate en Spring
🐘 9.1 ¿Qué es Hibernate?
Section titled “🐘 9.1 ¿Qué es Hibernate?”Definición
Section titled “Definición”Hibernate es un framework ORM (Object-Relational Mapping) que mapea objetos Java a tablas de base de datos. Es la implementación más popular de JPA (Java Persistence API).
JPA vs Hibernate
Section titled “JPA vs Hibernate”┌─────────────────────────────────────────┐│ Tu Aplicación │├─────────────────────────────────────────┤│ Spring Data JPA │ ← Abstracción de Spring├─────────────────────────────────────────┤│ JPA │ ← Especificación (interfaz)├─────────────────────────────────────────┤│ Hibernate │ ← Implementación├─────────────────────────────────────────┤│ JDBC │ ← Driver de conexión├─────────────────────────────────────────┤│ Base de Datos │└─────────────────────────────────────────┘🔗 9.2 Integración con Spring
Section titled “🔗 9.2 Integración con Spring”Configuración automática
Section titled “Configuración automática”Spring Boot configura Hibernate automáticamente cuando agregas spring-boot-starter-data-jpa.
# application.propertiesspring.jpa.hibernate.ddl-auto=updatespring.jpa.show-sql=truespring.jpa.properties.hibernate.format_sql=true
# Dialecto (opcional, Spring Boot lo detecta)spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
# Estadísticas de Hibernatespring.jpa.properties.hibernate.generate_statistics=trueEntityManager
Section titled “EntityManager”@Servicepublic class ProductoService {
@PersistenceContext private EntityManager entityManager;
public Producto buscarPorId(Long id) { return entityManager.find(Producto.class, id); }
public List<Producto> buscarPorCategoria(String categoria) { return entityManager.createQuery( "SELECT p FROM Producto p WHERE p.categoria = :cat", Producto.class) .setParameter("cat", categoria) .getResultList(); }
@Transactional public void guardar(Producto producto) { if (producto.getId() == null) { entityManager.persist(producto); // INSERT } else { entityManager.merge(producto); // UPDATE } }}⏳ 9.3 Lazy vs Eager Loading
Section titled “⏳ 9.3 Lazy vs Eager Loading”Estrategias de carga
Section titled “Estrategias de carga”| Estrategia | Descripción | Cuándo se carga |
|---|---|---|
| LAZY | Carga diferida | Cuando se accede al dato |
| EAGER | Carga inmediata | Con la entidad principal |
Ejemplo
Section titled “Ejemplo”@Entitypublic class Usuario { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String nombre;
// LAZY: Los pedidos se cargan cuando se accede a ellos @OneToMany(mappedBy = "usuario", fetch = FetchType.LAZY) private List<Pedido> pedidos;
// EAGER: El perfil se carga junto con el usuario @OneToOne(fetch = FetchType.EAGER) private Perfil perfil;}Valores por defecto
Section titled “Valores por defecto”| Relación | Default |
|---|---|
@OneToOne | EAGER |
@ManyToOne | EAGER |
@OneToMany | LAZY |
@ManyToMany | LAZY |
Problema con LAZY fuera de transacción
Section titled “Problema con LAZY fuera de transacción”// ❌ ERROR: LazyInitializationException@GetMapping("/{id}")public Usuario obtener(@PathVariable Long id) { Usuario usuario = repository.findById(id).orElseThrow(); // La sesión de Hibernate ya cerró usuario.getPedidos().size(); // ¡BOOM! LazyInitializationException return usuario;}
// ✅ SOLUCIÓN 1: Usar @Transactional@Transactional(readOnly = true)@GetMapping("/{id}")public Usuario obtener(@PathVariable Long id) { Usuario usuario = repository.findById(id).orElseThrow(); usuario.getPedidos().size(); // OK, sesión abierta return usuario;}
// ✅ SOLUCIÓN 2: Fetch en la consulta@Query("SELECT u FROM Usuario u LEFT JOIN FETCH u.pedidos WHERE u.id = :id")Optional<Usuario> findByIdConPedidos(@Param("id") Long id);
// ✅ SOLUCIÓN 3: EntityGraph@EntityGraph(attributePaths = {"pedidos"})Optional<Usuario> findById(Long id);🔄 9.4 Ciclo de Vida de Entidades
Section titled “🔄 9.4 Ciclo de Vida de Entidades”Estados de una entidad
Section titled “Estados de una entidad”┌─────────────┐│ NEW │ Objeto creado, no persistido└──────┬──────┘ │ persist() ▼┌─────────────┐│ MANAGED │ Asociado a sesión, cambios se sincronizan└──────┬──────┘ │ detach() / close() ▼┌─────────────┐│ DETACHED │ Desconectado de la sesión└──────┬──────┘ │ merge() ▼┌─────────────┐│ MANAGED │ Re-asociado a sesión└─────────────┘
│ remove() ▼┌─────────────┐│ REMOVED │ Marcado para eliminación└─────────────┘Ejemplo práctico
Section titled “Ejemplo práctico”@Service@Transactionalpublic class ProductoService {
@PersistenceContext private EntityManager em;
public void demostrarCicloVida() { // NEW: Objeto creado Producto producto = new Producto("Laptop", 999.99);
// MANAGED: Persistido em.persist(producto);
// Cambios se sincronizan automáticamente (dirty checking) producto.setPrecio(899.99); // UPDATE automático al commit
// DETACHED: Desconectado em.detach(producto); producto.setPrecio(799.99); // NO se guarda
// MANAGED de nuevo: Re-conectado Producto managed = em.merge(producto); managed.setPrecio(749.99); // Ahora SÍ se guarda
// REMOVED: Eliminado em.remove(managed); }}💾 9.5 Caché de Primer Nivel
Section titled “💾 9.5 Caché de Primer Nivel”¿Qué es?
Section titled “¿Qué es?”El caché de primer nivel (L1 Cache) es automático y está asociado a la sesión/transacción actual.
@Service@Transactionalpublic class UsuarioService {
@PersistenceContext private EntityManager em;
public void demostrarCacheL1() { // Primera consulta: va a la BD Usuario u1 = em.find(Usuario.class, 1L); System.out.println("Query 1 ejecutada");
// Segunda consulta: viene del caché L1 Usuario u2 = em.find(Usuario.class, 1L); System.out.println("Query 2 NO ejecutada (caché)");
// Son el mismo objeto System.out.println(u1 == u2); // true }}Características
Section titled “Características”- Automático: No requiere configuración
- Por sesión: Cada transacción tiene su propio caché
- Garantiza identidad: Mismo ID = mismo objeto
🗄️ 9.6 Caché de Segundo Nivel
Section titled “🗄️ 9.6 Caché de Segundo Nivel”¿Qué es?
Section titled “¿Qué es?”El caché de segundo nivel (L2 Cache) es compartido entre sesiones y requiere configuración.
Configuración con Ehcache
Section titled “Configuración con Ehcache”<!-- pom.xml --><dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-jcache</artifactId></dependency><dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId></dependency># application.propertiesspring.jpa.properties.hibernate.cache.use_second_level_cache=truespring.jpa.properties.hibernate.cache.region.factory_class=jcachespring.jpa.properties.hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProviderspring.jpa.properties.hibernate.cache.use_query_cache=trueMarcar entidades cacheables
Section titled “Marcar entidades cacheables”import jakarta.persistence.Cacheable;import org.hibernate.annotations.Cache;import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity@Cacheable@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)public class Categoria { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String nombre;
// Cachear también la colección @OneToMany(mappedBy = "categoria") @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) private List<Producto> productos;}Estrategias de concurrencia
Section titled “Estrategias de concurrencia”| Estrategia | Descripción |
|---|---|
READ_ONLY | Datos que nunca cambian |
READ_WRITE | Datos que cambian, con bloqueo |
NONSTRICT_READ_WRITE | Cambios poco frecuentes |
TRANSACTIONAL | Transacciones JTA |
⚡ 9.7 Optimización de Consultas
Section titled “⚡ 9.7 Optimización de Consultas”Proyecciones (DTOs)
Section titled “Proyecciones (DTOs)”// ❌ Cargar entidad completa cuando solo necesitas algunos campos@Query("SELECT u FROM Usuario u")List<Usuario> findAll(); // Carga TODOS los campos
// ✅ Proyección con DTOpublic record UsuarioResumenDTO(Long id, String nombre, String email) {}
@Query("SELECT new com.app.dto.UsuarioResumenDTO(u.id, u.nombre, u.email) FROM Usuario u")List<UsuarioResumenDTO> findAllResumen();
// ✅ Proyección con interfazpublic interface UsuarioResumen { Long getId(); String getNombre(); String getEmail();}
List<UsuarioResumen> findAllProjectedBy();Batch fetching
Section titled “Batch fetching”@Entitypublic class Usuario {
@OneToMany(mappedBy = "usuario") @BatchSize(size = 25) // Carga 25 colecciones a la vez private List<Pedido> pedidos;}
// O globalmente en propertiesspring.jpa.properties.hibernate.default_batch_fetch_size=25Fetch Join
Section titled “Fetch Join”// ❌ N+1 queriesList<Usuario> usuarios = repository.findAll();for (Usuario u : usuarios) { u.getPedidos().size(); // Query adicional por cada usuario}
// ✅ Una sola query con JOIN FETCH@Query("SELECT DISTINCT u FROM Usuario u LEFT JOIN FETCH u.pedidos")List<Usuario> findAllConPedidos();🔢 9.8 Problema N+1
Section titled “🔢 9.8 Problema N+1”¿Qué es?
Section titled “¿Qué es?”El problema N+1 ocurre cuando se ejecuta 1 query para obtener N entidades, y luego N queries adicionales para cargar sus relaciones.
// Ejemplo del problema N+1@GetMapping("/usuarios")public List<Usuario> listar() { // Query 1: SELECT * FROM usuarios (obtiene 100 usuarios) List<Usuario> usuarios = repository.findAll();
for (Usuario u : usuarios) { // Query 2-101: SELECT * FROM pedidos WHERE usuario_id = ? System.out.println(u.getPedidos().size()); }
return usuarios; // ¡101 queries en total!}Soluciones
Section titled “Soluciones”// SOLUCIÓN 1: JOIN FETCH@Query("SELECT u FROM Usuario u LEFT JOIN FETCH u.pedidos")List<Usuario> findAllConPedidos();
// SOLUCIÓN 2: EntityGraph@EntityGraph(attributePaths = {"pedidos"})List<Usuario> findAll();
// SOLUCIÓN 3: @BatchSize (en la entidad)@OneToMany(mappedBy = "usuario")@BatchSize(size = 25)private List<Pedido> pedidos;
// SOLUCIÓN 4: Subselect fetch@OneToMany(mappedBy = "usuario")@Fetch(FetchMode.SUBSELECT)private List<Pedido> pedidos;Detectar N+1
Section titled “Detectar N+1”# Habilitar estadísticasspring.jpa.properties.hibernate.generate_statistics=truelogging.level.org.hibernate.stat=DEBUG
# Verás en logs:# Session Metrics {# 23456 nanoseconds spent acquiring 1 JDBC connections;# 0 nanoseconds spent releasing 0 JDBC connections;# 123456 nanoseconds spent preparing 101 JDBC statements; ← ¡Alerta!# }
🐝