Skip to content

9. Hibernate en Spring

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).

Capas de persistencia
┌─────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────┘

Spring Boot configura Hibernate automáticamente cuando agregas spring-boot-starter-data-jpa.

Configuración de Hibernate
# application.properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# Dialecto (opcional, Spring Boot lo detecta)
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
# Estadísticas de Hibernate
spring.jpa.properties.hibernate.generate_statistics=true
Uso de EntityManager
@Service
public 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
}
}
}

EstrategiaDescripciónCuándo se carga
LAZYCarga diferidaCuando se accede al dato
EAGERCarga inmediataCon la entidad principal
Lazy vs Eager
@Entity
public 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;
}
RelaciónDefault
@OneToOneEAGER
@ManyToOneEAGER
@OneToManyLAZY
@ManyToManyLAZY
Soluciones a LazyInitializationException
// ❌ 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);

Estados de 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
└─────────────┘
Ciclo de vida en código
@Service
@Transactional
public 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);
}
}

El caché de primer nivel (L1 Cache) es automático y está asociado a la sesión/transacción actual.

Caché de primer nivel
@Service
@Transactional
public 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
}
}
  • Automático: No requiere configuración
  • Por sesión: Cada transacción tiene su propio caché
  • Garantiza identidad: Mismo ID = mismo objeto

El caché de segundo nivel (L2 Cache) es compartido entre sesiones y requiere configuración.

Dependencias
<!-- pom.xml -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jcache</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
Configuración L2 Cache
# application.properties
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.region.factory_class=jcache
spring.jpa.properties.hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider
spring.jpa.properties.hibernate.cache.use_query_cache=true
Entidad cacheable
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;
}
EstrategiaDescripción
READ_ONLYDatos que nunca cambian
READ_WRITEDatos que cambian, con bloqueo
NONSTRICT_READ_WRITECambios poco frecuentes
TRANSACTIONALTransacciones JTA

Proyecciones
// ❌ Cargar entidad completa cuando solo necesitas algunos campos
@Query("SELECT u FROM Usuario u")
List<Usuario> findAll(); // Carga TODOS los campos
// ✅ Proyección con DTO
public 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 interfaz
public interface UsuarioResumen {
Long getId();
String getNombre();
String getEmail();
}
List<UsuarioResumen> findAllProjectedBy();
Batch fetching
@Entity
public class Usuario {
@OneToMany(mappedBy = "usuario")
@BatchSize(size = 25) // Carga 25 colecciones a la vez
private List<Pedido> pedidos;
}
// O globalmente en properties
spring.jpa.properties.hibernate.default_batch_fetch_size=25
Fetch Join
// ❌ N+1 queries
List<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();

El problema N+1 ocurre cuando se ejecuta 1 query para obtener N entidades, y luego N queries adicionales para cargar sus relaciones.

Problema N+1
// 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 al N+1
// 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 con estadísticas
# Habilitar estadísticas
spring.jpa.properties.hibernate.generate_statistics=true
logging.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!
# }
🐝