query - Evitar que Hibernate elimine entidades huérfanas al fusionar una entidad que tenga asociaciones de entidades con orphanRemoval establecida en verdadero
jpa delete query (2)
En primer lugar, cambiemos su código original a una forma más simple:
StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId());
Country oldCountry = oldState.getCountry();
oldState.getCountry().hashCode(); // DELETE is issued, if removed.
Country newCountry = entityManager.find(Country.class, stateTable.getCountry().getCountryId());
stateTable.setCountry(newCountry);
if (ObjectUtils.notEquals(newCountry, oldCountry)) {
oldCountry.remove(oldState);
newCountry.add(stateTable);
}
entityManager.merge(stateTable);
Observe que solo agregué oldState.getCountry().hashCode()
en la tercera línea. Ahora puedes reproducir tu problema eliminando esta línea solamente.
Antes de explicar lo que está sucediendo aquí, primero algunos extractos de la especificación JPA 2.1 .
Sección 3.2.4 :
La semántica de la operación de descarga, aplicada a una entidad X es la siguiente:
- Si X es una entidad gestionada, se sincroniza con la base de datos.
- Para todas las entidades Y a las que hace referencia una relación de X, si la relación con Y se ha anotado con el valor del elemento en cascada en cascada = PERSIST o en cascada = TODO, la operación de persistencia se aplica a Y
Sección 3.2.2 :
La semántica de la operación de persistencia, aplicada a una entidad X es la siguiente:
- Si X es una entidad eliminada, se gestiona.
(Opcional) Si se aplica la operación de eliminación a las entidades que se han eliminado de la relación y se coloca en cascada la operación de eliminación a esas entidades.
Como podemos ver, orphanRemoval
se define en términos de la operación de remove
, por lo que todas las reglas que aplican para remove
deben aplicarse también para orphanRemoval
.
En segundo lugar, como se explica en esta respuesta , el orden de las actualizaciones ejecutadas por Hibernate es el orden en que se cargan las entidades en el contexto de persistencia. Para ser más precisos, actualizar una entidad significa sincronizar su estado actual (verificación sucia) con la base de datos y conectar en cascada la operación PERSIST
a sus asociaciones.
Ahora, esto es lo que está pasando en tu caso. Al final de la transacción, Hibernate sincroniza el contexto de persistencia con la base de datos. Tenemos dos escenarios:
Cuando la línea adicional (
hashCode
) está presente:- Hibernate sincroniza
oldCountry
con la base de datos. Lo hace antes de manejarnewCountry
, porqueoldCountry
se cargó primero (inicialización de proxy forzada llamando ahashCode
). - Hibernate ve que una instancia de
StateTable
se ha eliminado de la colecciónoldCountry
, marcando así la instancia deStateTable
como eliminada. - Hibernate sincroniza
newCountry
con la base de datos. La operaciónstateTableList
cascada a lastateTableList
que ahora contiene la instancia de entidad deStateTable
eliminada. - La instancia de
StateTable
eliminada ahora se administra de nuevo (sección 3.2.2 de la especificación JPA citada anteriormente).
- Hibernate sincroniza
Cuando la línea adicional (
hashCode
) está ausente:- Hibernate sincroniza
newCountry
con la base de datos. Lo hace antes de manejaroldCountry
, porquenewCountry
se cargó primero (conentityManager.find
). - Hibernate sincroniza
oldCountry
con la base de datos. - Hibernate ve que una instancia de
StateTable
se ha eliminado de la colecciónoldCountry
, marcando así la instancia deStateTable
como eliminada. - La eliminación de la instancia de
StateTable
se sincroniza con la base de datos.
- Hibernate sincroniza
El orden de las actualizaciones también explica sus hallazgos en los que básicamente obligó a la inicialización del proxy oldCountry
a ocurrir antes de cargar newCountry
desde la base de datos.
Entonces, ¿esto es de acuerdo con la especificación JPA? Obviamente sí, ninguna regla de especificación JPA está rota.
¿Por qué esto no es portátil?
La especificación JPA (como cualquier otra especificación después de todo) le da libertad a los proveedores para definir muchos detalles que no están cubiertos por la especificación.
Además, eso depende de su visión de la ''portabilidad''. La función orphanRemoval
y cualquier otra característica JPA son portátiles cuando se trata de sus definiciones formales. Sin embargo, depende de cómo los use en combinación con los detalles específicos de su proveedor de JPA.
Por cierto, la sección 2.9 de la especificación recomienda (pero no lo define claramente) para la orphanRemoval
:
De lo contrario, las aplicaciones portátiles no deben depender de un orden específico de eliminación y no deben reasignar a una entidad que ha quedado huérfana a otra relación o intentar de otra manera persistirla.
Pero este es solo un ejemplo de recomendaciones vagas o no bien definidas en la especificación, porque la persistencia de las entidades eliminadas está permitida por otras declaraciones en la especificación.
Tomando un ejemplo muy simple de relación uno a muchos (país ->
estado).
País (lado inverso):
@OneToMany(mappedBy = "country", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<StateTable> stateTableList=new ArrayList<StateTable>(0);
StateTable (lado propietario):
@JoinColumn(name = "country_id", referencedColumnName = "country_id")
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH})
private Country country;
El método que intenta actualizar una entidad StateTable
suministrada (separada) dentro de una transacción de base de datos activa (JTA o recurso local):
public StateTable update(StateTable stateTable) {
// Getting the original state entity from the database.
StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId());
// Get hold of the original country (with countryId = 67, for example).
Country oldCountry = oldState.getCountry();
// Getting a new country entity (with countryId = 68) supplied by the client application which is responsible for modifying the StateTable entity.
// Country has been changed from 67 to 68 in the StateTable entity using for example, a drop-down list.
Country newCountry = entityManager.find(Country.class, stateTable.getCountry().getCountryId());
// Attaching a managed instance to StateTable.
stateTable.setCountry(newCountry);
// Check whether the supplied country and the original country entities are equal.
// (Both not null and not equal - http://stackoverflow.com/a/31761967/1391249)
if (ObjectUtils.notEquals(newCountry, oldCountry)) {
// Remove the state entity from the inverse collection held by the original country entity.
oldCountry.remove(oldState);
// Add the state entity to the inverse collection held by the newly supplied country entity
newCountry.add(stateTable);
}
return entityManager.merge(stateTable);
}
Cabe señalar que orphanRemoval
se establece en true
. La entidad StateTable
es suministrada por una aplicación cliente que está interesada en cambiar el Country
asociación de entidad ( countryId = 67
) en StateTable
a otra cosa ( countryId = 68
) (por lo tanto, en el lado inverso en JPA, migrar una entidad secundaria de su matriz colección) a otro padre (colección) a la que orphanRemoval=true
se opondrá a su vez).
El proveedor de Hibernate emite una sentencia DELETE
DML que hace que la fila correspondiente a la entidad StateTable
se elimine de la tabla de base de datos subyacente.
A pesar del hecho de que orphanRemoval
se establece en true
, espero que Hibernate emita una declaración UPDATE
DML regular que cause que el efecto de orphanRemoval
se suspenda completamente porque el enlace de la relación se migra (no simplemente se elimina).
EclipseLink hace exactamente ese trabajo. Emite una instrucción UPDATE
en el escenario dado (que tiene la misma relación con orphanRemoval
establecida en true
).
¿Cuál se está comportando de acuerdo con la especificación? ¿Es posible hacer que Hibernate emita una declaración de UPDATE
en este caso que no sea la eliminación de orphanRemoval
del lado inverso?
Esto es solo un intento de hacer que una relación bidireccional sea más consistente en ambos lados.
Los métodos de administración de enlaces defensivos, a saber, add()
y remove()
utilizados en el fragmento de código anterior, si es necesario, se definen en la entidad de Country
siguiente manera.
public void add(StateTable stateTable) {
List<StateTable> newStateTableList = getStateTableList();
if (!newStateTableList.contains(stateTable)) {
newStateTableList.add(stateTable);
}
if (stateTable.getCountry() != this) {
stateTable.setCountry(this);
}
}
public void remove(StateTable stateTable) {
List<StateTable> newStateTableList = getStateTableList();
if (newStateTableList.contains(stateTable)) {
newStateTableList.remove(stateTable);
}
}
Actualización:
Hibernate solo puede emitir una sentencia DUP UPDATE
esperada, si el código dado se modifica de la siguiente manera.
public StateTable update(StateTable stateTable) {
StateTable oldState = entityManager.find(StateTable.class, stateTable.getStateId());
Country oldCountry = oldState.getCountry();
// DELETE is issued, if getReference() is replaced by find().
Country newCountry = entityManager.getReference(Country.class, stateTable.getCountry().getCountryId());
// The following line is never expected as Country is already retrieved
// and assigned to oldCountry above.
// Thus, oldState.getCountry() is no longer an uninitialized proxy.
oldState.getCountry().hashCode(); // DELETE is issued, if removed.
stateTable.setCountry(newCountry);
if (ObjectUtils.notEquals(newCountry, oldCountry)) {
oldCountry.remove(oldState);
newCountry.add(stateTable);
}
return entityManager.merge(stateTable);
}
Observe las siguientes dos líneas en la versión más reciente del código.
// Previously it was EntityManager#find()
Country newCountry = entityManager.getReference(Country.class, stateTable.getCountry().getCountryId());
// Previously it was absent.
oldState.getCountry().hashCode();
Si la última línea está ausente o EntityManager#getReference()
se reemplaza por EntityManager#find()
, entonces se emite inesperadamente una instrucción DELETE
DML.
¿Entonces, qué está pasando aquí? Especialmente, enfatizo la portabilidad. No portar este tipo de funcionalidad básica a través de diferentes proveedores de JPA anula el uso de los marcos ORM severamente.
Entiendo la diferencia básica entre EntityManager#getReference()
y EntityManager#find()
.
Tan pronto como su entidad referenciada se puede usar en otros padres, se complica de todos modos. Para hacerlo realmente limpio, el ORM tuvo que buscar en la base de datos cualquier otro uso de la entidad eliminada antes de eliminarla (recolección de basura persistente). Esto consume tiempo y, por lo tanto, no es realmente útil y, por lo tanto, no se implementa en Hibernate.
Eliminar huérfanos solo funciona si su hijo se usa para un solo padre y nunca se reutiliza en otro lugar. Incluso puede obtener una excepción cuando intente reutilizarla para detectar mejor el mal uso de esta función.
Decida si desea mantener eliminar los huérfanos o no. Si desea conservarlo, debe crear un nuevo hijo para el nuevo padre en lugar de moverlo.
Si abandona la eliminación de huérfanos, deberá eliminar a los hijos usted mismo tan pronto como ya no se haga referencia a ellos.