Gerar código UNIQUE ANO + Sequencial usando Trigger - Problemas com concorrência

Prezados, estamos com uma dúvida aqui no meu trabalho com relação a geração de um código. É um sistema WEB desenvolvido em JAVA e com vários atendentes o utilizando no Brasil inteiro. O Banco de dados é Oracle. Todos as classes Services estão com a annotation @Transactional do Spring.

Temos uma tabela chamada MANIFESTACAO, que possui uma sequence como chave primária (campo ID_MANIFESTACAO). Essa tabela também possui vários outros campos. Até agora, toda consulta para identificar cada manifestação é realizada através do campo ID_MANIFESTACAO.

Nosso cliente quer, agora, que a consulta seja feita através de outro código, que seja mais legível. Sugeriu, então, que cada manifestação, daqui por diante, ganhe um novo código que irá identificá-la. Esse código deve ser no formato AAAANNNNNNN, onde o AAAA é o ano corrente e os NNNNNNN é uma sequencia de números. Na virada de ano, a sequencia de números é zerada.

Exemplos desse novo código:
20140000001
20140000002
.
.
.
20140000325
.
.
.
20150000001
20150000002

Para cada registro guardado na tabela manifestação, é gerado um código que é o incremento do código anterior, exceto na virada do ano.

Precisamos criar um campo CODIGO (que será unique) na tabela MANIFESTACAO. Até aí, tudo bem. O problema que estou passando é justamente como gerar esse código.

Criamos uma trigger para a geração automática desses códigos. Criamos também uma tabela chamada GERACAO_CODIGO, que possui 2 campos, ANO e CONTADOR, para ajudar na criação dos códigos. Não sabemos se a trigger é a melhor alternativa

Essa tabela GERACAO_CODIGO terá apenas 2 campos, o campo ANO e o campo CONTADOR. Inicialmente, a tabela conterá apenas 1 registro (abaixo):

ANO CONTADOR
2014 0

Essa tabela servirá apenas para ajudar na geração do campo CODIGO da tabela MANIFESTACAO. O campo CODIGO será formado pelo ano corrente da data do cadastro da manifestação concatenado om o campo CONTADOR + 1 da tabela GERACAO_CODIGO.

Então, após o primeiro insert na tabela MANIFESTACAO, o campo CODIGO receberá o valor 20140000001 e a tabela GERACAO_CODIGO sofrerá um update, ficando assim:

ANO CONTADOR
2014 1

Com novas inserções na tabela MANIFESTACAO, novos códigos serão gerados e novos updates na tabela GERACAO_CODIGO serão relizados:

ANO CONTADOR
2014 2

ANO CONTADOR
2014 3

.
.
.

ANO CONTADOR
2014 1569

Para a geração do CODIGO, optamos por utilizar uma trigger BEFORE INSERT para a tabela MANIFESTACAO.

O código para essa trigger é o seguinte:

[code]CREATE OR REPLACE TRIGGER TRIGGER_GERA_CODIGO BEFORE INSERT ON MANIFESTACAO
FOR EACH ROW
DECLARE
P_ANO NUMBER(4);
P_CONTADOR NUMBER(11);
P_CODIGO NUMBER(11);
BEGIN

--recupera o ano da data do cadastro da manifestação
P_ANO := to_number(tochar(:NEW.DT_CADASTRO_MANIFESTACAO,'yyyy'));

--recupera o valor do campo contador e coloca o resultado na variável P_CONTADOR, já incrementando o seu valor. A variável P_CONTADOR ajudará a formar o campo CODIGO
SELECT NVL(CONTADOR + 1,1) 
INTO P_CONTADOR
FROM GERACAO_CODIGO WHERE ANO = PANO;

--atualiza a tabela GERACAO_CODIGO. Se a variável P_CONTADOR for maior que 1, devo apenas atualizar o campo CONTADOR do ano correspondente com o seu novo valor, já incrementado
IF (P_CONTADOR > 1) THEN
	UPDATE GERACAO_CODIGO
    SET CONTADOR = P_CONTADOR
    WHERE ANO = P_ANO;        
ELSE --atualiza a tabela GERACAO_CODIGO. Se a variável P_CONTADOR for igual a 1, significa que houve virada de ano, então eu devo inserir um registro na tabela GERACAO_CODIGO
    INSERT INTO GERACAO_CODIGO values(P_ANO,0);
END IF;

--gera o valor do campo CODIGO, a partir do ano da manifestação e da variável P_CONTADOR
PCODIGO := TO_NUMBER(PANO || LPAD(P_CONTADOR,7,0));
:NEW.CODIGO := PCODIGO;

END;
[/code]

A trigger está funcionando e os códigos estão sendo gerados corretamente. A minha dúvida é com relação à concorrência. Se duas transações passarem pelo trecho da trigger abaixo ao mesmo tempo, elas terão o mesmo valor atribuído à variável P_CONTADOR, o que gerá um CODIGO igual ao da outra transação, causando um erro na hora do insert, posto que o campo CODIGO é UNIQUE.

SELECT NVL(CONTADOR + 1,1) INTO P_CONTADOR FROM GERACAO_CODIGO WHERE ANO = PANO;

Para resolver isso, eu preciso bloquear a tabela GERACAO_CODIGO para que apenas 1 transação possa ler os dados dessa tabela por vez, ou seja, antes do trecho de código acima, a tabela teria que ser lockada e só liberada após um commit ou rollback.

Não possuo experiência com locks de tabelas no Oracle, gostaria que, por favor, alguém com mais experiência me ajudasse a resolver este problema. Um colega em outro fórum, sugeriu que eu colocasse o seguinte trecho de código bem antes do último trecho de código citado acima:

LOCK TABLE geracao_codigo IN EXCLUSIVE MODE NOWAIT;

[b]Ainda não cheguei a testar, pois não pude ir ao trabalho hoje. Irei testar amanhã. Alguém possui mais alguma sugestão. Esse lock descrito acima garante que a tabela GERACAO_CODIGO fique locada inclusive para leitura?

Antecipadamente agradeço a ajuda![/b]

Honestamente eu tiraria a trigger e passaria essa responsabilidade para o código java.

Se você já está usando o Spring, ele já faz esse controle automaticamente. Quando você passa esse controle ao banco de dados esse tipo de problema começa a aparecer pois: você está usando uma transação controlada pelo servidor, mas está disparando uma trigger que está fora do contexto da transação.

O Spring não tem a mínima idéia do que está acontecendo no DB. Outro problema que você não visualizou mas pode acontecer é:

  1. Você salva a entidade objetoA.
  2. Antes de salvar, o objeto vai ter o campo VALOR_DA_TRIGGER atualizado no BD
  3. Ao receber OK do commit, objetoA pode estar com esse campo valorDaTrigger como null. (Se não me engano, o JPA não é obrigado a sincronizar nada após o persist.
  4. Qualquer alteração feita no valorDaTrigger ou na entidade, pode disparar uma alteração no DB sobrescrevendo seu valor.

Por isso eu digo arrancar essa trigger e deixar o controle no projeto.

Agora, se o banco for utilizado por mais de uma aplicação aí vocẽ terá que adotar uma outra estratégia. Eu diria que seria melhor persistir por uma stored procedure por exemplo.

Olá, Hebert, obrigado pela ajuda.

Pensei em gerar o código pela aplicação mesmo, apesar de dar bem mais trabalho. O banco é usado apenas por uma aplicação.

Se eu for fazer pela aplicação, em algum momento eu terei que realizar um select na tabela GERACAO_CODIGO para recuperar o valor do CONTADOR para o ano corrente. Só depois disso que eu geraria o valor do campo CODIGO para a outra tabela. O Spring não vai garantir que 2 transações não leiam o mesmo valor do campo CONTADOR.

Eu teria que, de alguma forma, pela aplicação, travar a leitura da tabela GERACAO_CODIGO após a primeira transação ler o seu valor e só liberar após um commit ou rollback, não é isso?

[quote=Hebert Coelho]Honestamente eu tiraria a trigger e passaria essa responsabilidade para o código java.

Se você já está usando o Spring, ele já faz esse controle automaticamente. Quando você passa esse controle ao banco de dados esse tipo de problema começa a aparecer pois: você está usando uma transação controlada pelo servidor, mas está disparando uma trigger que está fora do contexto da transação.

O Spring não tem a mínima idéia do que está acontecendo no DB. Outro problema que você não visualizou mas pode acontecer é:

  1. Você salva a entidade objetoA.
  2. Antes de salvar, o objeto vai ter o campo VALOR_DA_TRIGGER atualizado no BD
  3. Ao receber OK do commit, objetoA pode estar com esse campo valorDaTrigger como null. (Se não me engano, o JPA não é obrigado a sincronizar nada após o persist.
  4. Qualquer alteração feita no valorDaTrigger ou na entidade, pode disparar uma alteração no DB sobrescrevendo seu valor.

Por isso eu digo arrancar essa trigger e deixar o controle no projeto.

Agora, se o banco for utilizado por mais de uma aplicação aí vocẽ terá que adotar uma outra estratégia. Eu diria que seria melhor persistir por uma stored procedure por exemplo.[/quote]

[quote=Aroldo Rique]Olá, Hebert, obrigado pela ajuda.

Pensei em gerar o código pela aplicação mesmo, apesar de dar bem mais trabalho. O banco é usado apenas por uma aplicação.

Se eu for fazer pela aplicação, em algum momento eu terei que realizar um select na tabela GERACAO_CODIGO para recuperar o valor do CONTADOR para o ano corrente. Só depois disso que eu geraria o valor do campo CODIGO para a outra tabela. O Spring não vai garantir que 2 transações não leiam o mesmo valor do campo CONTADOR.

Eu teria que, de alguma forma, pela aplicação, travar a leitura da tabela GERACAO_CODIGO após a primeira transação ler o seu valor e só liberar após um commit ou rollback, não é isso?
[/quote]

O Spring poderá garantir se você usar a questão do LOCK do JPA, pesquise sobre isso que você encontrará diversos materiais pela internet.

Bastaria você localizar o registro, incrementar o valor e depois persistir a nova chave.

Primeira alternativa:
Seu cliente faz absoluta questão que esses códigos gerados sejam sequenciais?
Ele pode ser convencido a ter buracos nesse código?

Se ele aceitar buracos, você pode usar uma sequence para gerar esse código concatenando com o ano atual.
(Teria também que criar um job ou procedimento para resetar a sequence quando o ano virar)

Isso evita qualquer problema de concorrência pois as sequences operam fora de qualquer transação. Por isso não há garantia que os valores gerados por ela são sequenciais sem “buracos”.

Segunda alternativa:
Usar um timestamp (até milisegundos) para representar esse código.
Isso daria um código ainda mais legível e uma maior precisão (o código no formato que passou tem capacidade até 3 transações por segundo por ano)
Nesse caso, usar o timestamp no timezone UTC seria o ideal para evitar problemas com fuso horário

Terceira alternativa:
Crie uma tabela com duas colunas: ano e valor atual.
Para gerar um novo código, comece sempre incrementando o valor atual. Ao fazer isso, o Oracle vai gerar um bloqueio automático nessa linha sendo atualizada, e resolve seu problema de concorrência. (Lembre-se de que você deve atualizar o valor, ler o novo valor e usar na tabela de manifestação durante a mesma transação).
Essa abordagem pode fazer exatamente o que o cliente quer, mas gera contenção para gerar manifestações.
Na prática, você nunca poderia criar duas manifestações ao mesmo tempo, pois precisa esperar uma terminar, para gerar o código, antes de começar outra.

Eu fugiria de triggers, stored procedures ou comandos de lock explícitos.
Geralmente é mais complicado para automatizar os testes e fica mais complicado para novas pessoas no time entender o que está rolando.