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:

EstadoSignificadoComo se chega lá
New (transient)Um objeto comum; o contexto não sabe nada sobre elenew Order()
ManagedAcompanhado pelo contexto; as alterações são detectadas e gravadas no flushpersist, find, getReference, o resultado de uma consulta, o valor retornado por merge
DetachedEra managed por um contexto que já foi fechado, ou foi desanexado explicitamenteFim da transação, detach, clear, serialização
RemovedAgendado para um DELETE no flushremove 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. Com allocationSize, 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 ao INCREMENT BY da 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 do INSERT, o que obriga o provedor a inserir imediatamente no persist e desativa o batching de inserts.
  • UUID: novidade do Persistence 3.1; o provedor gera um java.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. @JoinColumn dá o nome a ela.
  • @OneToMany(mappedBy = "order") é o lado inverso. Adicionar um item apenas a order.getLines() não grava nada; quem preenche a chave estrangeira é line.setOrder(order). Métodos auxiliares como addLine mantêm os dois lados sincronizados.
  • Um @OneToMany unidirecional sem mappedBy usa por padrão uma tabela de junção, o que pega muita gente de surpresa; adicione @JoinColumn para usar, em vez disso, uma chave estrangeira na tabela filha.
  • @OneToOne coloca 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.
  • @ManyToMany usa uma tabela de junção dos dois lados; @JoinTable dá 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égiaTabelasTrade-off
SINGLE_TABLE (padrão)Uma tabela para a hierarquia inteira, com uma coluna discriminadoraConsultas mais rápidas, sem joins; as colunas das subclasses precisam aceitar NULL
JOINEDUma tabela por classe, unidas pela chave primáriaNormalizado, sem nulos obrigatórios; toda consulta polimórfica faz joins
TABLE_PER_CLASSUma tabela completa por classe concretaSem 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 LAZY e faça fetch por caso de uso.
  • Acessar coleções lazy fora da transação. LazyInitializationException em views e serializadores. Faça fetch na consulta ou converta para DTO dentro do service.
  • Esquecer o lado dono. Adicionar itens à coleção mappedBy nã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. Use STRING.
  • cascade = ALL em entidades compartilhadas. Remover um pedido apaga o cliente dele. Use cascata só ao longo de composições.
  • hashCode baseado em id gerado. Muda depois do persist; as entidades somem dos sets. Use uma chave de negócio ou um UUID estável atribuído no construtor.
  • allocationSize diferente 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 EntityManager gerenciado 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 merge retorna a cópia managed em vez de anexar o seu objeto.
  • Mapeie identificadores com SEQUENCE quando o banco permitir; IDENTITY desativa o batching e UUID é 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 @MappedSuperclass para 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.
JPQL, a API Criteria e consultas nativas no Jakarta PersistenceConsulte entidades com JPQL (joins, fetch joins, agregações, projeções, paginação), monte consultas Criteria tipadas e recorra a SQL nativo quando precisar.Intermediário8 min de leituraJakarta Transactions (JTA 2): transações gerenciadas pelo contêiner, propagação e XAComo as transações JTA funcionam em um servidor Jakarta EE: isolamento, propagação do @Transactional, regras de rollback, UserTransaction e commit XA.Intermediário11 min de leituraEnterprise Beans 4: session beans stateless, stateful e singletonO que os session beans EJB oferecem (pooling, transações, segurança, timers, chamadas assíncronas), como diferem e quando um bean CDI é a melhor escolha.Intermediário10 min de leituraSpringIntegrando JPA e Hibernate ao SpringConfigure EntityManagerFactory e JpaTransactionManager em Java, use o EntityManager compartilhado e evite lazy loading, N+1 e Open Session in View.Intermediário8 min de leituraSpring BootSpring Data JPA com Spring Boot: repositórios, consultas, transações e migraçõesSpring Data JPA no Spring Boot 3 e Hibernate 6: repositórios, métodos derivados e @Query, paginação, projeções, transações, N+1 e Flyway.Intermediário8 min de leituraDadosModelagem de banco de dados e normalização: das entidades à FNBCModele entidades, chaves e dependências funcionais, vá da 1FN à FNBC em um exemplo, escolha chaves e desnormalize de forma consciente.Iniciante9 min de leitura