Spring Framework
Integrando JPA e Hibernate ao Spring
Intermediário8 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
O Spring não substitui o JPA: ele o monta — a EntityManagerFactory, o EntityManager vinculado à transação, a tradução de exceções e o ciclo de vida do contexto de persistência. Ao final deste guia, você saberá configurar o Hibernate ORM 6 com o Spring Framework 6 sem persistence.xml, vai entender o que @PersistenceContext realmente injeta e reconhecer os erros de contexto de persistência que causam LazyInitializationException, consultas N+1 e atualizações perdidas.
O que você vai aprender
- Configurar LocalContainerEntityManagerFactoryBean e JpaTransactionManager em configuração Java
- Explicar o que é o proxy de EntityManager compartilhado e como ele se vincula a uma transação do Spring
- Escrever um repositório com EntityManager e ganhar de graça a tradução para DataAccessException
- Diagnosticar LazyInitializationException e selects N+1 e corrigi-los com fetch joins ou entity graphs
- Decidir se deve habilitar o Open EntityManager in View e entender o custo disso
O Jakarta Persistence (JPA) define a API de mapeamento objeto-relacional — as entidades, o EntityManager, a JPQL, a Criteria API — e o Hibernate ORM é a implementação que quase todo mundo usa. O papel do Spring é mais restrito do que os iniciantes imaginam: ele não mapeia objetos nem gera SQL. Ele cria a EntityManagerFactory, entrega a cada transação o seu próprio EntityManager, traduz as exceções de persistência para a sua própria hierarquia e gerencia a transação dentro da qual vive um contexto de persistência JPA. Acerte essas quatro coisas e o código JPA que você escreve é o mesmo que escreveria em qualquer outro lugar. Este guia tem como alvo o Spring Framework 6.x com Hibernate ORM 6 e o namespace jakarta.persistence no Java 17 ou superior; a camada de repositórios do Spring Data JPA, que fica por cima de tudo isso, é tratada na trilha de Spring Boot.
Configurando a unidade de persistência em Java
O jeito clássico do Java EE é um persistence.xml que nomeia uma unidade de persistência, suas classes e seu provider. Com o Spring, você pode dispensar o arquivo e descrever a unidade 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 é um FactoryBean: o contêiner expõe o produto, uma EntityManagerFactory, sob o nome do bean, e é por isso que o método do transaction manager pode pedir uma EntityManagerFactory diretamente. O “container” do nome se refere ao Spring atuando como contêiner JPA — ele faz o bootstrap que um servidor de aplicação faria, incluindo o scan do classpath e, se houver um weaver configurado, o enhancement de bytecode em tempo de carga. O antigo LocalEntityManagerFactoryBean usa um simples Persistence.createEntityManagerFactory e só serve para testes.
HibernateJpaVendorAdapter define padrões específicos do Hibernate e instala um HibernateJpaDialect, que é o que permite ao JpaTransactionManager aplicar níveis de isolamento, expor a conexão JDBC subjacente e traduzir as exceções do Hibernate. As propriedades do provider passam direto por setJpaProperties; no Hibernate 6, o hibernate.dialect é detectado a partir da conexão e raramente precisa ser configurado.
O EntityManager compartilhado
Um EntityManager do JPA não é thread-safe e representa um contexto de persistência: uma unidade de trabalho com seu cache de primeiro nível de entidades gerenciadas. Numa configuração gerenciada pelo contêiner, você nunca cria um por conta própria. Em vez disso, injeta um 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
}
}
O @PersistenceContext é processado pelo PersistenceAnnotationBeanPostProcessor do Spring, registrado automaticamente pelos contextos baseados em anotações. O que ele injeta é um proxy de EntityManager compartilhado. A cada chamada de método, o proxy procura o EntityManager vinculado à transação Spring corrente na thread atual; se existir, delega a ele. Se não existir, as operações de leitura recebem um EntityManager de vida curta que é fechado após a chamada, e as operações de escrita, como persist, lançam TransactionRequiredException. É esse mecanismo que torna seguro usar, a partir de qualquer thread de requisição, um repositório com um único campo injetado — e é também por isso que a transação precisa ser iniciada pelo Spring: uma chamada JPA em.getTransaction().begin() no proxy é rejeitada, porque quem controla a fronteira transacional é o Spring.
A consequência para o design: um contexto de persistência vive exatamente tanto quanto a transação do Spring. Entidades carregadas dentro de um método de serviço @Transactional ficam gerenciadas até o método retornar e a transação fazer commit; depois disso ficam desanexadas (detached), e acessar uma associação lazy não inicializada lança LazyInitializationException.
@PersistenceContext funciona em campos e setters, mas não em construtores. Para injeção por construtor, declare um SharedEntityManagerBean na configuração e injete EntityManager normalmente — o Spring Data JPA registra um bean equivalente automaticamente, e é por isso que @Autowired EntityManager é comum em projetos Boot.
Transações e o contexto de persistência
O JpaTransactionManager faz para o JPA o que o JdbcTransactionManager faz para o JDBC: na entrada de um @Transactional, ele cria um EntityManager, inicia uma transação nele e vincula ambos à thread; na saída, faz flush, commit e o fecha. Como o EntityManager está vinculado à thread, um JdbcTemplate que use o mesmo DataSource dentro dessa transação enxerga a mesma conexão — o dialeto a expõe —, então misturar JPA para as escritas e JDBC para as consultas de relatório numa só transação funciona e é um padrão 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();
}
}
A linha setPrice sem um save explícito é o hábito que define o JPA com Spring: uma entidade gerenciada é rastreada e, no flush, o Hibernate compara o estado atual com o snapshot tirado no carregamento e emite o UPDATE necessário. Quem vem do JDBC costuma acrescentar um em.merge(p) redundante, que é inofensivo para uma entidade gerenciada, mas revela um mal-entendido.
Aqui, readOnly = true é mais do que uma dica. O JpaTransactionManager o repassa à Session do Hibernate, que muda o flush mode para MANUAL e marca as entidades carregadas como somente leitura, pulando a criação de snapshots e o dirty checking. Numa página que carrega milhares de linhas, a economia é mensurável.
Tradução de exceções
O Hibernate lança subclasses de jakarta.persistence.PersistenceException e seus próprios tipos HibernateException. O PersistenceExceptionTranslationPostProcessor envolve cada bean @Repository num proxy que os converte para a hierarquia DataAccessException do Spring; assim, um serviço captura DataIntegrityViolationException de um repositório JPA exatamente como faria com um JDBC, e OptimisticLockException aparece como ObjectOptimisticLockingFailureException. O Spring Boot registra esse post-processor para você; numa configuração Spring Framework pura, você o declara, como acima. Observe que exceções lançadas no commit — uma violação de constraint diferida, uma falha de lock otimista detectada no flush — são traduzidas pelo próprio JpaTransactionManager, então chegam traduzidas mesmo sem o post-processor.
Lazy loading, N+1 e estratégias de fetch
Por padrão, o JPA carrega @ManyToOne e @OneToOne de forma eager, e @OneToMany e @ManyToMany de forma lazy. A maioria das equipes experientes deixa tudo lazy (fetch = FetchType.LAZY também nas associações to-one) e decide o fetch consulta a consulta, porque uma associação to-one eager é carregada a cada acesso ao pai, seja necessária ou não, e os carregamentos eager se multiplicam pelas associações aninhadas.
Associações lazy criam dois modos de falha:
LazyInitializationExceptionquando uma associação é acessada pela primeira vez depois que o contexto de persistência foi fechado — tipicamente num template de view ou num serializador JSON executado depois que o método de serviço retornou.- Selects N+1 quando o código percorre uma lista de N pais e toca uma associação lazy em cada um, gerando uma consulta para a lista e mais N para os filhos.
Os dois se resolvem da mesma forma: diga à consulta do que você precisa.
// 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();
Fazer fetch join de duas coleções na mesma consulta gera um produto cartesiano, e o Hibernate 6 rejeita isso para bags; busque uma coleção por consulta ou use @BatchSize na associação para que o Hibernate carregue os filhos de vários pais em lotes IN (...). Para telas somente leitura, a resposta mais limpa muitas vezes é não carregar entidades: uma expressão construtora JPQL — select new com.example.ProductSummary(p.sku, p.name, p.price) from Product p — retorna exatamente as colunas de que a tela precisa, sem proxies lazy para atrapalhar.
Open EntityManager in View
O padrão Open EntityManager in View (OpenEntityManagerInViewFilter ou OpenEntityManagerInViewInterceptor no Spring MVC) mantém o contexto de persistência aberto durante toda a requisição HTTP, de modo que associações lazy ainda podem ser inicializadas enquanto um template de view é renderizado. Ele faz a LazyInitializationException desaparecer, e é por isso que o Spring Boot o habilita por padrão em aplicações web e registra um warning no log avisando disso.
O custo é real. As conexões com o banco ficam presas durante a renderização, incluindo quaisquer chamadas remotas que a view faça; as consultas N+1 ficam invisíveis porque funcionam, só que devagar; e a fronteira transacional da camada de serviço deixa de definir quais dados a requisição pode alcançar. Numa aplicação com uma camada de serviço clara e uma API REST que devolve DTOs, desligue-o e deixe a exceção mostrar onde uma consulta está incompleta. Numa aplicação renderizada no servidor e mantida por uma equipe pequena, é uma troca razoável, desde que alguém fique de olho na quantidade de consultas.
Usando o Hibernate diretamente
O Spring ainda traz suporte nativo ao Hibernate no spring-orm: o LocalSessionFactoryBean monta uma SessionFactory e o HibernateTransactionManager a controla. Em código novo, prefira o bootstrap JPA mostrado acima e desça ao Hibernate apenas onde o JPA não tiver equivalente:
Session session = em.unwrap(Session.class);
Product p = session.byNaturalId(Product.class).using("sku", sku).load();
O unwrap não custa nada e deixa o caso especial visível. O HibernateTransactionManager continua útil num único cenário: uma aplicação legada que já usa Session em todo lugar e não pode ser reescrita de uma vez — o JpaTransactionManager também consegue expor a sessão via unwrap, então mesmo nesse caso o bootstrap JPA costuma bastar.
Erros comuns
Acessar uma associação lazy depois da transação. Busque-a na consulta, mapeie para um DTO dentro do serviço ou estenda a transação para cobrir o uso — nunca habilite o Open EntityManager in View só para silenciar a exceção sem entender o custo.
hibernate.hbm2ddl.auto=update em produção. Ele não sabe renomear nem remover com segurança, se distancia silenciosamente do schema pretendido e não guarda histórico. Use uma ferramenta de migração (Flyway ou Liquibase) e configure validate para que a inicialização falhe quando houver divergência.
Associações to-one eager em todo lugar. Cada carregamento de um Order também carrega o seu Customer, o Address do cliente e assim por diante. Deixe as associações lazy por padrão.
Chamar save/merge num loop sobre entidades gerenciadas. É redundante. Altere a instância gerenciada; o Hibernate faz o flush das mudanças.
Esquecer o @Version. Quando dois usuários editam a mesma linha, a última escrita vence em silêncio. Uma coluna @Version transforma o conflito numa ObjectOptimisticLockingFailureException que a interface pode tratar.
Esperar que em.persist execute o INSERT imediatamente. O SQL é adiado até o flush — no commit, antes de uma consulta que precise de resultados consistentes ou num em.flush() explícito. Identificadores gerados com IDENTITY são a exceção, porque o Hibernate precisa inserir para descobrir a chave, o que também desabilita o batching de inserts para essa entidade; prefira SEQUENCE.
Transações grandes que carregam milhares de entidades. O contexto de persistência mantém cada entidade gerenciada até o commit, e o dirty checking varre todas elas no flush. Processe em blocos, chame clear() entre um bloco e outro ou use uma stateless session para processamento em massa.
Principais pontos
- O Spring faz o bootstrap do JPA com
LocalContainerEntityManagerFactoryBeane controla as transações comJpaTransactionManager; nenhumpersistence.xmlé necessário. @PersistenceContextinjeta um proxy compartilhado que delega aoEntityManagervinculado à transação Spring corrente; escritas fora de uma transação falham por design.- O contexto de persistência vive exatamente tanto quanto a transação; entidades gerenciadas passam pelo dirty checking e recebem flush no commit, entidades desanexadas não.
@Repositorysomado aoPersistenceExceptionTranslationPostProcessorfornece os mesmos tiposDataAccessExceptiondo JDBC.- Deixe as associações lazy e faça o fetch explicitamente com fetch joins, entity graphs ou projeções em DTO; essa é a solução tanto para
LazyInitializationExceptionquanto para o N+1. - O Open EntityManager in View troca tempo de conexão presa e custos de consulta ocultos por conveniência; decida isso de forma consciente.
Fontes e leituras complementares
Perguntas frequentes
- Ainda preciso de um persistence.xml com o Spring?
- Não. O LocalContainerEntityManagerFactoryBean consegue montar a unidade de persistência inteiramente a partir da configuração Java: você fornece o DataSource, os pacotes onde procurar classes @Entity e o vendor adapter, e passa as propriedades do provider via setJpaProperties. Um persistence.xml existente continua sendo respeitado, o que ajuda na migração a partir de um servidor de aplicação Jakarta EE.
- Qual a diferença entre @PersistenceContext e um EntityManager com @Autowired?
- O @PersistenceContext é processado pelo PersistenceAnnotationBeanPostProcessor do Spring e injeta um proxy compartilhado thread-safe que delega ao EntityManager vinculado à transação. Um @Autowired simples só funciona se existir um bean do tipo EntityManager; o Spring Data JPA registra justamente um bean proxy compartilhado desse tipo, e é por isso que os dois parecem intercambiáveis em projetos Spring Boot. Numa aplicação Spring Framework sem Boot, use @PersistenceContext ou declare você mesmo um SharedEntityManagerBean.
- Por que recebo “No EntityManager with actual transaction available” no persist?
- O proxy de EntityManager compartilhado exige uma transação ativa gerenciada pelo Spring para operações de escrita, porque fora dela não há contexto de persistência para guardar a entidade até o flush. Anote o método de serviço chamador com @Transactional e garanta que a chamada passe pelo proxy — e não por autoinvocação — e que @EnableTransactionManagement esteja presente.
- Devo usar a API Session do Hibernate ou o EntityManager do JPA?
- Programe contra EntityManager e JPQL sempre que possível: isso mantém a camada de persistência portável e alinhada ao Spring Data JPA e a qualquer tutorial atual. Quando precisar de um recurso exclusivo do Hibernate, como StatelessSession, buscas por natural id ou filtros, chame entityManager.unwrap(Session.class) naquele ponto específico em vez de configurar a SessionFactory nativa globalmente.