JPA / Hibernate: entidad separada pasada para persistir

Tengo un modelo de objetos persistido por JPA que contiene una relación muchos a uno: una cuenta tiene muchas transacciones. Una transacción tiene una cuenta.

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 transactions; 

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

 Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.paulsanwald.Account at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:141) 

Entonces, puedo persistir una Cuenta que contiene transacciones, pero no una Transacción que tiene una Cuenta. Pensé que esto se debía a que la Cuenta podría no estar adjunta, 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 una transacción asociada a un objeto de cuenta ya existente?

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

De acuerdo con los artículos en los 2 enlaces anteriores, debe arreglar sus setters en ambos lados de la relación bidireccional. Un setter de ejemplo para One side está en este enlace.

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

Después de corregir a los instaladores, quiere declarar que el tipo de acceso a la Entidad es “Propiedad”. La mejor práctica para declarar el tipo de acceso “Propiedad” es mover TODAS las anotaciones desde las propiedades del miembro a los getters 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.

La solución es simple, simplemente use CascadeType.MERGE lugar de CascadeType.PERSIST o CascadeType.ALL .

He tenido el mismo problema y CascadeType.MERGE ha funcionado.

Espero que estés ordenado

Probablemente en este caso haya obtenido su objeto de account utilizando la lógica de fusión, y persist se usa para persistir nuevos objetos y se quejará si la jerarquía tiene un objeto ya existente. Deberías usar saveOrUpdate en tales casos, en lugar de persist .

Usar la combinación es arriesgado y complicado, por lo que es una solución sucia en tu caso. Debe recordar al menos que cuando pasa un objeto de entidad para fusionarse, 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 la entidad en su poder, los cambios se ignoran silenciosamente y se descartan en commit.

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 persiste. En ese caso, se espera que el proveedor de persistencia abra una nueva transacción para cada operación JPA que realice e inmediatamente la comprometa y la cierre antes de que la llamada regrese. Si este es el caso, la fusión se ejecutará en una primera transacción y luego, una vez que el método de fusión retorna, la transacción se completa y se cierra, y la entidad devuelta se separa ahora. La persistencia debajo de ella abriría una segunda transacción e intentaría referirse a una entidad que está separada, dando una excepción. Siempre envuelva su código dentro de una transacción a menos que sepa muy bien lo que está haciendo.

Si nada es útil y aún obtiene esta excepción, revise sus métodos equals() y no incluya la colección de elementos secundarios en ella. Especialmente si tiene una estructura profunda de colecciones incrustadas (por ejemplo, A contiene Bs, B contiene Cs, etc.).

En el ejemplo de Account -> Transactions :

  public class Account { private Long id; private String accountName; private Set 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 las verificaciones equals() . Esto se debe a que hibernate implicará que no está tratando de actualizar un objeto antiguo, pero le pasará un objeto nuevo para que persista siempre que cambie el 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 equals y hashCode .

No pase id (pk) para persistir el método o pruebe el método save () en lugar de persist ().

En la definición de su entidad, no especifica el @JoinColumn para la Account unida a una 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 anotación @Table en tu clase. Je. 🙂

Tal vez es el error de OpenJPA, cuando se retrotrae restablece el campo @Version, pero el pcVersionInit se mantiene verdadero. Tengo un AbstraceEntity que declaró el campo @Version. Puedo solucionarlo reiniciando 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; } 

Incluso si sus anotaciones están declaradas correctamente para administrar adecuadamente la relación de uno a muchos, es posible que todavía encuentre esta excepción precisa. Al agregar un nuevo objeto secundario, Transaction , a un modelo de datos adjunto, tendrá que administrar el valor de la clave principal, a menos que se suponga que no . Si proporciona un valor de clave primaria para una entidad hija declarada de la siguiente manera antes de llamar a persist(T) , encontrará esta excepción.

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

En este caso, las anotaciones están declarando que la base de datos gestionará la generación de los valores de clave primaria de la entidad tras la inserción. Proporcionar uno usted mismo (por ejemplo, a través del setter del Id) causa esta excepción.

Alternativamente, pero efectivamente igual, esta statement 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 valor de id . En el código de su aplicación cuando ya esté siendo administrado.

Necesita establecer Transacción para cada Cuenta.

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

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

 // list of existing accounts List accounts = new ArrayList<>(transactionObj.getAccounts()); foreach(Account account : accounts){ account.setId(null); } transactionObj.setAccounts(accounts); // just persist transactionObj using EntityManager merge() method. 
 cascadeType.MERGE,fetch= FetchType.LAZY