Entendendo a geração de chaves com JPA

Entendendo a geração de chaves com JPA

Sempre que escrevemos uma entidade utilizando JPA, precisamos definir qual a chave primária desta e geralmente, também colocamos uma estratégia para geração das chaves.

Por padrão, quando utilizamos somente a anotação @GeneratedValue sem passar nenhum argumento, a JPA utiliza a estratégia automática (GenerationType.AUTO). Ou seja, a implementação da especificação escolherá uma estratégia para geração dos ids.

Por exemplo, utilizando o Hibernate como implementação da JPA, a estratégia AUTO escolherá o método de sequências (sequence) para o banco Postgres. Agora quando utilizamos o MySQL, qual estratégia esperamos?

Podemos pensar que ele usará o AUTO_INCREMENT, porém, a partir da versão 5 do Hibernate , a estratégia utilizada é a TABLE que já veremos como ela funciona. Mas já podemos falar que essa estratégia tem alguns problemas relacionados a performance.

A partir do Hibernate 5, toda vez que é utilizado o GenerationType.AUTO, ele tentará utilizar sequences. Caso não consiga, ele utilizará GenerationType.TABLE. Por não termos controle da estratégia de geração, muitas pessoas optam por não utilizar o GenerationType.AUTO.

Além disso, pelo fato de a partir da versão 5 o Hibernate colocar a estratégia TABLE como padrão, não é recomendado utilizar essa estratégia Mas qual o problema da GenerationType.TABLE?

Conhecendo o GenerationType.TABLE

Essa é a estratégia que funciona em todos os bancos de dados relacionais, sendo compatível com todas as soluções do mercado, pois todos os bancos possuem tabelas. Parece perfeita, não? Então qual o problema com ela?

Quando utilizamos a GenerationType.TABLE, o Hibernate utilizará uma tabela para gerar as chaves primárias. Essa tabela pode ser global, servir para todas as entidades no banco, ou específica para uma entidade.

Podemos definir a tabela geradora pelo parâmetro generator da anotação @GeneratedValue e através da anotação @TableGenerator definir a tabela que será usada para gerar os ids:

@Entity
public class Livro {
    @Id
    @GeneratedValue(
        strategy = GenerationType.TABLE, 
        generator = "tabela_id_livros"
    )
    @TableGenerator(
        name =  "tabela_id_livros",
        table = "ids_livro",
    )
    private Long id;

    // restantes dos atributos e métodos 
}

O problema de utilizar a TABLE como estratégia está justamente no fato de precisarmos de uma tabela para ficar armazenando os próximos ids. Mas qual o problema de fato com isso?

O problema começa quando começamos a escalar a aplicação. O que acontece se duas entidades sejam persistidas ao mesmo tempo? Precisamos, sempre, fazer um lock pessimista da tabela que gera os ids. Não só isso, mas o que acontece se tivermos que usar outras instâncias do banco de dados? Como faremos com essa tabela?

Por coisas assim, a recomendação é nunca utilizar o GenerationType.TABLE, dado seus problemas com escalabilidade.

Como o GenerationType.AUTO usará o TABLE caso o banco de dados não suporte sequences (isso a partir do Hibernate 5), as pessoas preferem utilizar outras estratégias mais específicas como a GenerationType.SEQUENCE e GenerationType.IDENTITY

Banner promocional da Alura, com um design futurista em tons de azul, apresentando dois blocos de texto, no qual o bloco esquerdo tem os dizeres:

Criando sequências com o GenerationType.SEQUENCE

Quando utilizamos o GenerationType.SEQUENCE, o Hibernate utilizará as sequences do banco de dados para gerar as chaves primárias. Da mesma forma que TABLE, podemos ter apenas uma sequence para todas as entidades, ou sequences específicas para cada tabela.

O bom de utilizar as sequences é que, pensando em performance, podemos utilizar o batch do JDBC na hora da inserção e atualização.

O problema de utilizar sequences é que nem todos os bancos de dados oferecem suporte a esse tipo de estratégia de geração. Portanto, utilizar essa estratégia aumenta um pouco o acoplamento com o banco utilizado (se bem que não é todo o dia que trocamos o banco de dados).

Podemos definir o gerador das sequências através da anotação @SequenceGenerator de uma forma parecida com a @TableGenerator.

@Entity
public class Livro {
    @Id
    @GeneratedValue(
        strategy = GenerationType.SEQUENCE, 
        generator = "sequence_id_livros"
    )
    @SequenceGenerator(
        name =  "sequence_id_livros",
        sequenceName = "sequence_livro",
    )
    private Long id;

    // restantes dos atributos e métodos 
}

Com essas anotações, será criada uma sequência chamada sequence_livro que será usada para popular os ids da entidade Livro. Além de sequences, podemos falar para o banco gerar os números da coluna id através da estratégia Identity.

Conhecendo a estratégia Identity

A estratégia GenerationType.IDENTITY é, talvez, uma das mais utilizadas no mundo da JPA.

@Entity
public class Livro {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // restantes dos atributos e métodos 
}

Vamos supor que estamos utilizando um banco de dados como o MySQL. Quando falamos que a estratégia de criação é a IDENTITY, o Hibernate utilizará como estratégia a geração AUTO_INCREMENT. Já, se o banco de dados for o Postgres, o Hibernate gerará uma coluna do tipo SERIAL.

Isto é, a cada nova inserção, uma chave primária será gerada para a entidade. Isso é um pouco menos performático para o Hibernate, pois ele utiliza a chave primária para gerenciar as entidades. Ou seja, neste caso, ele precisa fazer a inserção no banco imediatamente, não conseguindo usar técnicas como o JDBC batching.

Para saber mais

Podemos usar o Hibernate para gerar UUIDs também. Dessa forma, caso algum de nossos modelos utilizem um UUID como chave primária, o Hibernate consegue gerá-la também.

Uma curiosidade sobre usar ids sequenciais é que isso pode trazer algumas informações na hora que estamos analisando os dados do banco. Por exemplo, um id com o número mais baixo significa que é um cadastro mais antigo, enquanto números altos é um id mais recente.

Aqui na Alura, temos uma formação Java onde vemos mais sobre JPA e Hibernate. Nessa formação, você verá como começar a utilizar a especificação, entenderá alguns conceitos como EntityManager e LazyLoading, aprenderá a fazer consultas com JPQL, relacionar entidades e muito mais.

Yuri Matheus
Yuri Matheus

Yuri é desenvolvedor e instrutor. É estudante de Sistemas de Informação na FIAP e formado como Técnico em Informática no Senac SP. O seu foco é nas plataformas Java e Python e em outras áreas como Arquitetura de Software e Machine Learning. Yuri também atua como editor de conteúdo no blog da Alura, onde escreve, principalmente, sobre Redes, Docker, Linux, Java e Python.

Veja outros artigos sobre Programação