Spring Framework
Integrar JPA e Hibernate con Spring
Intermedio9 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
Spring no sustituye a JPA: lo ensambla — la EntityManagerFactory, el EntityManager ligado a la transacción, la traducción de excepciones y el ciclo de vida del contexto de persistencia. Al terminar esta guía sabrás montar Hibernate ORM 6 sobre Spring Framework 6 sin persistence.xml, entenderás qué inyecta realmente @PersistenceContext y reconocerás los errores de contexto de persistencia que provocan LazyInitializationException, consultas N+1 y actualizaciones perdidas.
Lo que aprenderás
- Configurar LocalContainerEntityManagerFactoryBean y JpaTransactionManager con configuración Java
- Explicar qué es el proxy de EntityManager compartido y cómo se vincula a una transacción de Spring
- Escribir un repositorio con EntityManager y obtener sin esfuerzo la traducción a DataAccessException
- Diagnosticar LazyInitializationException y selects N+1 y corregirlos con fetch joins o entity graphs
- Decidir si activar Open EntityManager in View y entender su coste
Jakarta Persistence (JPA) define la API de mapeo objeto-relacional — las entidades, el EntityManager, JPQL, la Criteria API — y Hibernate ORM es la implementación que usa casi todo el mundo. El papel de Spring es más limitado de lo que suponen quienes empiezan: no mapea objetos ni genera SQL. Crea la EntityManagerFactory, entrega a cada transacción su propio EntityManager, traduce las excepciones de persistencia a su propia jerarquía y gestiona la transacción dentro de la cual vive un contexto de persistencia JPA. Si aciertas con esas cuatro cosas, el código JPA que escribes es el mismo que escribirías en cualquier otro sitio. Esta guía se centra en Spring Framework 6.x con Hibernate ORM 6 y el namespace jakarta.persistence sobre Java 17 o posterior; la capa de repositorios de Spring Data JPA que se apoya en todo esto se trata en la materia de Spring Boot.
Configurar la unidad de persistencia en Java
La forma clásica de Java EE es un persistence.xml que nombra una unidad de persistencia, sus clases y su proveedor. Con Spring puedes prescindir de ese fichero y describir la unidad como beans:
@Configuration
@EnableTransactionManagement
public class PersistenceConfig {
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
var emf = new LocalContainerEntityManagerFactoryBean();
emf.setDataSource(dataSource);
emf.setPackagesToScan("com.example.shop.domain"); // finds @Entity classes
emf.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
var props = new Properties();
props.put("hibernate.hbm2ddl.auto", "validate"); // never "update" in production
props.put("hibernate.jdbc.batch_size", "50");
props.put("hibernate.order_inserts", "true");
emf.setJpaProperties(props);
return emf;
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
return new PersistenceExceptionTranslationPostProcessor();
}
}
LocalContainerEntityManagerFactoryBean es un FactoryBean: el contenedor expone el producto, una EntityManagerFactory, bajo el nombre del bean, y por eso el método del transaction manager puede pedir directamente una EntityManagerFactory. El «container» del nombre se refiere a Spring actuando como contenedor JPA: realiza el arranque que haría un servidor de aplicaciones, incluido el escaneo del classpath y, si hay un weaver configurado, el enriquecimiento del bytecode en tiempo de carga. El antiguo LocalEntityManagerFactoryBean usa un simple Persistence.createEntityManagerFactory y solo sirve para tests.
HibernateJpaVendorAdapter fija valores por defecto específicos de Hibernate e instala un HibernateJpaDialect, que es lo que permite a JpaTransactionManager aplicar niveles de aislamiento, exponer la conexión JDBC subyacente y traducir las excepciones de Hibernate. Las propiedades del proveedor pasan tal cual por setJpaProperties; en Hibernate 6, hibernate.dialect se detecta a partir de la conexión y rara vez hace falta configurarlo.
El EntityManager compartido
Un EntityManager de JPA no es thread-safe y representa un contexto de persistencia: una unidad de trabajo con su caché de primer nivel de entidades gestionadas. En una configuración gestionada por el contenedor nunca lo creas tú. En su lugar, inyectas un proxy:
@Repository
public class JpaProductRepository implements ProductRepository {
@PersistenceContext
private EntityManager em; // shared, thread-safe proxy
@Override
public Optional<Product> findBySku(String sku) {
return em.createQuery("select p from Product p where p.sku = :sku", Product.class)
.setParameter("sku", sku)
.getResultStream()
.findFirst();
}
@Override
public Product save(Product product) {
if (product.getId() == null) {
em.persist(product); // becomes managed; INSERT at flush
return product;
}
return em.merge(product); // copies state onto a managed instance; returns that instance
}
}
@PersistenceContext lo procesa el PersistenceAnnotationBeanPostProcessor de Spring, que los contextos basados en anotaciones registran automáticamente. Lo que inyecta es un proxy de EntityManager compartido. En cada llamada a un método, el proxy busca el EntityManager ligado a la transacción de Spring en curso en el thread actual; si existe, delega en él. Si no existe, las operaciones de lectura reciben un EntityManager efímero que se cierra tras la llamada, y las operaciones de escritura como persist lanzan TransactionRequiredException. Este es el mecanismo que permite usar sin riesgo, desde cualquier thread de petición, un repositorio con un único campo inyectado, y también la razón por la que la transacción debe iniciarla Spring: una llamada JPA em.getTransaction().begin() sobre el proxy se rechaza, porque la frontera transaccional pertenece a Spring.
La consecuencia para el diseño: un contexto de persistencia vive exactamente lo mismo que la transacción de Spring. Las entidades cargadas dentro de un método de servicio @Transactional están gestionadas hasta que el método retorna y la transacción hace commit; a partir de ahí quedan desasociadas (detached), y acceder a una asociación lazy sin inicializar lanza LazyInitializationException.
@PersistenceContext funciona en campos y setters, pero no en constructores. Para la inyección por constructor, declara un SharedEntityManagerBean en la configuración e inyecta EntityManager con normalidad; Spring Data JPA registra automáticamente un bean equivalente, y por eso @Autowired EntityManager es habitual en los proyectos Boot.
Transacciones y contexto de persistencia
JpaTransactionManager hace para JPA lo que JdbcTransactionManager hace para JDBC: al entrar en un método @Transactional crea un EntityManager, inicia una transacción sobre él y vincula ambos al thread; al salir, hace flush, commit y lo cierra. Como el EntityManager está vinculado al thread, un JdbcTemplate que use el mismo DataSource dentro de esa transacción ve la misma conexión — el dialecto la expone —, así que combinar JPA para las escrituras y JDBC para las consultas de informes en una misma transacción funciona y es un patrón legítimo.
@Service
public class CatalogService {
private final ProductRepository products;
private final PriceHistoryRepository history;
public CatalogService(ProductRepository products, PriceHistoryRepository history) {
this.products = products;
this.history = history;
}
@Transactional
public void reprice(String sku, Money newPrice) {
Product p = products.findBySku(sku).orElseThrow(() -> new UnknownSkuException(sku));
history.record(p, p.getPrice(), newPrice);
p.setPrice(newPrice); // no save() call: dirty checking issues the UPDATE at commit
}
@Transactional(readOnly = true)
public List<ProductSummary> summaries() {
return products.findSummaries();
}
}
La línea setPrice sin un guardado explícito es el hábito que define JPA bajo Spring: una entidad gestionada está bajo seguimiento y, en el flush, Hibernate compara su estado actual con la instantánea tomada al cargarla y emite el UPDATE necesario. Quienes vienen de JDBC suelen añadir un em.merge(p) redundante, que no hace daño con una entidad gestionada pero delata un malentendido.
Aquí readOnly = true es algo más que una pista. JpaTransactionManager lo transmite a la Session de Hibernate, que pone el flush mode en MANUAL y marca las entidades cargadas como de solo lectura, con lo que se ahorra la creación de instantáneas y el dirty checking. En una página que carga miles de filas, el ahorro es medible.
Traducción de excepciones
Hibernate lanza subclases de jakarta.persistence.PersistenceException y sus propios tipos HibernateException. PersistenceExceptionTranslationPostProcessor envuelve cada bean @Repository en un proxy que las convierte a la jerarquía DataAccessException de Spring, de modo que un servicio captura DataIntegrityViolationException de un repositorio JPA exactamente igual que de uno JDBC, y OptimisticLockException aflora como ObjectOptimisticLockingFailureException. Spring Boot registra este post-processor por ti; en una configuración de Spring Framework sin Boot lo declaras tú, como arriba. Ten en cuenta que las excepciones lanzadas en el commit — una violación de restricción diferida, un fallo de bloqueo optimista detectado en el flush — las traduce el propio JpaTransactionManager, así que llegan traducidas incluso sin el post-processor.
Lazy loading, N+1 y estrategias de carga
Por defecto, JPA carga @ManyToOne y @OneToOne en modo eager, y @OneToMany y @ManyToMany en modo lazy. La mayoría de los equipos con experiencia hacen todo lazy (fetch = FetchType.LAZY también en las asociaciones to-one) y deciden la carga consulta a consulta, porque una asociación to-one eager se carga en cada acceso al padre, se necesite o no, y las cargas eager se multiplican a través de las asociaciones anidadas.
Las asociaciones lazy generan dos modos de fallo:
LazyInitializationExceptioncuando se accede por primera vez a una asociación después de que el contexto de persistencia se haya cerrado; típicamente en una plantilla de vista o en un serializador JSON que se ejecuta después de que el método de servicio haya retornado.- Selects N+1 cuando el código recorre una lista de N padres y toca una asociación lazy en cada uno, lo que produce una consulta para la lista y N más para los hijos.
Ambos se corrigen de la misma forma: dile a la consulta lo que necesitas.
// JPQL fetch join: one query, children initialised
List<Order> orders = em.createQuery("""
select distinct o from Order o
join fetch o.lines l
join fetch l.product
where o.customer.id = :customer
""", Order.class)
.setParameter("customer", customerId)
.getResultList();
// Entity graph: same effect, reusable and composable
EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addSubgraph("lines").addAttributeNodes("product");
List<Order> sameOrders = em.createQuery("select o from Order o where o.customer.id = :customer", Order.class)
.setParameter("customer", customerId)
.setHint("jakarta.persistence.fetchgraph", graph)
.getResultList();
Hacer fetch join de dos colecciones en una misma consulta produce un producto cartesiano, y Hibernate 6 lo rechaza para los bags; carga una colección por consulta o usa @BatchSize en la asociación para que Hibernate cargue los hijos de muchos padres en lotes IN (...). Para pantallas de solo lectura, la respuesta más limpia suele ser no cargar entidades en absoluto: una expresión constructora JPQL — select new com.example.ProductSummary(p.sku, p.name, p.price) from Product p — devuelve exactamente las columnas que la pantalla necesita, sin proxies lazy con los que tropezar.
Open EntityManager in View
El patrón Open EntityManager in View (OpenEntityManagerInViewFilter u OpenEntityManagerInViewInterceptor en Spring MVC) mantiene abierto el contexto de persistencia durante toda la petición HTTP, de modo que las asociaciones lazy aún pueden inicializarse mientras se renderiza una plantilla de vista. Hace desaparecer la LazyInitializationException, y por eso Spring Boot lo activa por defecto en las aplicaciones web y lo advierte con un warning en el log.
El coste es real. Las conexiones a la base de datos se retienen durante todo el renderizado, incluidas las llamadas remotas que haga la vista; las consultas N+1 se vuelven invisibles porque funcionan, solo que despacio; y la frontera transaccional de la capa de servicio deja de definir a qué datos puede llegar la petición. En una aplicación con una capa de servicio clara y una API REST que devuelve DTO, desactívalo y deja que la excepción te diga dónde una consulta está incompleta. En una aplicación renderizada en servidor que mantiene un equipo pequeño, es un compromiso razonable, siempre que alguien vigile el número de consultas.
Usar Hibernate directamente
Spring sigue incluyendo soporte nativo para Hibernate en spring-orm: LocalSessionFactoryBean construye una SessionFactory y HibernateTransactionManager la gestiona. En código nuevo, prefiere el arranque JPA anterior y baja a Hibernate solo donde JPA no tenga equivalente:
Session session = em.unwrap(Session.class);
Product p = session.byNaturalId(Product.class).using("sku", sku).load();
unwrap no cuesta nada y deja el caso especial a la vista. HibernateTransactionManager sigue siendo útil en un único escenario: una aplicación legacy que ya usa Session por todas partes y no puede reescribirse de golpe. Aun así, JpaTransactionManager también puede exponer la sesión mediante unwrap, de modo que incluso en ese caso el arranque JPA suele bastar.
Errores habituales
Tocar una asociación lazy después de la transacción. Cárgala en la consulta, mapea a un DTO dentro del servicio o amplía la transacción para que cubra el uso; nunca actives Open EntityManager in View solo para silenciar la excepción sin entender su coste.
hibernate.hbm2ddl.auto=update en producción. No sabe renombrar ni eliminar de forma segura, se aleja en silencio del esquema previsto y no deja historial. Usa una herramienta de migraciones (Flyway o Liquibase) y configura validate para que el arranque falle si hay divergencias.
Asociaciones to-one eager por todas partes. Cada carga de un Order carga también su Customer, la Address del cliente, y así sucesivamente. Haz que las asociaciones sean lazy por defecto.
Llamar a save/merge en un bucle sobre entidades gestionadas. Es redundante. Modifica la instancia gestionada; Hibernate hace flush de los cambios.
Olvidar @Version. Si dos usuarios editan la misma fila, gana la última escritura sin que nadie se entere. Una columna @Version convierte el conflicto en una ObjectOptimisticLockingFailureException que la interfaz puede gestionar.
Esperar que em.persist ejecute el INSERT de inmediato. El SQL se difiere hasta el flush: en el commit, antes de una consulta que necesite resultados coherentes o con un em.flush() explícito. Los identificadores generados con IDENTITY son la excepción, porque Hibernate tiene que insertar para conocer la clave, lo que además desactiva el batching de inserciones para esa entidad; prefiere SEQUENCE.
Transacciones grandes que cargan miles de entidades. El contexto de persistencia retiene cada entidad gestionada hasta el commit, y el dirty checking las recorre todas en el flush. Procesa por bloques, llama a clear() entre bloques o usa una stateless session para el trabajo masivo.
Ideas clave
- Spring arranca JPA con
LocalContainerEntityManagerFactoryBeany gestiona las transacciones conJpaTransactionManager; no hace falta ningúnpersistence.xml. @PersistenceContextinyecta un proxy compartido que delega en elEntityManagerligado a la transacción de Spring en curso; las escrituras fuera de una transacción fallan por diseño.- El contexto de persistencia vive exactamente lo mismo que la transacción; las entidades gestionadas pasan por el dirty checking y se vuelcan en el commit, las desasociadas no.
@Repositoryjunto conPersistenceExceptionTranslationPostProcessorofrece los mismos tiposDataAccessExceptionque JDBC.- Haz lazy las asociaciones y carga de forma explícita con fetch joins, entity graphs o proyecciones a DTO; esa es la solución tanto para
LazyInitializationExceptioncomo para el N+1. - Open EntityManager in View cambia tiempo de retención de conexiones y costes de consultas ocultos por comodidad; decídelo de forma consciente.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Sigo necesitando un persistence.xml con Spring?
- No. LocalContainerEntityManagerFactoryBean puede construir la unidad de persistencia íntegramente a partir de la configuración Java: le das el DataSource, los paquetes donde buscar clases @Entity y el vendor adapter, y pasas las propiedades del proveedor mediante setJpaProperties. Si existe un persistence.xml, se sigue respetando, lo que resulta útil al migrar desde un servidor de aplicaciones Jakarta EE.
- ¿Qué diferencia hay entre @PersistenceContext y un EntityManager con @Autowired?
- @PersistenceContext lo procesa el PersistenceAnnotationBeanPostProcessor de Spring e inyecta un proxy compartido thread-safe que delega en el EntityManager ligado a la transacción. Un @Autowired a secas solo funciona si existe un bean de tipo EntityManager; Spring Data JPA registra precisamente un bean proxy compartido de ese tipo, y por eso ambos parecen intercambiables en los proyectos Spring Boot. En una aplicación Spring Framework sin Boot, usa @PersistenceContext o declara tú mismo un SharedEntityManagerBean.
- ¿Por qué obtengo «No EntityManager with actual transaction available» al hacer persist?
- El proxy de EntityManager compartido exige una transacción activa gestionada por Spring para las operaciones de escritura, porque fuera de ella no hay contexto de persistencia que conserve la entidad hasta el flush. Anota el método de servicio que hace la llamada con @Transactional y asegúrate de que la llamada pasa por el proxy — no por autoinvocación — y de que @EnableTransactionManagement está presente.
- ¿Debo usar la API Session de Hibernate o el EntityManager de JPA?
- Programa contra EntityManager y JPQL siempre que puedas: mantiene la capa de persistencia portable y encaja con Spring Data JPA y con cualquier tutorial actual. Cuando necesites una funcionalidad exclusiva de Hibernate, como StatelessSession, las búsquedas por natural id o los filtros, llama a entityManager.unwrap(Session.class) justo en ese punto en lugar de configurar la SessionFactory nativa de forma global.