Rollback (indevido) em transação JPA

Caros,

Estou com um problema que achei, inicialmente que podia ser do OC4J ou Toplink (Essentials), rodando com JPA, mas vi que com o Hibernate ocorre o mesmo.

Estou lendo um arquivo onde cada linha tem os dados de um novo registro a grava no BD. Devo processar esse arquivo e gravar linha a linha os dados no BD. Faço isso com JPA (oc4j + toplink). Se algum registro dr algum pau, eu pulo para o próximo e sigo até o final, confirmando a transação, ou seja, grava os OK e os com erro não gravam.

Acontece que em todos os cenários (dentro ou fora oc4j, com toplink ou hibernate), ele faz rollback da transação toda depois que persisto um registro com erro (ex: chave duplicada ou valor longo demais para o campo).

Não entendi porque no final ele não confirma a transação.

Exemplo do código:

[code]EntityManager em = getEntityManager();
// se rodo fora do oc4j, habilito a linha abaixo
//em.getTransaction().begin();

for( String linha: arquivo ) {
try {
MeuObj obj = new MeuObj();
// seta os valores da linha nos atributos do obj

em.persist(obj);
em.flush();

} catch(Exception e) {
e.printStackTrace();
}
} //for()

// se rodo fora do oc4j, habilito as linhas abaixo
//em.getTransaction().commit();
//em.close();[/code]

Erro com Hibernate: [quote]Exception in thread “main” javax.persistence.RollbackException: Transaction marked as rollbackOnly[/quote]
Erro com Toplink: [quote]Caused by: javax.persistence.RollbackException: Transaction rolled back because transaction was set to RollbackOnly[/quote]

Olá Daniel,

Qual o transaction type está configurado para este método (requiresNew, etc)?

Cada registro está na mesma transação?

Em alguns apps servers, quando ocorre uma RuntimeException ele sempre marca a transação como Rollback only

Não tem anotação nenhuma de tipo de transação e, mesmo rodando fora do app server, ele dá rollback.

O persistence.xml no app server o transaction-type=“JTA”. Fora do app server não tem este atributo, ou está marcado como “RESOURCE_LOCAL”.

Em ambos falham.

Daniel,

Você está usando EJB ou Spring em sua app?

Uso EJB 3.0 (dentro do oc4j) ou classe normal (fora). Nada de Spring!

Maracuja, nem testei, mas posso dizer que isso não funcionaria, ainda mais se eu levar isso pra dentro do app server, onde vou legar ao container o gerenciamento da transação.

humm

Entao talvez vc deva criar um metodo ‘save’ e colocar o transaction attribute dele para RequiresNew, e chamar esse método dentro do for.

Senao, ele sempre vai dar rollback (por causa da exception), pois todos os seus inserts estao em uma mesma transação

Tente algo como:

 for( String linha: arquivo ) {  
   try {  
     MeuObj obj = new MeuObj();  
     // seta os valores da linha nos atributos do obj  
   
    save(obj)
   
   } catch(Exception e) {  
     e.printStackTrace();  
   }  
 } //for()

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void save(MeuObj obj){
  EntityManager em = getEntityManager(); 
  em.persist(obj);    
  em.flush();  
}

Isso pode funcionar no EJB, mas fora dele ainda continuaria com erro.
E, para mim, não faz muito sentido ele dar rollback na transação toda, sendo que eu estou tratando um erro específico.

Imagina a seguinte transação:

INSERT
DELETE
UPDATE
INSERT

Imagina que o UPDATE não seja um passo que, obrigatoriamente, tenha que ser efetivado com sucesso, ou seja, mesmo que dê erro, as outras operações devem ser confirmadas na transação. Não tem cabimento ele dar rollback na minha transação sendo que eu trato um ponto específico.

E se abrir uma transação para cada registro?

[quote=danieldestro]E, para mim, não faz muito sentido ele dar rollback na transação toda, sendo que eu estou tratando um erro específico.
[/quote]

A questão toda são as exceptions. Se deu exception, ele da rollback.

http://java.sun.com/javaee/5/docs/tutorial/doc/bncij.html#bnciv

Pois é, mas ate onde sei esse é o comportamento padrão. Tudo ou nada.
Por isso existemos diferentes tipos de isolamento.
Vc vai precisar fazer de um dos jeitos sugeridos:
1 - criar um metodo para salvar e fazer ele ser REQUIRED_NEW.
2- criar uma transacao para cada registro. Pode ser dentro do metodo save mesmo.

Não vejo outra opção.

[]´s

ps. se vc estivesse usando Spring seria bem facil de fazer REQUIRED_NEW.

Pois é, de acordo com o link, todas os EJBs por padrão são marcados como “Required” na transação. E a transação é demarcada pelo início e fim do método do bean.

Então, se eu faço assim:

public void meuMetodo() { em.persist( x ); em.merge( y ); try { em.delete( w ); } catch( Exception e ) { } em.persist( z ); }

Se der pau no delete, eu to dando um catch e mando seguir o resto. Ele deveria dar commit se o resto foi OK.

Ou eu estou errado???

Então,

Eu acredito que ainda vai dar rollback, porque a exception está ocorrendo dentro da transação (que como você destacou, começa no inicio de seu método).

Teria que ter um jeito de isolar esse trecho

      try {  
        em.delete( w );  
      } catch( Exception e ) {  
      }  

em outro método.
Só que aí, talvez não atenda a seus requisitos de transação. Se der pau no ultimo persist "em.persist( z ); ", ele deve dar rollback em tudo, inclusive no “em.delete( w );”?

NESTE caso (o primeiro código) cada persist() é um registro gravado (transação atômica).
Mas no fictício caso acima (DELETE), tudo seria uma transação toda, onde apenas o DELETE seria opcional.

humm

É, no caso dos insert a unica forma que vejo é usando o REQUIRES_NEW. Acho que tambem funciona fora do EJB, sendo que no seu método save vc teria algo do genero

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)  
    public void save(MeuObj obj){  
      try{
           EntityManager em = getEntityManager();
           //em.getTransaction().begin(); //descomentado quando tiver fora do EJB   
           em.persist(obj);      
           em.flush();
           //em.getTransaction().commit();
      catch(Exception ex){
         //tratar
         //em.getTransaction().rollback();
      }    
    }  

o problema é aparentemente simples, mas da dor de cabeça neh? rs

Olha, um detalhe importante das sessions do Hibernate (que, vendo isso, eu suponho que seja verdade também pra os EntityManagers) é que se uma Session lançar uma exceção, ela deve ser descartada na mesma hora, ela não deve ser reutilizada pra nada.

Não lembro mais exatamente aonde eu li isso, mas eu suponho que tenha sido no bom e velho Hibernate in Action.

Mas não funciona quando o EM lança exceção no persist().

Eu uso CMT, como falei no começo.

Resolvi e parece que entendi o problema todo. Vou explicar o meu caso aqui para vocês entenderem melhor.

Tenho um EJB Stateless Session Bean (ESSB) que serve como meu repositório de dados e trabalha internamente com o EntityManager do JPA.
Tenho outro ESSB que tem as regras de negócio. Um dos processos é ler um arquivo e gravar cada linha como um novo registro no BD. Se uma linha der erro, devo registrar um log e continuar a gravar os outros registros até o fim. Não posso simplesmente dar rollback. O controle transacional é feito pelo container (CMT).

Então: EJB_A --> EJB_B --> EntityManager_JPA --> BD

Meu EJB_A estava mais ou menos assim:

while( (linha = lerLinha()) != null ) { MeuObj o = new MeuObj( dadosDoArquivoAqui ); try { ejbB.gravar( o ); } catch( Exception e ) { // log do registro com problemas } }

Meu EJB_B estava mais ou menos assim:

public void gravar(T t) { em.persist(t); em.flush(); }

Acontece que qualquer exceção lançada pelo EntityManager (ex: chave duplicada), invalidava toda minha transação, mesmo eu tratando. Eu considerava que a transação seria confirmada (commit) se o método do EJB originalmente chamado retornasse com sucesso. Mas a exceção do EM invalida tudo (rollback).

Ai tentei o seguinte no meu EJB_A:

[code]while( (linha = lerLinha()) != null ) {
MeuObj o = new MeuObj( dadosDoArquivoAqui );
try {
gravaComTransacaoNova( o );
} catch( Exception e ) {
// log do registro com problemas
}
}

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void gravaComTransacaoNova(MeuObj o) {
ejbB.gravar( o );
}[/code]

Mesmo assim não funcionou. Li no livro EJB in Action (versão pt_br) que o JPA não funciona com transação por método.

Então eu tentei isso aqui:

EJB_A

while( (linha = lerLinha()) != null ) { MeuObj o = new MeuObj( dadosDoArquivoAqui ); try { ejbB.gravar( o ); } catch( Exception e ) { // log do registro com problemas } }

EJB_B

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public void gravar(T t) { em.persist(t); em.flush(); }

Então funcionou, ou seja, ele cria uma nova transação para cada gravação e se der erro no EM, ele executa os próximo.

Conclusão, no mesmo EJB não rola definir uma nova transação no método. De EJB para outro EJB funciona.

Problema: Eu não deveria fazer isso, haja vista que o EJB_B é mais granular e reutilizado em outros serviços do sistema. Eu queria ser capaz de fazer isso no meu EJB_A (de negócio), que é mais específico.

Alguém tem algo a acrescentar?