Java EE / Jakarta EE
Jakarta Persistence (JPA 3.1): entidades, mapeamento e contexto de persistência
Intermediário10 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
Jakarta Persistence é o padrão de mapeamento objeto-relacional implementado pelo Hibernate e pelo EclipseLink, e a camada de persistência sob o Spring Data JPA. Este guia explica a unidade de persistência e o EntityManager, os estados pelos quais uma entidade passa, como mapear identificadores, colunas, valores embutidos e relacionamentos, o que o lazy loading e as cascatas realmente fazem e como o locking otimista protege as atualizações, junto com as armadilhas que geram consultas N+1, LazyInitializationException e atualizações perdidas.
O que você vai aprender
- Configurar uma unidade de persistência e obter um EntityManager gerenciado pelo contêiner
- Descrever o ciclo de vida da entidade e o que fazem persist, merge, remove, detach e flush
- Mapear identificadores, atributos básicos, embeddables, enums e tipos do java.time
- Mapear associações one-to-many, many-to-one, one-to-one e many-to-many com o lado dono correto
- Escolher conscientemente estratégias de fetch, cascatas, mapeamentos de herança e locking otimista
Bancos de dados relacionais armazenam linhas; programas Java trabalham com grafos de objetos. Toda aplicação que usa os dois precisa de um mapeamento entre eles, e escrever esse mapeamento à mão (SQL para cada operação, código que converte result set em objetos, controle de identidade) é repetitivo e sujeito a erros. O Jakarta Persistence (JPA) padroniza o mapeamento e o runtime que o aplica: você declara uma vez como Order se relaciona com a tabela orders e passa a trabalhar com objetos. Mas o runtime não é mágico: um EntityManager acompanha os objetos que carregou, decide quando gravá-los de volta e emite SQL que você não escreveu. A maioria dos problemas com JPA vem de não saber o que ele está fazendo por você; este guia deixa isso visível.
Unidades de persistência e o EntityManager
Uma unidade de persistência é um conjunto nomeado de classes de entidade mais a configuração para acessar um banco de dados, declarado em META-INF/persistence.xml. Num contêiner, a fonte de dados é uma referência JNDI e as transações são do JTA:
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence
https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
<persistence-unit name="shop" transaction-type="JTA">
<jta-data-source>java:/jdbc/ShopDS</jta-data-source>
<!-- entity classes are discovered by scanning the archive; list them only outside a container -->
<properties>
<property name="jakarta.persistence.schema-generation.database.action" value="none"/>
<property name="hibernate.show_sql" value="false"/> <!-- provider-specific; harmless if the provider is EclipseLink -->
</properties>
</persistence-unit>
</persistence>
transaction-type="RESOURCE_LOCAL", com as propriedades jakarta.persistence.jdbc.url, user e password, é a forma standalone usada em testes e programas Java SE; nela você cria um EntityManagerFactory com Persistence.createEntityManagerFactory("shop") e gerencia as EntityTransaction por conta própria. Dentro de um servidor Jakarta EE, você injeta o gerenciador e o contêiner cuida do resto:
@PersistenceContext(unitName = "shop")
EntityManager em; // container-managed, transaction-scoped: one persistence context per JTA transaction
O EntityManager é uma fachada sobre um contexto de persistência: um mapa que associa uma identidade (classe da entidade mais chave primária) à única instância managed daquela linha. Dentro de um contexto, chamar em.find(Order.class, 1L) duas vezes retorna o mesmo objeto, e nenhuma linha é representada duas vezes. Um contexto com escopo de transação vive exatamente tanto quanto a transação JTA e sofre flush (as mudanças pendentes viram SQL) logo antes do commit. Contextos gerenciados pela aplicação e contextos estendidos existem para casos especiais (beans stateful, jobs batch), mas o de escopo de transação é o padrão e o primeiro a entender.
O ciclo de vida da entidade
Uma instância de entidade está sempre em um de quatro estados, e os métodos do EntityManager a movem entre eles:
| Estado | Significado | Como se chega lá |
|---|---|---|
| New (transient) | Um objeto comum; o contexto não sabe nada sobre ele | new Order() |
| Managed | Acompanhado pelo contexto; as alterações são detectadas e gravadas no flush | persist, find, getReference, o resultado de uma consulta, o valor retornado por merge |
| Detached | Era managed por um contexto que já foi fechado, ou foi desanexado explicitamente | Fim da transação, detach, clear, serialização |
| Removed | Agendado para um DELETE no flush | remove numa instância managed |
A consequência mais importante é o dirty checking automático: você não chama update. Carregue uma entidade, altere um campo dentro da transação, e o UPDATE acontece no flush, inclusive para entidades carregadas por uma consulta e alteradas de passagem. merge é a ponte de volta de detached para managed, com um detalhe que todo mundo erra pelo menos uma vez: ele retorna a cópia managed e deixa o argumento detached.
@Transactional
public Order rename(Long id, String newRef) {
Order order = em.find(Order.class, id); // managed
order.setReference(newRef); // dirty; UPDATE issued at flush, no explicit call
return order; // becomes detached when the transaction commits
}
@Transactional
public Order update(Order detachedFromForm) {
Order managed = em.merge(detachedFromForm); // copies state onto the managed instance
managed.setUpdatedAt(Instant.now()); // affects the row; detachedFromForm is untouched
return managed;
}
getReference retorna um proxy sem ir ao banco, ideal quando você só precisa preencher uma chave estrangeira. refresh recarrega do banco, descartando as alterações. flush força o envio do SQL mais cedo (uma consulta com FlushModeType.AUTO, o padrão, faz flush de qualquer forma quando pode ler mudanças pendentes), e clear desanexa tudo, que é como jobs batch mantêm o uso de memória sob controle.
Mapeando uma classe para uma tabela
Uma entidade é uma classe não final com um construtor sem argumentos (no mínimo protected), anotada com @Entity e com um identificador. Os campos são mapeados por padrão (todo campo não transient vira uma coluna com o nome do campo), então as anotações só são necessárias onde o padrão não serve.
import java.math.BigDecimal;
import java.time.Instant;
import java.util.*;
import jakarta.persistence.*;
@Entity
@Table(name = "orders", // "order" is a reserved word in SQL
indexes = @Index(name = "ix_orders_customer", columnList = "customer_id"))
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "orders_seq")
@SequenceGenerator(name = "orders_seq", sequenceName = "orders_seq", allocationSize = 50)
private Long id;
@Column(nullable = false, length = 20, unique = true)
private String reference;
@Enumerated(EnumType.STRING) // never ORDINAL: reordering the enum corrupts data
@Column(nullable = false, length = 16)
private OrderStatus status = OrderStatus.NEW;
@Column(nullable = false)
private Instant placedAt = Instant.now(); // java.time maps natively since JPA 2.2
@Embedded
private Address shippingAddress; // columns inlined into the orders table
@Convert(converter = MoneyConverter.class) // custom type <-> column, via AttributeConverter
private Money total;
@Version
private long version; // optimistic locking, see below
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "customer_id")
private Customer customer;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("lineNo")
private List<OrderLine> lines = new ArrayList<>();
protected Order() { } // required by the specification
public Order(Customer customer) { this.customer = customer; }
public void addLine(Product product, int quantity) {
var line = new OrderLine(this, product, quantity, lines.size() + 1);
lines.add(line); // keep both sides consistent
}
// getters; setters only where the domain allows change
}
Identificadores
Escolha a estratégia de geração pensando no banco de dados:
SEQUENCE: a melhor escolha padrão para PostgreSQL, Oracle e qualquer banco com sequences. ComallocationSize, o provedor busca um bloco de ids de uma vez, e assim cada insert não precisa de uma ida e volta própria. Mantenha o tamanho de alocação igual aoINCREMENT BYda sequence; caso contrário, surgem lacunas e, em alguns provedores, colisões.IDENTITY: colunas autoincremento (MySQL, SQL Server). Simples, mas o id só é conhecido depois doINSERT, o que obriga o provedor a inserir imediatamente nopersiste desativa o batching de inserts.UUID: novidade do Persistence 3.1; o provedor gera umjava.util.UUID. Os ids são globalmente únicos e podem ser atribuídos no cliente, ao custo de chaves de 16 bytes e pior localidade de índice em alguns bancos.TABLE: portável, mas lenta; evite.AUTO: deixa o provedor escolher; é melhor ser explícito, porque a escolha afeta o batching e o schema.
Chaves compostas são suportadas com @EmbeddedId ou @IdClass; aparecem em schemas legados e vale a pena evitá-las nos novos.
Atributos básicos, embeddables e conversores
@Column controla nome, nulabilidade, tamanho e precisão; @Basic(fetch = LAZY) num @Lob grande pede ao provedor que adie o carregamento, embora nem todos respeitem isso sem bytecode enhancement. @Transient exclui um campo. Classes @Embeddable agrupam colunas que andam juntas (um endereço, um valor monetário) sem uma tabela própria, e @AttributeOverride renomeia as colunas quando o mesmo embeddable aparece duas vezes. Um AttributeConverter<X, Y> mapeia qualquer tipo Java para um tipo de coluna suportado pelo JDBC e é a forma padrão de armazenar value objects, strings criptografadas ou blobs JSON. @Temporal só é necessário para campos legados java.util.Date; use LocalDate, LocalDateTime, Instant ou OffsetDateTime.
Relacionamentos e o lado dono
É nas associações que os mapeamentos dão errado. A regra que resolve a maior parte da confusão: o banco tem uma única chave estrangeira, então só um lado da associação é o dono dela, e só as alterações feitas no lado dono são gravadas.
@ManyToOneé sempre o lado dono; a chave estrangeira fica na tabela dele.@JoinColumndá o nome a ela.@OneToMany(mappedBy = "order")é o lado inverso. Adicionar um item apenas aorder.getLines()não grava nada; quem preenche a chave estrangeira éline.setOrder(order). Métodos auxiliares comoaddLinemantêm os dois lados sincronizados.- Um
@OneToManyunidirecional semmappedByusa por padrão uma tabela de junção, o que pega muita gente de surpresa; adicione@JoinColumnpara usar, em vez disso, uma chave estrangeira na tabela filha. @OneToOnecoloca a chave estrangeira no lado dono; só torne o lado inverso lazy se aceitar que o provedor talvez ainda precise consultar o banco para saber se o destino existe.@ManyToManyusa uma tabela de junção dos dois lados;@JoinTabledá o nome a ela. Quando a tabela de junção precisa de colunas próprias (uma data, uma quantidade), ela vira uma entidade própria com duas associações@ManyToOne.
Fetching
Padrões: @ManyToOne e @OneToOne são EAGER, coleções são LAZY. Para a maioria das aplicações, esses padrões erram para o lado do eager: um many-to-one eager em toda entidade significa que toda consulta arrasta junto os clientes, os endereços deles e assim por diante. Declare fetch = FetchType.LAZY nas associações to-one e carregue explicitamente o que cada caso de uso precisa com JOIN FETCH ou um entity graph, ambos abordados no guia de consultas. O lazy loading só funciona enquanto o contexto está aberto, e é por isso que acessar order.getLines() numa view depois do fim da transação lança LazyInitializationException no Hibernate. O problema N+1 é o reflexo disso: percorrer cem pedidos chamando getCustomer() em cada um dispara cem selects extras. Ambos se resolvem na consulta, não trocando tudo para eager.
Cascatas e remoção de órfãos
cascade propaga uma operação do EntityManager ao longo de uma associação: CascadeType.PERSIST em lines significa que persistir o pedido persiste os novos itens dele. ALL é adequado para uma composição (os itens não existem sem o pedido) e perigoso para qualquer coisa compartilhada: cascade = ALL em Order.customer apagaria o cliente ao remover um pedido. orphanRemoval = true também apaga um filho quando ele é removido da coleção, o que transforma order.getLines().remove(line) num DELETE. Juntos, permitem tratar um agregado como uma unidade só.
Herança e mapeamentos compartilhados
Entidades podem herdar de entidades, e o JPA oferece três layouts de tabela:
| Estratégia | Tabelas | Trade-off |
|---|---|---|
SINGLE_TABLE (padrão) | Uma tabela para a hierarquia inteira, com uma coluna discriminadora | Consultas mais rápidas, sem joins; as colunas das subclasses precisam aceitar NULL |
JOINED | Uma tabela por classe, unidas pela chave primária | Normalizado, sem nulos obrigatórios; toda consulta polimórfica faz joins |
TABLE_PER_CLASS | Uma tabela completa por classe concreta | Sem joins em consultas sobre uma classe concreta; consultas polimórficas exigem UNION, e a geração de identificadores é restrita |
Declare a estratégia com @Inheritance na raiz e @DiscriminatorValue nas subclasses. Muitas vezes o que você quer não é herança de entidades, e sim uma @MappedSuperclass: uma classe base como AuditedEntity com id, version, createdAt e updatedAt, cujos campos são mapeados na tabela de cada subclasse, mas que não pode ser consultada por si só.
Locking otimista com @Version
Dois usuários carregam o mesmo pedido, os dois alteram, os dois salvam: sem proteção, a segunda gravação sobrescreve a primeira em silêncio, a chamada atualização perdida (lost update). Um campo @Version (um número ou um timestamp) faz o provedor adicionar WHERE version = ? a todo UPDATE e incrementar a versão; se nenhuma linha for afetada, a linha mudou nesse meio-tempo e uma OptimisticLockException é lançada no flush ou no commit. Toda entidade que pode ser editada de forma concorrente deveria ter um. Leve a versão até o cliente (um campo oculto num formulário, um campo num payload JSON) e de volta até o merge; caso contrário, a verificação compara a versão recém-carregada com ela mesma e não protege nada. O locking pessimista (em.find(Order.class, id, LockModeType.PESSIMISTIC_WRITE), um SELECT … FOR UPDATE) é a alternativa para atualizações curtas e muito disputadas, como decrementar estoque.
Provedores e geração de schema
Hibernate ORM 6.x e EclipseLink 4.x implementam o Persistence 3.1; ambos acompanham os principais servidores (Hibernate no WildFly, EclipseLink no GlassFish e no Payara, qualquer um dos dois no Open Liberty). Mantenha seu código na API do JPA e você poderá trocar de provedor; use as extensões de forma consciente: @BatchSize, @Formula, natural ids e o HQL mais rico do Hibernate são úteis, mas cada um tem um custo de portabilidade.
A propriedade padrão jakarta.persistence.schema-generation.database.action (none, create, drop, drop-and-create) pode gerar as tabelas a partir dos seus mapeamentos; a propriedade própria do Hibernate, hibernate.hbm2ddl.auto, acrescenta update e validate. A geração é prática para testes e protótipos, e uma péssima forma de rodar produção: o DDL gerado não tem as constraints, as escolhas de índice nem o histórico de migrações de que um schema de verdade precisa. Gerencie schemas de produção com scripts de migração versionados (Flyway, Liquibase), configure a propriedade padrão como none e use o validate do Hibernate se quiser que os mapeamentos sejam verificados contra o schema real na inicialização.
Erros comuns
- Associações to-one eager em todo lugar. Toda consulta carrega uma árvore inteira. Marque-as como
LAZYe faça fetch por caso de uso. - Acessar coleções lazy fora da transação.
LazyInitializationExceptionem views e serializadores. Faça fetch na consulta ou converta para DTO dentro do service. - Esquecer o lado dono. Adicionar itens à coleção
mappedBynão grava nada. Preencha o many-to-one ou use métodos auxiliares que façam as duas coisas. EnumType.ORDINAL. Inserir uma constante no meio do enum reinterpreta todos os valores armazenados. UseSTRING.cascade = ALLem entidades compartilhadas. Remover um pedido apaga o cliente dele. Use cascata só ao longo de composições.hashCodebaseado em id gerado. Muda depois dopersist; as entidades somem dos sets. Use uma chave de negócio ou um UUID estável atribuído no construtor.allocationSizediferente do incremento da sequence. Lacunas, na melhor das hipóteses; erros de chave duplicada, na pior. Mantenha os dois iguais.- Geração de schema em produção. Constraints faltando e nenhum histórico de migrações. Use Flyway ou Liquibase.
Principais pontos
- Uma unidade de persistência define as entidades e a fonte de dados; um
EntityManagergerenciado pelo contêiner fornece um contexto de persistência com o escopo da transação JTA. - Entidades são new, managed, detached ou removed; entidades managed passam por dirty checking e são gravadas no flush, e
mergeretorna a cópia managed em vez de anexar o seu objeto. - Mapeie identificadores com
SEQUENCEquando o banco permitir;IDENTITYdesativa o batching eUUIDé padrão desde o Persistence 3.1. - O lado dono de uma associação guarda a chave estrangeira; mantenha os dois lados das associações bidirecionais consistentes com métodos auxiliares.
- Deixe as associações to-one lazy e faça fetch explícito por caso de uso; use cascata e remoção de órfãos só ao longo de composições.
- Use
@MappedSuperclasspara campos de auditoria compartilhados, e herança de entidades só quando o modelo for realmente polimórfico. - Adicione
@Versionàs entidades editáveis e leve a versão pela interface ou pela API para evitar atualizações perdidas.
Fontes e leituras complementares
Perguntas frequentes
- Qual a diferença entre JPA e Hibernate?
- JPA (Jakarta Persistence) é a especificação: as anotações, a API do EntityManager, o JPQL e a API Criteria. O Hibernate ORM é a implementação mais usada dela, ao lado do EclipseLink, que é a implementação compatível usada para certificar a especificação. Código escrito contra a API do JPA roda em qualquer uma das duas; anotações específicas do Hibernate e métodos da Session, não. Desde o Hibernate 6 os dois convergiram ainda mais, e o HQL do Hibernate é um superconjunto do JPQL.
- Por que recebo LazyInitializationException?
- Você acessou uma associação lazy depois que o contexto de persistência que carregou a entidade foi fechado, normalmente numa página Faces ou num serializador REST, depois do fim da transação. Resolva na consulta: use um JOIN FETCH ou um entity graph para carregar dentro da transação o que a view precisa, ou converta o resultado em DTO antes de retorná-lo. Deixar tudo EAGER não é solução; só transfere o custo para todas as consultas.
- Devo usar merge ou persist para salvar uma entidade?
- persist para um objeto novo que você criou com new: ele passa a ser managed e um INSERT é agendado. merge para um objeto detached (vindo de uma transação anterior, de um formulário ou de uma requisição desserializada): ele copia o estado para uma instância managed e retorna essa instância, enquanto o objeto que você passou continua detached. Chamar persist numa entidade detached lança exceção, e chamar merge numa entidade nova funciona, mas dispara um SELECT extra.
- Minhas entidades precisam de equals e hashCode?
- Só se você colocar entidades num Set ou usá-las como chaves de map, o que os mapeamentos many-to-many e one-to-many com Set fazem. Baseie-os numa chave de negócio ou no identificador depois que ele for atribuído, nunca em todos os campos, e nunca num id gerado antes do persist: o hash mudaria quando o id fosse gerado e o objeto sumiria de qualquer HashSet em que já estivesse.