Na última semana descobri um problema (pra mim é um bug) no Hibernate3 que me deixou muito confuso por algumas horas.
Eu tenho uma entidade do Hibernate chamada Pessoa, que é usada em vários outros sistemas (afinal, todos os sistemas possuem alguma associação com pessoas).
Tudo estava funcionando perfeitamente, até que surgiu a idéia de implementar versioning for optimistic locking nesta classe (se você não conhece como funciona o versionamento do Hibernate, clique aqui). Foi muito simples, apenas adicionei @Version em um atributo do tipo Date e as instâncias ficaram protegidas de alterações concorrentes.
O problema é que, depois dessa alteração, todos os sistemas começaram apresentar problemas em telas variadas. A solução (depois de quebrar muito a cabeça) foi não usar versionamento para esta entidade. Eu sei que na verdade não foi uma solução, mas resolveu o problema. E nessas horas o difícil é imaginar que um simples @Version gerou tudo isso.
Para tentar exemplificar o problema, criei duas entidades chamadas Customer e Invoice, como seguem:
@Entity @Table public class Customer implements Serializable {@Id @GeneratedValue private Long id; private String name;@Version @Column(name="update_date") private Date updateDate;//getters and setters}
@Entity @Table public class Invoice implements Serializable {@Id @GeneratedValue private Long id; private Integer number; private BigDecimal amount;//getters and setters}
Veja que o atributo updateDate da classe Customer está anotada com @Version.
Criei um método com o seguinte código para testar o comportamento do Hibernate com @Version:
Session session = sessionFactory.getCurrentSession(); Transaction transaction = session.beginTransaction();Customer customer = new Customer(); customer.setId(1L);Invoice invoice = new Invoice(); invoice.setNumber(1234); invoice.setAmount(new BigDecimal(4500.19)); invoice.setCustomer(customer);session.merge(invoice); transaction.commit();
Considere o customer de código 1 já cadastrado no banco de dados.
Agora tente adivinhar o resultado da execução desse código. Pra mim deveria funcionar, pois o merge() deveria tornar o meu objeto customer persistente, mas infelizmente é lançada a seguinte exceção:
org.hibernate.TransientObjectException: object references an unsaved transient instance – save the transient instance before flushing: example.Customer
O engraçado é que se incluirmos a seguinte linha depois de atribuir o código 1 ao id de customer, funciona:
customer.setUpdateDate(new Date(1, 1, 1, 1, 1, 1));
Veja que eu defini uma data de alteração (que nem faz sentido) ao objeto, e agora funciona perfeitamente, gerando o seguinte SQL:
Hibernate: select customer0_.id as id0_0_, customer0_.name as name0_0_, customer0_.update_date as update3_0_0_ from Customer customer0_ where customer0_.id=?
Hibernate: insert into Invoice (amount, customer_id, number) values (?, ?, ?)
Se comentarmos o @Version do atributo updateDate da classe Customer e também a atribuição da data de alteração de Customer, funcionará corretamente também, pois aí não estaria mais utilizando versionamento.
Se quiser fazer outros testes, baixe do código-fonte do projeto de exemplo clicando aqui.
E pra você, isso é um bug? Será que a especificação do JPA contempla isso?
Assim que tiver tempo, irei traduzir esse post e cadastrar no JIRA do Hibernate.
Abraços,
Tentou recarregar o Customer do banco ao invés de reinstanciá-lo?
Outra possível solução seria mapear ‘customer’ com cascade=ALL ou MERGE.
Talvez isto não seja um bug, mas sim o contrário, o Hibernate que estava deixando código incorreto ser executado, antes de você colocar o @Version. (talvez)
Para mim é um bug.
Vale uma busca (no fórum também) antes de cadastrar a issue.
valeuz…
Tetsuo,
Se mapear com CascadeType.ALL ou MERGE, ele tenta inserir o customer (mesmo com o id diferente de null).
Se recarregar o customer “manualmente” funciona, mas não queria fazer isso, pois no meu caso, tenho muito código que não recarrega as instâncias (e espera que o merge faça isso).
Sei não… Acho que não é bug. Minha suspeita é de que como o [i]versioning[/i] é feito com o atributo [i]updateDate[/i], na hora do merge espera-se que este campo esteja preenchido, para haver o optimistic locking, comparando esta coluna com o registro existente, caso exista.
De fato, o versioning geralmente eh feito utilizando-se uma coluna de numero inteiro, que o valor default eh sempre 1. Ou seja, nunca está nulo.
[]’s
miojo
Thiago,
Também acho que é um bug. Porém somente quando o atributo mapeado é um Date. Se olhar na API verá que para utilizar @Version as propriedades suportadas são: int (Integer), short (Short), long (Long) e Timestamp (que é um Date).
Se você fizer o teste com Integer, por exemplo, verá que vai funcionar perfeitamente.
Também acho interessante você postar este bug no JIRA do Hibernate, mas destacando que o problema existe com atributos mapeados como Timestamp.
Opa!
Nunca vi algo assim acontecer, até porque eu sempre uso a anotação @Version em campos Integer. Apesar da anotação @Version funcionar também em Timestamps, o próprio manual do Hibernate recomenda a utilização de um tipo numérico, tal como o Integer.
Mas, de qualquer forma, também acredito que o que foi relatado no seu post representa sim um bug!
Até mais.
Pode ser por causa disso:
Link direto do site Hibernate
Em 5.1.7. version (optional)
A version or timestamp property should never be null for a detached instance, so Hibernate will detact any instance with a null version or timestamp as transient, no matter what other unsaved-value strategies are specified. Declaring a nullable version or timestamp property is an easy way to avoid any problems with transitive reattachment in Hibernate, especially useful for people using assigned identifiers or composite keys!
Bem, eu estava fazendo uns testes, e consegui reproduzir o erro. Depois de ver o comentário do Helder, tentei simplesmente declarar o atributo updateDate já atribuindo new Date() e funcionou. Tipo
@Version private Date updateDate = new Date();
Colocar @Basic(optional=true) – como eu entendi que a documentação sugere – não funcionou.
Eu continuo achando que o correto é carregar o objeto antes de usar, ao invés de instanciar um novo, mas se funciona…
Ronald,
E com @Column(nullable=true)? (embora eu pense que ambas as formas devem significar a mesma coisa).
Não tenho como testar aqui, senão eu já verificava se isso funcionaria!
Thiago,
Não cheguei a ler o post inteiro e testar, mas se chegar a conclusão que realmente é um bug, por favor, não esqueça de abrir a issue no JIRA. E por favor, procure antes se já não existe uma cadastrada com o mesmo problema, anexando um teste executável(java+mapping), pra facilitar nosso trabalho
Att.