JPA em cascata persistem e referências a entidades desanexadas lançam PersistentObjectException. Por quê?

Eu tenho uma entidade Foo que faz referência a uma barra de entidade:

@Entity public class Foo { @OneToOne(cascade = {PERSIST, MERGE, REFRESH}, fetch = EAGER) public Bar getBar() { return bar; } } 

Quando eu persisto um novo Foo, ele pode obter uma referência a uma nova barra ou a uma barra existente. Quando ele recebe uma barra existente, que por acaso é desanexada, meu provedor de JPA (Hibernate) lança a seguinte exceção:

 Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.example.Bar at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:102) at org.hibernate.impl.SessionImpl.firePersist(SessionImpl.java:636) at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:628) at org.hibernate.engine.EJB3CascadingAction$1.cascade(EJB3CascadingAction.java:28) at org.hibernate.engine.Cascade.cascadeToOne(Cascade.java:291) at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:239) at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:192) at org.hibernate.engine.Cascade.cascade(Cascade.java:153) at org.hibernate.event.def.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:454) at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:288) at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:204) at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:130) at org.hibernate.ejb.event.EJB3PersistEventListener.saveWithGeneratedId(EJB3PersistEventListener.java:49) at org.hibernate.event.def.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:154) at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:110) at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:61) at org.hibernate.impl.SessionImpl.firePersist(SessionImpl.java:645) at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:619) at org.hibernate.impl.SessionImpl.persist(SessionImpl.java:623) at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:220) ... 112 more 

Quando eu me certifico de que a referência a Bar é gerenciada (anexada) ou quando omito o PERSIST em cascata na relação, tudo funciona bem.

Nenhuma solução, no entanto, é 100% satisfatória. Se eu remover a cascata persistir, obviamente não posso mais persistir em Foo com uma referência a uma nova barra. Tornar a referência à Barra gerenciada requer um código assim antes de persistir:

 if (foo.getBar().getID() != null && !entityManager.contains(foo.getBar())) { foo.setBar(entityManager.merge(foo.getUBar())); } entityManager.persist(foo); 

Para um único Bar, isso pode não parecer grande coisa, mas se eu tiver que levar em conta todas as propriedades, acabarei com um código horrível que parece derrotar a razão de usar o ORM em primeiro lugar. Eu poderia muito bem persistir meu gráfico de object manualmente usando o JDBC novamente.

Quando é fornecida uma referência de Bar existente, a única coisa que o JPA precisa fazer é pegar seu ID e inseri-lo em uma coluna da tabela que contém o Foo. Ele faz exatamente isso quando Bar está conectado, mas lança a exceção quando Bar é desanexado.

Minha pergunta é; Por que precisa de Bar para ser anexado? Certamente, sua ID não será alterada quando a instância da barra passar de desanexada para o estado anexado, e essa ID parece ser a única coisa necessária aqui.

Isso é talvez um bug no Hibernate ou estou faltando alguma coisa?

Você pode usar merge() vez de persist() neste caso:

 foo = entityManager.merge(foo); 

Quando aplicada à nova instância, merge() torna persistente (na verdade – retorna a instância persistente com o mesmo estado) e mescla as referências em cascata, conforme você tenta fazer manualmente.

Se bem entendi, você só precisa da referência Bar para permitir que o novo Foo tenha o valor da chave estrangeira (para a Bar existente) quando persistir. Existe um método JPA no EntityManager chamado getReference() que pode ser útil para você neste caso. O método getReference() é semelhante a find() exceto pelo fato de não se incomodar em retornar uma instância gerenciada (de Bar ), a menos que já esteja em cache no contexto de persistência. Ele retornará um object proxy que satisfará suas necessidades de chave estrangeira para manter o object Foo . Eu não tenho certeza se este é o tipo de solução que você estava esperando, mas tente e veja se isso funciona para você.

Também notei pelo seu código que você está usando o access ao estilo “propriedade” em vez do access ao estilo “campo” anotando seu método getter (para o relacionamento Bar ). Alguma razão para isso? É recomendável anotar os membros em vez dos getters por motivos de desempenho. É suposto ser mais eficiente para o provedor JPA acessar o campo diretamente, em vez de usar getters e setters.

EDITAR:

Como alguém mencionou, o uso de uma mesclagem em cascata () persistirá em novas entidades, bem como mesclará entidades modificadas e reconectará os desvios separados que possuem um relacionamento com a opção em cascata MERGE. Usar a opção de cascata PERSIST não irá reconectar nada ou mesclar nada e deve ser usado quando esse for o comportamento desejado.