PersistentObjectException: entidad separada pasada para persistir lanzada por JPA e Hibernate

237

Tengo un modelo de objetos con JPA que contiene una relación de muchos a uno: un Accounttiene muchos Transactions. A Transactiontiene uno Account.

Aquí hay un fragmento del código:

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    private Account fromAccount;
....

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @OneToMany(cascade = {CascadeType.ALL},fetch= FetchType.EAGER, mappedBy = "fromAccount")
    private Set<Transaction> transactions;

Puedo crear un Accountobjeto, agregarle transacciones y mantener el Accountobjeto correctamente. Pero, cuando creo una transacción, usando una cuenta ya existente y persistiendo la transacción , obtengo una excepción:

Causado por: org.hibernate.PersistentObjectException: entidad separada pasada a persistir: com.paulsanwald.Account at org.hibernate.event.internal.DefaultPersistEventListener.onPersist (DefaultPersistEventListener.java:141)

Entonces, puedo persistir un Accountque contiene transacciones, pero no una Transacción que tiene un Account. Pensé que esto se debía a que Accountpodría no estar adjunto, pero este código todavía me da la misma excepción:

if (account.getId()!=null) {
    account = entityManager.merge(account);
}
Transaction transaction = new Transaction(account,"other stuff");
 // the below fails with a "detached entity" message. why?
entityManager.persist(transaction);

¿Cómo puedo guardar correctamente un Transactionasociado con un Accountobjeto ya persistente ?

Paul Sanwald
fuente
15
En mi caso, estaba configurando la identificación de una entidad que intentaba persistir usando Entity Manager. Cuando, eliminé el setter para id, comenzó a funcionar bien.
Rushi Shah
En mi caso, no estaba configurando la identificación, pero había dos usuarios que usaban la misma cuenta, uno de ellos persistió en una entidad (correctamente) y el error ocurrió cuando el segundo intentó persistir en la misma entidad, eso ya era persistió
sergioFC

Respuestas:

129

Este es un problema típico de consistencia bidireccional. Está bien discutido en este enlace , así como en este enlace.

De acuerdo con los artículos en los 2 enlaces anteriores, debe corregir sus establecedores en ambos lados de la relación bidireccional. Un setter de ejemplo para el Un lado está en este enlace.

Un ejemplo de establecimiento para el lado Muchos está en este enlace.

Después de corregir sus establecedores, desea declarar que el tipo de acceso de entidad es "Propiedad". La mejor práctica para declarar el tipo de acceso "Propiedad" es mover TODAS las anotaciones de las propiedades del miembro a los captadores correspondientes. Una gran advertencia es no mezclar los tipos de acceso "Campo" y "Propiedad" dentro de la clase de entidad; de lo contrario, el comportamiento no está definido por las especificaciones JSR-317.

Sym-Sym
fuente
2
ps: la anotación @Id es la que usa hibernate para identificar el tipo de acceso.
Diego Plentz
2
La excepción es: detached entity passed to persist¿Por qué mejorar la consistencia hace que funcione? Ok, se reparó la consistencia pero el objeto todavía está separado.
Gilgamesz
1
@ Sam, muchas gracias por tu explicación. Pero aún no lo entiendo. Veo que sin un setter 'especial' la relación bidireccional no está satisfecha. Pero, no veo por qué se separó el objeto.
Gilgamesz
28
No publique una respuesta que responda únicamente con enlaces.
El Mac
66
¿No puede ver cómo esta respuesta está relacionada con la pregunta?
Eugen Labun
271

La solución es simple, solo use el en CascadeType.MERGElugar de CascadeType.PERSISTo CascadeType.ALL.

He tenido el mismo problema y CascadeType.MERGEha funcionado para mí.

Espero que estés ordenado

ochiWlad
fuente
55
Sorprendentemente, ese también funcionó para mí. No tiene sentido ya que CascadeType.ALL incluye todos los demás tipos de cascada ... ¿WTF? Tengo spring 4.0.4, spring data jpa 1.8.0 e hibernate 4.X .. ¿Alguien tiene alguna idea de por qué ALL no funciona, pero MERGE sí?
Vadim Kirilchuk
22
@VadimKirilchuk Esto también funcionó para mí y tiene mucho sentido. Como la transacción es PERSISTIDA, intenta PERSISTAR la cuenta también y eso no funciona ya que la cuenta ya está en la base de datos. Pero con CascadeType.MERGE, la cuenta se fusiona automáticamente.
Pistolero
44
Esto puede suceder si no utiliza transacciones.
lanoxx
1
otra solución: trate de no insertar objetos ya persistentes :)
Andrii Plotnikov
3
Gracias hombre. No es posible evitar la inserción de objetos persistentes, si tiene restricciones para que la clave de referencia NO sea NULL. Entonces esta es la única solución. Gracias de nuevo.
makkasi
22

Elimine la cascada de la entidad secundariaTransaction , debería ser solo:

@Entity class Transaction {
    @ManyToOne // no cascading here!
    private Account account;
}

( FetchType.EAGERse puede quitar y es el valor predeterminado para @ManyToOne)

¡Eso es todo!

¿Por qué? Al decir "en cascada TODOS" en la entidad secundaria Transaction, necesita que cada operación de DB se propague a la entidad principal Account. Si lo haces persist(transaction), persist(account)se invocará también.

Pero solo las entidades transitorias (nuevas) pueden pasarse a persist( Transactionen este caso). Los desconectados (u otros estados no transitorios) pueden no hacerlo ( Accounten este caso, ya que está en DB).

Por lo tanto, obtiene la excepción "entidad separada pasada para persistir" . ¡La Accountentidad está destinada! No el Transactionque llamas persist.


Por lo general, no desea propagarse de hijo a padre. Desafortunadamente, hay muchos ejemplos de código en los libros (incluso en los buenos) y a través de la red, que hacen exactamente eso. No sé por qué ... Quizás a veces simplemente se copia una y otra vez sin pensarlo mucho ...

¿Adivina qué sucede si llamas remove(transaction)aún teniendo "cascada TODOS" en ese @ManyToOne? El account(por cierto, con todas las demás transacciones!) También se eliminará de la base de datos. Pero esa no era tu intención, ¿verdad?

Eugen Labun
fuente
Solo quiero agregar, si su intención es realmente salvar al niño junto con el padre y también eliminar al padre junto con el niño, como persona (padre) y dirección (niño) junto con addressId autogenerado por DB y luego antes de llamar a guardar en Persona, simplemente haga una llamada para ahorrar en la dirección en su método de transacción. De esta forma, se guardaría junto con la ID también generada por DB. No hay impacto en el rendimiento ya que Hibenate todavía realiza 2 consultas, solo estamos cambiando el orden de las consultas.
Vikky
Si no podemos pasar nada, entonces qué valor predeterminado se necesita para todos los casos.
Dhwanil Patel
15

Usar la fusión es arriesgado y complicado, por lo que es una solución sucia en su caso. Debe recordar al menos que cuando pasa un objeto de entidad para fusionar, deja de estar adjunto a la transacción y en su lugar se devuelve una nueva entidad ahora adjunta. Esto significa que si alguien tiene el antiguo objeto de entidad todavía en su posesión, los cambios en él se ignoran silenciosamente y se descartan en la confirmación.

No está mostrando el código completo aquí, por lo que no puedo verificar su patrón de transacción. Una forma de llegar a una situación como esta es si no tiene una transacción activa al ejecutar la fusión y persistir. En ese caso, se espera que el proveedor de persistencia abra una nueva transacción para cada operación JPA que realice e inmediatamente confirme y cierre antes de que vuelva la llamada. Si este es el caso, la fusión se ejecutará en una primera transacción y luego, una vez que regrese el método de fusión, la transacción se completa y se cierra y la entidad devuelta ahora se separa. La persistencia a continuación abriría una segunda transacción y trataría de referirse a una entidad separada, dando una excepción. Envuelva siempre su código dentro de una transacción a menos que sepa muy bien lo que está haciendo.

El uso de transacciones gestionadas por contenedor se vería así. Tenga en cuenta: esto supone que el método está dentro de un bean de sesión y se llama a través de la interfaz local o remota.

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void storeAccount(Account account) {
    ...

    if (account.getId()!=null) {
        account = entityManager.merge(account);
    }

    Transaction transaction = new Transaction(account,"other stuff");

    entityManager.persist(account);
}
Zds
fuente
Estoy enfrentando el mismo problema. He usado el @Transaction(readonly=false)servicio en la capa de servicio. Todavía tengo el mismo problema.
Senthil Arumugam SP
No puedo decir que entiendo completamente por qué las cosas funcionan de esta manera, pero colocar el método de persistencia y ver el mapeo de entidades juntos dentro de una anotación transaccional solucionó mi problema, así que gracias.
Deadron
Esta es en realidad una mejor solución que la más votada.
Aleksei Maide
13

Probablemente en este caso haya obtenido su accountobjeto utilizando la lógica de fusión, y persistse usa para persistir objetos nuevos y se quejará si la jerarquía tiene un objeto ya persistente. Debe usar saveOrUpdateen tales casos, en lugar de persist.

dan
fuente
3
es JPA, así que creo que el método análogo es .merge (), pero eso me da la misma excepción. Para ser claros, la transacción es un objeto nuevo, la cuenta no lo es.
Paul Sanwald el
@PaulSanwald ¿Usando mergeel transactionobjeto obtienes el mismo error?
dan
En realidad, no, hablé mal. si combino (transacción), entonces la transacción no persiste en absoluto.
Paul Sanwald el
@PaulSanwald Hmm, ¿estás seguro de que transactionno persistió? ¿Cómo lo comprobaste? Tenga en cuenta que mergeestá devolviendo una referencia al objeto persistente.
dan
el objeto devuelto por .merge () tiene una identificación nula. Además, estoy haciendo un .findAll () después, y mi objeto no está allí.
Paul Sanwald el
12

No pase la identificación (pk) al método persistente ni intente guardar () en lugar de persistir ().

Angad Bansode
fuente
2
¡Buen consejo! Pero solo si se genera id. En caso de que se asigne, es normal establecer la identificación.
Aldian
Esto funcionó para mí. Además, puede usar TestEntityManager.persistAndFlush()para hacer que una instancia sea administrada y persistente y luego sincronizar el contexto de persistencia con la base de datos subyacente. Devuelve la entidad fuente original
Anyul Rivas
7

Como esta es una pregunta muy común, escribí este artículo , en el que se basa esta respuesta.

Para solucionar el problema, debe seguir estos pasos:

1. Eliminar la asociación de niños en cascada

Por lo tanto, debe eliminar el @CascadeType.ALLde la @ManyToOneasociación. Las entidades secundarias no deben conectarse en cascada a las asociaciones principales. Solo las entidades principales deberían conectarse en cascada a las entidades secundarias.

@ManyToOne(fetch= FetchType.LAZY)

Tenga en cuenta que configuré el fetchatributo FetchType.LAZYporque la búsqueda ansiosa es muy mala para el rendimiento .

2. Establecer ambos lados de la asociación

Siempre que tenga una asociación bidireccional, debe sincronizar ambos lados utilizando los métodos addChildy removeChilden la entidad principal:

public void addTransaction(Transaction transaction) {
    transcations.add(transaction);
    transaction.setAccount(this);
}

public void removeTransaction(Transaction transaction) {
    transcations.remove(transaction);
    transaction.setAccount(null);
}

Para obtener más detalles sobre por qué es importante sincronizar ambos extremos de una asociación bidireccional, consulte este artículo .

Vlad Mihalcea
fuente
4

En su definición de entidad, no está especificando @JoinColumn para los Accountunidos a Transaction. Querrás algo como esto:

@Entity
public class Transaction {
    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    @JoinColumn(name = "accountId", referencedColumnName = "id")
    private Account fromAccount;
}

EDITAR: Bueno, supongo que sería útil si estuvieras usando la @Tableanotación en tu clase. Je :)

NemesisX00
fuente
2
sí, no creo que sea así, de todos modos, agregué @JoinColumn (name = "fromAccount_id", referencedColumnName = "id") y no funcionó :).
Paul Sanwald el
Sí, generalmente no uso un archivo xml de mapeo para mapear entidades a tablas, por lo que asumo que está basado en anotaciones. Pero si tuviera que adivinar, está utilizando un hibernate.xml para asignar entidades a tablas, ¿verdad?
NemesisX00
no, estoy usando datos de primavera JPA, así que todo está basado en anotaciones. Tengo una anotación "mappedBy" en el otro lado: @OneToMany (cascade = {CascadeType.ALL}, fetch = FetchType.EAGER, mappedBy = "fromAccount")
Paul Sanwald
4

Incluso si sus anotaciones se declaran correctamente para administrar adecuadamente la relación uno a muchos, aún puede encontrar esta excepción precisa. Al agregar un nuevo objeto secundario Transaction, a un modelo de datos adjunto, deberá administrar el valor de la clave primaria, a menos que se suponga que no lo haga . Si proporciona un valor de clave principal para una entidad secundaria declarada de la siguiente manera antes de llamar persist(T), encontrará esta excepción.

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
....

En este caso, las anotaciones declaran que la base de datos administrará la generación de los valores de clave principal de la entidad tras la inserción. Proporcionar uno usted mismo (como a través del configurador de Id) provoca esta excepción.

Alternativamente, pero efectivamente igual, esta declaración de anotación da como resultado la misma excepción:

@Entity
public class Transaction {
    @Id
    @org.hibernate.annotations.GenericGenerator(name="system-uuid", strategy="uuid")
    @GeneratedValue(generator="system-uuid")
    private Long id;
....

Por lo tanto, no establezca el idvalor en el código de su aplicación cuando ya se esté administrando.

dan
fuente
4

Si nada ayuda y sigue recibiendo esta excepción, revise su equals() métodos, y no incluya la colección secundaria. Especialmente si tiene una estructura profunda de colecciones incrustadas (por ejemplo, A contiene Bs, B contiene Cs, etc.).

En ejemplo de Account -> Transactions:

  public class Account {

    private Long id;
    private String accountName;
    private Set<Transaction> transactions;

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (!(obj instanceof Account))
        return false;
      Account other = (Account) obj;
      return Objects.equals(this.id, other.id)
          && Objects.equals(this.accountName, other.accountName)
          && Objects.equals(this.transactions, other.transactions); // <--- REMOVE THIS!
    }
  }

En el ejemplo anterior, elimine las transacciones de los equals()cheques. Esto se debe a que hibernar implicará que no está intentando actualizar un objeto antiguo, sino que pasa un objeto nuevo para que persista, siempre que cambie un elemento en la colección secundaria.
Por supuesto, estas soluciones no se adaptarán a todas las aplicaciones y debe diseñar cuidadosamente lo que desea incluir en los métodos equalsy hashCode.

FazoM
fuente
1

Tal vez sea el error de OpenJPA, cuando revierte restablece el campo @Version, pero pcVersionInit se mantiene verdadero. Tengo una AbstraceEntity que declaró el campo @Version. Puedo solucionarlo restableciendo el campo pcVersionInit. Pero no es una buena idea. Creo que no funciona cuando la entidad persiste en cascada.

    private static Field PC_VERSION_INIT = null;
    static {
        try {
            PC_VERSION_INIT = AbstractEntity.class.getDeclaredField("pcVersionInit");
            PC_VERSION_INIT.setAccessible(true);
        } catch (NoSuchFieldException | SecurityException e) {
        }
    }

    public T call(final EntityManager em) {
                if (PC_VERSION_INIT != null && isDetached(entity)) {
                    try {
                        PC_VERSION_INIT.set(entity, false);
                    } catch (IllegalArgumentException | IllegalAccessException e) {
                    }
                }
                em.persist(entity);
                return entity;
            }

            /**
             * @param entity
             * @param detached
             * @return
             */
            private boolean isDetached(final Object entity) {
                if (entity instanceof PersistenceCapable) {
                    PersistenceCapable pc = (PersistenceCapable) entity;
                    if (pc.pcIsDetached() == Boolean.TRUE) {
                        return true;
                    }
                }
                return false;
            }
Yaocl
fuente
1

Debe configurar la transacción para cada cuenta.

foreach(Account account : accounts){
    account.setTransaction(transactionObj);
}

O puede ser suficiente (si corresponde) para establecer los identificadores en nulos en muchos lados.

// list of existing accounts
List<Account> accounts = new ArrayList<>(transactionObj.getAccounts());

foreach(Account account : accounts){
    account.setId(null);
}

transactionObj.setAccounts(accounts);

// just persist transactionObj using EntityManager merge() method.
stakahop
fuente
1
cascadeType.MERGE,fetch= FetchType.LAZY
James Siva
fuente
1
Hola James y bienvenido, debes intentar evitar las respuestas de solo código. Indique cómo resuelve esto el problema planteado en la pregunta (y cuándo es aplicable o no, nivel de API, etc.).
Maarten Bodewes
Revisores de VLQ: ver meta.stackoverflow.com/questions/260411/… . las respuestas de solo código no merecen ser eliminadas, la acción apropiada es seleccionar "Se ve bien".
Nathan Hughes el
0

En mi caso, estaba cometiendo una transacción cuando se usaba el método persistir . Al cambiar el método de persistir para guardar , se resolvió.

H.Ostwal
fuente
0

Si las soluciones anteriores no funcionan solo una vez, comente los métodos getter y setter de la clase de entidad y no establezca el valor de id. (Clave primaria) Entonces esto funcionará.

Shubham
fuente
0

@OneToMany (mappedBy = "xxxx", cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE}) funcionó para mí.

SateeshKasaboina
fuente
0

Se resuelve guardando el objeto dependiente antes del siguiente.

Esto me sucedió porque no estaba configurando Id (que no se generó automáticamente). y tratando de ahorrar con relación @ManytoOne

Shahid Hussain Abbasi
fuente