Oracle-ADF-1508x706_c

ADF: JBO-25014: Another user has changed the row with primary key

Ao trabalhar com o framework Oracle ADF 11g é muito comum nos depararmos com o famigerado erro JBO-25014: Another user has changed the row with primary key, no qual significa que 2 usuários tentaram modificar o mesmo registro no banco ao mesmo tempo. Na verdade, o que acontece é que o framework está alertando o último usuário que o registro que ele previamente havia carregado em tela foi modificado antes dele finalizar sua atualização, ou seja, se trata de um sistema de detecção de um fenômeno bastante conhecido em sistemas de alta concorrência, o Lost Update.

adf-lost-update-detection

Entender o Lost Update Problem não é difícil:

A prolemática é simples, basta imaginar que 2 transações querem alterar o mesmo registro numa tabela, a segunda transação irá sobrescrever os dados da primeira, portanto descartando a atualização da primeira transação.

Dessa forma, um UPDATE é perdido quando um usuário sobrescreve o estado atual do banco de dados sem perceber que outro usuário havia alterado a mesma informação entre o momento do carregamento do registro e o momento que a atualização ocorre.

Para detectar Lost Updates, o ADF BC (ADF Business Component) se utiliza por padrão do modelo de concorrência Optimistc Locking (lock otimista). A idéia desde modelo é versionar (atribuir uma versão) as entidades de tal forma que a aplicação possa validar a versão da entidade antes de atualizar seu registro correspondente no banco; se a versão do registro a ser atualizado não confere com o registro no banco então o erro ocorre. Para implementar este vesionamento, é muito comum a entidade ter uma coluna do tipo INTEGER ou TIMESTAMP, pois ambas são facilmente incrementadas a cada UPDATE na tabela. Sua maior vantagem está na escalabilidade, pois a aplicação não precisará fazer um lock físico (Pessimistic Locking) no registro da tabela, permitindo assim que outras transações possam ler e/ou escrever no mesmo registro concorrentemente.

Este sistema de detecção de conflitos do ADF faz todo sentido em aplicações web onde temos diversos usuários enviando requisições concorrentemente para aplicação; se dois usuários tentarem modificar o mesmo registro ao mesmo tempo o erro JBO-25014 ocorre. No entanto, a forma como o ADF aplica o modelo Optimistic Locking faz com que o erro apareça mesmo quando existe somente um único usuário na aplicação.

O problema do Usuário Fantasma (Phantom User)

Embora o ADF BC evite o Lost Update de maneira automática, ele se utiliza de uma abordagem peculiar para versionar as entidades do sistema; em vez de uma coluna “Versao” do tipo INTEGER, o framework utiliza todos os campos da entidade como alternativa de versionamento, afinal de contas ele não tem como obrigar o desenvolvedor ou DBA a colocar uma coluna de versionamento na tabela. Esta abordagem simplifica a vida dos desenvolvedores, mas ela apresenta algumas fragilidades que acabam por desencadear a exceção oracle.jbo.RowInconsistentException: JBO-25014: Another user has changed the row with primary key quando menos esperamos.

Por esse motivo, é muito comum cairmos neste problema durante o desenvolvimento, onde só existe um único usuário interagindo com a aplicação, ou seja, o desenvolvedor. Neste caso, o erro pode aparecer após algumas interações numa tela de cadastro; é como se houvesse um outro usuário, um usuário fantasma, na aplicação enquanto fazemos nossos testes locais – o que não faz o menor sentido! Este problema um tanto quanto estranho é decorrente da forma como o ADF versiona suas entidades.

Como já falamos, o ADF usa todas os campos da entidade como estratégia de versionamento, desse modo, para detectar se algum outro usuário (ou transação) modificou uma entidade no banco de dados, o ADF armazena em memória (Entity Cache) os valores originais dos atributos da entidade durante seu carregamento e, no momento de atualizar o registro na tabela, o framework compara cada atributo com sua respectiva coluna no banco; se a entidade e o registro no banco não conferem (estão inconsistentes), o erro RowInconsistentException é lançado.

Perceba a fragilidade desta abordagem, todos os campos da entidade em memória devem estar consistentes com seu registro mais recente no banco, basta uma pequena inconsistência e temos o problema. Não é à toa que este erro pode ser causado comumente pelos seguintes motivos:

  1. Triggers no banco de dados: ao enviar uma atualização pro banco uma trigger é disparada modificando algumas colunas da tabela; como o ADF BC não tem ciência destas modificações, afinal elas foram feitas dentro do SGBD, ele mantém as entidades desatualizadas em seu cache interno, o que leva ao problema exatamente no próximo commit;
  2. Colunas com Default Value: configurar uma coluna no banco com um valor padrão (default value) pode lançar o erro pois a atualização da coluna é de responsabilidade do SGBD; cair neste cenário não é difícil, basta inserirmos uma entidade sem preencher o valor de uma coluna com default value, dessa forma o cache interno ficará desatualizado; em seguida ao tentarmos atualizar a mesma entidade o erro acontece pois o atributo não preenchido estará inconsistente com o banco;
  3. Atributo ROWID na Entity Object: usar um atributo ROWID pode ocasionalmente desencadear o erro; apesar de incomum, algumas configurações do SGBD, como particionamento, podem modificar o valor do ROWID quando uma linha da tabela é modificada;
  4. Select customizado na View Object: escrever um SELECT customizado no “Expert Mode” de uma View Object sem a correta sincronização ou mapeamento com os atributos da View Object; escrever uma consulta na qual preenche os atributos de uma View Object de maneira incorreta pode disparar o erro;
  5. Atributo de domínio customizado: usar uma classe de domínio para representar um atributo da entidade sem definir corretamente seu critério de igualdade;
  6. Múltiplos Application Modules: implementar uma funcionalidade na qual se utiliza de dois ou mais Application Modules (AM) pode acarretar o erro; basta um AM ser responsável pelo carregamento da entidade e o outro pela atualização que o problema é passível de acontecer;

A maioria das vezes estes serão os principais causadores da exceção. Contudo, outros motivos podem fazer com que o erro aconteça, por exemplo a feature Passivation/Activation do ADF, embora estes sejam mais incomuns no dia a dia.

Solução rápida para JBO-25014

Para cada situação acima há uma possível solução na qual despenderá pouco esforço para o desenvolvedor. Basta verificar em qual situação sua entidade se encontra e em seguida aplicar sua solução correspondente. Segue as soluções:

  1. Para 1a, 2a e 3a situação basta habilitar a feature Refresh After Insert/Update do atributo da entidade que é modificado pelo SGBD, seja via trigger, defualt value ou PLSQL (a feature se encarregará de recarregar o atributo do banco após INSERT e/ou UPDATE);
  2. Para 4a situação, verifique se o mapeamento das colunas com os atributos estão de acordo como esperado, caso contrário corrija-os;
  3. Para 5a situação, implemente corretamente os métodos equals() e hashCode() (estes métodos são responsáveis pela identidade de um objeto);
  4. Para 6a situação, o ideal seria carregar e atualizar a entidade dentro do mesmo AM, dessa forma ambas as operações estariam dentro da mesma transação; caso não seja possível, certifique-se de recarregar a entidade no momento apropriado;

As soluções acima funcionam como “receita de bolo” para as causas mais comuns e certamente resolverão a maioria dos casos, mas em alguns momentos talvez elas não sejam suficientes.

Solução pontual para JBO-25014: definindo o atributo versionador

Em alguns momentos não será tão fácil resolver o problema, pois talvez o atributo problemático não seja exatamente aquele que pensamos que é. Para falar a verdade, algumas vezes a inconsistência pode estar em mais de um atributo. Mas como descobrir quais deles é o causador do erro?

Na maioria das vezes o problema está em atributos do tipo data-hora como DATE ou TIMESTAMP, ou atributos com alta precisão, como DOUBLE. Mas para não ficarmos feito louco tentando adivinhar o atributo inconsistente, o ideal é tirarmos proveito dos logs do próprio ADF. Sempre que o JBO-25014 ocorre, o ADF BC imprime o log de falha indicando os atributos inconsistentes:

<EntityImpl><compare> [3359] Entity compare failed for attribute DataDeAtualizacao
<EntityImpl><compare> [3360] Original value: 2017-09-11 13:45:00.25
<EntityImpl><compare> [3361] Target value: 2017-09-11 13:45:00.48

Após identificar os atributos, o próximo passo é decidir o que fazer com relação a eles. Algumas vezes basta habilitar o Refresh After para os atributos informados no log e tem-se o problema corrigido; contudo, o problema pode ainda persistir e seja necessário ir além. Neste caso, temos duas alternativas:

  1. Ensinar ao ADF BC como versionar determinada entidade. Para isso, basta indicar na entidade um ou mais atributos com Change Indicator; essa configuração informa ao ADF quais atributos devem ser utilizados na hora de comparar a entidade;
    adf-change-indicator-attribute
    Para habilitar esta opção num campo, basta abrir o editor de atributos da entidade e selecionar a propriedade “Change Indicator”;
  2. Remover o atributo que está causando o problema na comparação. Para isso, precisamos sobrescrever o método compare() da entidade e remover os atributos que não queremos que participem da comparação. O código ficaria semelhante a este:
    @Override
    protected boolean compare(SparseArray camposAComparar) {
        // ignora campos que não participarão do versionamento
        camposAComparar.clear(DATADEATUALIZACAO);
    
        // invoca lógica de comparação padrão
        return super.compare(camposAComparar);
    }
    

    É importante ficar ciente que, caso o algum campo da entidade esteja marcado como Change Indicator, somente estes campos participarão da comparação.

Perceba que a solução do Change Indicator se baseia em definir um atributo como “versionador” da entidade. Dessa forma, ao definir este atributo é importante que este seja um atributo atualizado de forma automática a cada alteração da entidade (via trigger por exemplo), isto é, este atributo não pode ser alterado pelo usuário do sistema, caso contrário cairíamos no mesmo problema.

Apesar de muitos livros informarem que é possível ter múltiplos atributos com Change Indicator, isso não acontece no jDeveloper v11.1.1.7.0, nessa versão somente um único atributo pode ser selecionado. Quando um atributo tem seu Change Indicator selecionado, o jDeveloper cuida de resetar a propriedade no outro atributo previamente marcado.

Na minha opinião, a melhor solução seria que toda tabela tivesse uma coluna VERSAO na qual denotasse a versão do registro. Para isso, o ADF nos permite definir uma History Column do tipo Version Number. Dessa forma, o próprio framework se encarrega de popular e incrementar o valor da coluna sempre que um registro é inserido ou atualizado.

Outra alternativa mais simples, seria utilizar a coluna de auditoria Modified On do ADF como versionador da entidade, já que o framework se preocupa de atualizá-la a cada alteração da entidadade.

Por fim, uma solução um tanto quanto drástica seria habilitar o recarregamento das View Objects a cada commit. O problema, é que todas as View Objects do Application Module seriam recarregadas, o que poderia levar a sérios problemas de performance caso o AM possua muitas View Objects. Na minha opinião, esta seria a alternativa menos recomendada.

Concluindo

Apesar de uma coluna do tipo History Column ser a solução mais eficiente para detectar o fenômeno Lost Update, ela com certeza não é a mais simples de aplicar em sistemas legados ou que façam integração entre sistemas via banco de dados. Por esse motivo, o ADF abre espaço para abordagens mais simples que beneficiam a produtividade do desenvolvedor e do arquiteto e que lidam com sistemas corporativos de todos os tipos e tamanhos utilizados no mercado.

Ao trabalhar com aplicações web usar Optimistic Locking é praticamente mandatório, pois assim evitamos conflitos ao atualizar registros no banco de dados e ainda garantimos uma maior escalabilidade e performance do sistema. Não é por acaso, que o lock otimista se tornou padrão no ADF desde sua versão 11.1.2.x.

Referências:

Para preparar este artigo e entender o problema de fato, eu utilizei os seguintes artigos de blogs e documentação da própria Oracle:

  1. https://blogs.oracle.com/onesizedoesntfitall/the-case-of-the-phantom-adf-developer-and-other-yarns
  2. http://radio-weblogs.com/0118231/stories/2004/03/24/whyDoIGetOraclejborowinconsistentexception.html
  3. http://huysmansitt.blogspot.com.br/2013/05/adf-bc-jbo-25014-another-user-has.html
  4. http://www.software-architect.net/blog/article/date/2014/07/01/some-background-on-optimistic-locking-in-adf.html
  5. http://andrejusb.blogspot.com.br/2010/03/optimistic-and-pessimistic-locking-in.html
  6. http://www.avromroyfaderman.com/2008/05/bring-back-the-hobgoblin-dealing-with-rowinconsistentexception/
  7. http://www.jobinesh.com/2011/02/yet-another-reason-for-jbo-25014.html
  8. http://www.jobinesh.com/2010/03/what-you-may-need-to-know-about-nested.html
  9. http://adfdiary.blogspot.com.br/2012/09/resolving-rowinconsistentexception-jbo.html
  10. http://dba-adf.blogspot.com.br/2014/08/another-cause-of-error-jbo-25014.html
  11. https://mjabr.wordpress.com/2011/06/10/what-is-change-indicator-property/
  12. https://mjabr.wordpress.com/2011/06/10/differences-between-pessimistic-and-optimistic-locking/
  13. https://docs.oracle.com/cd/B14099_19/web.1012/b14022/oracle/jbo/server/EntityImpl.html#compare_oracle_jbo_server_SparseArray_
  14. http://andrejusb.blogspot.com.br/2016/03/adf-bc-version-number-and-change.html
  15. http://www.adftraining.com/blog/how-to-use-history-columns-in-oracle-adf-for-mini-auditing
  16. http://rogersuen.blogspot.com.br/2015/06/adf-timezone-for-history-columns.html
  17. http://dkleppinger.blogspot.com.br/2014/10/what-is-entity-cache.html
  18. http://adfpractice-fedor.blogspot.com.br/2016/04/application-modules-and-entity-cache.html
  19. http://adfpractice-fedor.blogspot.com.br/2014/11/populating-dirty-entities-while-vo.html
  20. http://www.oracle.com/technetwork/topics/o16frame-092747.html
  21. https://vladmihalcea.com/2014/09/14/a-beginners-guide-to-database-locking-and-the-lost-update-phenomena/
  22. https://vladmihalcea.com/2014/09/22/preventing-lost-updates-in-long-conversations/
  23. https://en.wikipedia.org/wiki/Concurrency_control#Why_is_concurrency_control_needed.3F
  24. https://vladmihalcea.com/2014/12/08/how-to-prevent-optimisticlockexception-using-hibernate-versionless-optimistic-locking/
computer_security_stock_image-100582931-orig

Segurança: não coloque o usuário logado no controller

É incrível como você aprende com a experiência. Saca só a jornada que tive para aprender a implementar segurança na web…

Quando comecei minha carreira como programador, lá por volta de 2005, e tive que implementar meu primeiro controle de acesso eu só sabia fazer isso de uma forma, e era jogando os dados do usuário logado diretamente na sessão:

public String logar(String login, String senha) {
    Usuario usuario = this.autentica(login, senha);
    if (usuario != null) {
        Session sessao = this.getSession();
        sessao.setAttribute("usuarioLogado", usuario); // coloca usuario na sessão
        return "/home.jsp";
    }
    return "/login.jsp";
}

Um dos problemas chatos dessa abordagem é que eu tinha que ficar testando se o usuário existia na sessão antes de fazer minhas lógicas de segurança – um saco! Era algo muito parecido com o código abaixo:

Usuario usuario = sessao.getAttribute("usuarioLogado");
if (usuario != null && usuario.getTipo() == ADMIN) {
    // logica de negocio...
}

Daí o tempo passou e depois aprendi outra forma mais esperta, que era deixando o usuário logado dentro do controller, no caso do JSF eu deixava dentro do managed bean:

@ManagedBean
@SessionScoped // managed bean na sessão
public class LoginBean {
    // outros atributos
    private Usuario usuarioLogado;

    public String logar() {
        // lógica para autenticar usuário e setar atributo usuarioLogado
    }
}

Resolveu? Ahhh… não!

Essa abordagem também trazia problemas sutis que eu levei alguns anos pra enxergar, apesar de sutis eles foram bem sérios e sobrecarregaram a memoria do servidor. Por esse motivo, para você não cometer os erros que eu cometi, eu bloguei sobre como representar o usuário logado no meu sistema mas desta vez tirando proveito da OO de verdade:

[Post] OO na prática: representando usuário logado no sistema

Após ler o post sobre essa prática você vai se perguntar porque diabos você não fez isso antes, afinal de contas, basta usar OO para modelar conceitos em objetos, que é algo que fazemos dia a dia para nossas entidades mas que ignoramos quando falamos de segurança.

E aí, como você tem feito pra guardar os dados do usuário logado?

2o Mau Habito Dos Desenvolvedores JSF

Método getter invocado múltiplas vezes?

Você sabia que uma simples consulta ao banco de dados colocada no método errado do seu managed bean pode tornar suas páginas 10x mais lentas?

Entre 2008 e 2014 eu palestrei em diversos lugares do Brasil sobre os 10 maus hábitos dos desenvolvedores JSF, e sem dúvida um dos problemas mais comuns que encontrei durante estas palestras conversando com profissionais, em consultorias e treinamentos foi o 2o mau hábito: colocar lógica cara em métodos getters.

Provavelmente você já caiu neste 2o mau hábito ou conhece alguém que tenha caído, de qualquer forma, podemos simplificá-lo com um simples trecho de código. Por exemplo, um problema grave pode estar num simples método getter:

public List<Produto> getProdutos() {
    return this.dao.lista(); // lista produtos do banco
}

Apesar deste método parecer inofensivo ele pode tornar sua página até 10x mais lenta para abrir no navegador! Caso duvide ou queira entender por que ele pode ser tão perigoso para sua aplicação, eu recomendo a leitura do meu novo post:

>> JSF: Não coloque processamento caro em métodos getters

Colocar consultas dentro de getters é tão perigoso que, na maioria dos casos, além de impactar diretamente na aplicação Java ele impacta no banco de dados pois este é bombardeado com inúmeras consultas.

E aí, o que achou da dica?

Deixe seu comentário, e se tiver algum amigo que curte meter consultas em getters por comodidade, eis a chance de convence-lo do contrário.