PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Anotação de hibernação para o tipo serial do PostgreSQL


Perigo: Sua pergunta implica que você pode estar cometendo um erro de design - você está tentando usar uma sequência de banco de dados para um valor "comercial" que é apresentado aos usuários, neste caso, números de fatura.

Não use uma sequência se precisar de algo mais do que testar o valor para igualdade. Não tem ordem. Não tem "distância" de outro valor. É apenas igual, ou não igual.

Reversão: As sequências geralmente não são apropriadas para esses usos porque as alterações nas sequências não são revertidas com a transação ROLLBACK . Veja os rodapés em function-sequence e CREATE SEQUENCE .

Reversões são esperadas e normais. Eles ocorrem devido a:
  • deadlocks causados ​​por ordem de atualização conflitante ou outros bloqueios entre duas transações;
  • reversões de bloqueio otimistas no Hibernate;
  • erros transitórios do cliente;
  • manutenção do servidor pelo DBA;
  • conflitos de serialização em SERIALIZABLE ou transações de isolamento de instantâneo

... e mais.

Seu aplicativo terá "buracos" na numeração da fatura onde ocorrem essas reversões. Além disso, não há garantia de pedido, então é totalmente possível que uma transação com um número de sequência posterior seja confirmada mais cedo (às vezes muito anterior) do que um com um número posterior.

Agrupamento:

Também é normal para alguns aplicativos, incluindo o Hibernate, pegar mais de um valor de uma sequência por vez e distribuí-los para transações internamente. Isso é permitido porque você não deve esperar que os valores gerados pela sequência tenham qualquer ordem significativa ou sejam comparáveis ​​de qualquer forma, exceto pela igualdade. Para a numeração de faturas, você também quer fazer o pedido, então não será de forma alguma feliz se o Hibernate pegar os valores 5900-5999 e começar a distribuí-los a partir de 5999 contando regressivamente ou alternadamente para cima e para baixo, então seus números de fatura são:n, n+1, n+49, n+2, n+48, ... n+50, n+99, n+51, n+98, [n+52 perdido na reversão], n+97, ... . Sim, o alocador high-the-low existe no Hibernate.

Isso não ajuda, a menos que você defina @SequenceGenerator individual s em seus mapeamentos, o Hibernate gosta de compartilhar uma única sequência para cada ID gerado, também. Feio.

Uso correto:

Uma sequência só é apropriada se você somente exigem que a numeração seja única. Se você também precisa que seja monotônico e ordinal, você deve pensar em usar uma tabela comum com um campo de contador via UPDATE ... RETURNING ou SELECT ... FOR UPDATE ("bloqueio pessimista" no Hibernate) ou via bloqueio otimista do Hibernate. Dessa forma, você pode garantir incrementos sem intervalos, sem furos ou entradas fora de ordem.

O que fazer em vez disso:

Crie uma tabela apenas para um balcão. Tenha uma única linha nela e atualize-a à medida que a lê. Isso irá bloqueá-lo, impedindo que outras transações obtenham um ID até que o seu seja confirmado.

Como ele força todas as suas transações a operar em série, tente manter as transações que geram IDs de fatura curtas e evite fazer mais trabalho do que o necessário.
CREATE TABLE invoice_number (
    last_invoice_number integer primary key
);

-- PostgreSQL specific hack you can use to make
-- really sure only one row ever exists
CREATE UNIQUE INDEX there_can_be_only_one 
ON invoice_number( (1) );

-- Start the sequence so the first returned value is 1
INSERT INTO invoice_number(last_invoice_number) VALUES (0);

-- To get a number; PostgreSQL specific but cleaner.
-- Use as a native query from Hibernate.
UPDATE invoice_number
SET last_invoice_number = last_invoice_number + 1
RETURNING last_invoice_number;

Alternativamente, você pode:
  • Defina uma entidade para fatura_number, adicione um @Version coluna e deixe o bloqueio otimista cuidar dos conflitos;
  • Defina uma entidade para invoice_number e use o bloqueio pessimista explícito no Hibernate para fazer uma seleção ... para atualização e depois uma atualização.

Todas essas opções serializarão suas transações - revertendo conflitos usando @Version ou bloqueando-os (bloqueando) até que o detentor do bloqueio confirme. De qualquer forma, sequências sem intervalos irão realmente reduza a velocidade dessa área do seu aplicativo, portanto, use apenas sequências sem intervalos quando necessário.

@GenerationType.TABLE :É tentador usar @GenerationType.TABLE com um @TableGenerator(initialValue=1, ...) . Infelizmente, enquanto GenerationType.TABLE permite especificar um tamanho de alocação via @TableGenerator, ele não fornece nenhuma garantia sobre o comportamento de ordenação ou reversão. Consulte a especificação JPA 2.0, seção 11.1.46 e 11.1.17. Em particular "Esta especificação não define o comportamento exato dessas estratégias. e nota de rodapé 102 "Aplicativos portáteis não devem usar a anotação GeneratedValue em outros campos ou propriedades persistentes [além de @Id chaves primárias]" . Portanto, não é seguro usar @GenerationType.TABLE para numeração que você precisa ser sem intervalos ou numeração que não esteja em uma propriedade de chave primária, a menos que seu provedor JPA dê mais garantias do que o padrão.

Se você estiver preso a uma sequência :

O pôster observa que eles têm aplicativos existentes usando o banco de dados que já usam uma sequência, então eles estão presos a ela.

O padrão JPA não garante que você possa usar colunas geradas exceto em @Id, você pode (a) ignorar isso e seguir em frente enquanto seu provedor permitir, ou (b) fazer a inserção com um valor padrão e re -ler do banco de dados. O último é mais seguro:
    @Column(name = "inv_seq", insertable=false, updatable=false)
    public Integer getInvoiceSeq() {
        return invoiceSeq;
    }

Por causa de insertable=false o provedor não tentará especificar um valor para a coluna. Agora você pode definir um DEFAULT adequado no banco de dados, como nextval('some_sequence') e será honrado. Você pode ter que reler a entidade do banco de dados com EntityManager.refresh() depois de persistir - não tenho certeza se o provedor de persistência fará isso por você e não verifiquei a especificação ou escrevi um programa de demonstração.

A única desvantagem é que parece que a coluna não pode ser feita @ NotNull ou nullable=false , pois o provedor não entende que o banco de dados tem um padrão para a coluna. Ainda pode ser NOT NULL no banco de dados.

Se você tiver sorte, seus outros aplicativos também usarão a abordagem padrão de omitir a coluna de sequência do INSERT lista de colunas de ou especificando explicitamente a palavra-chave DEFAULT como o valor, em vez de chamar nextval . Não será difícil descobrir isso ativando log_statement = 'all' em postgresql.conf e pesquisando os logs. Se o fizerem, você poderá mudar tudo para sem intervalos se decidir que precisa substituindo seu DEFAULT com um BEFORE INSERT ... FOR EACH ROW função de gatilho que define NEW.invoice_number da mesa do balcão.