Empêcher Hibernate de supprimer les entités orphelines tout en fusionnant une entité ayant des associations d'entités avec orphanRemoval mis à true

Prendre un exemple très simple de un-à-plusieurs relations (pays -> état).

Pays (l'inverse de côté) :

@OneToMany(mappedBy = "country", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<StateTable> stateTableList=new ArrayList<StateTable>(0);

StateTable (propriétaire de côté) :

@JoinColumn(name = "country_id", referencedColumnName = "country_id")
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH})
private Country country;

la méthode qui tente de mettre à jour un (detached) fourni StateTable entité au sein d'une transaction de base de données (JTA ou de ressources locales) :

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);
}

Il est à noter que orphanRemoval est réglé sur true. StateTable l'entité est fournie par une application client qui est intéressé à changer l'association entity Country (countryId = 67)StateTable pour autre chose (countryId = 68) (donc du côté inverse dans JPA, la migration d'une entité enfant de son parent (collection) à un autre parent (collection) qui orphanRemoval=true s'opposera à son tour).

Hibernate problèmes de fournisseur DELETE DML déclaration provoquant la ligne correspondant au StateTable entité à être retiré de la table sous-jacente.

Malgré le fait que orphanRemoval est réglé sur true, je m'attends à Hiberner à l'émission d'un régulierUPDATE instruction DML provoquant l'effet de orphanRemoval pour être suspendu dans son intégralité parce que le lien de parenté est migré (et non pas simplement supprimés).

EclipseLink fait exactement ce travail. Il émet un UPDATE énoncé dans le scénario donné (ayant la même relation avec orphanRemovaltrue).

lequel se comporte selon la spécification? Est-il possible de faire hiberner question UPDATE déclaration dans ce cas autre que supprimer orphanRemoval à partir de l'inverse de ce côté?


ce n'est qu'une tentative pour rendre une relation bidirectionnelle plus cohérente des deux côtés.

les méthodes de gestion des liens défensifs à savoiradd() et remove() utilisé dans l'extrait ci-dessus, si nécessaire, sont définies dans le Country entité suivre.

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);
    }
}


mise à Jour :

hibernation ne peut émettre qu'un UPDATE déclaration DML, si le code indiqué est modifié de la manière suivante.

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);
}

observez les deux lignes suivantes dans la nouvelle version du code.

// Previously it was EntityManager#find()
Country newCountry = entityManager.getReference(Country.class, stateTable.getCountry().getCountryId());
// Previously it was absent.
oldState.getCountry().hashCode();

Si la dernière ligne est absente ou EntityManager#getReference() est remplacé par EntityManager#find(), puis DELETE la déclaration DML est inattendue émettre.

Donc, ce qui se passe ici? Surtout, j'insiste sur la portabilité. Ne pas porter ce genre de base la fonctionnalité chez les différents fournisseurs JPA va à l'encontre de l'utilisation des cadres ORM.

je comprends la différence fondamentale entre EntityManager#getReference() et EntityManager#find().

22
demandé sur Tiny 2015-11-30 20:52:06

2 réponses

tout D'abord, changeons votre code original pour une forme plus 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);

notez que j'ai seulement ajouté oldState.getCountry().hashCode() dans la troisième ligne. Vous pouvez maintenant reproduire votre numéro en retirant cette ligne seulement.

avant que nous expliquions ce qui se passe ici, d'abord quelques extraits de la spécification JPA 2.1.

3.2.4:

La sémantique de l'opération de vidage, appliqué à une entité X suit:

  • Si X est une entité gérée, elle est synchronisée avec la base de données.
    • pour toutes les entités Y référencées par une relation de X, si la relation à Y a été annotée avec la valeur de l'élément cascade cascade = PERSIST ou cascade=ALL, l'opération persist est appliquée à Y

3.2.2:

la sémantique de l'opération persist, appliqué à une entité X suit:

  • Si X est une entité supprimée, elle devient gérée.

orphanRemoval JPA javadoc:

(optionnel) Si appliquer l'opération de suppression aux entités qui ont été retirés de la relation et à la cascade le remove d'exploitation à ces entités.

Comme on peut le voir, orphanRemoval est définie dans termes de remove opération, donc toutes les règles qui s'appliquent pour remove demander orphanRemoval.

Deuxièmement, comme expliqué dans cette réponse, l'ordre des mises à jour exécutée par Hibernate est l'ordre dans lequel les entités sont chargées dans le contexte de persistance. Pour être plus précis, mettre à jour une entité signifie synchroniser son état actuel (check sale) avec la base de données et la cascade du PERSIST fonctionnement à son Association.

voilà ce qui se passe dans votre dossier. À la fin de la transaction, Hibernate synchronise le contexte de persistance avec la base de données. Nous avons deux scénarios:

  1. quand la ligne supplémentaire (hashCode):

    1. synchronise L'hibernation oldCountry avec le DB. Il le fait avant de manipuler newCountry, parce que oldCountry a été chargé en premier (initialisation par procuration forcée en appelant hashCode).
    2. hibernation voit que un StateTable l'instance a été retirée de oldCountry's collection, marquant ainsi l' StateTable exemple comme supprimé.
    3. synchronise L'hibernation newCountry avec le DB. PERSIST opération de cascades à l' stateTableList qui contient maintenant supprimés StateTable instance d'entité.
    4. supprimé StateTable l'instance est maintenant gérée à nouveau (3.2.2 section de la spécification JPA Citée surtout.)
  2. quand la ligne supplémentaire (hashCode) est absent :

    1. synchronise L'hibernation newCountry avec le DB. Il le fait avant de manipuler oldCountry, parce que newCountry a été chargé en premier (avec entityManager.find).
    2. synchronise L'hibernation oldCountry avec le DB.
    3. hibernation voit que un StateTable l'instance a été retirée de oldCountry's collection, marquant ainsi l' StateTable exemple comme retiré.
    4. la suppression du StateTable l'instance est synchronisée avec la base de données.

L'ordre des mises à jour explique aussi vos conclusions dans lesquelles vous avez forcé oldCountry initialisation par procuration avant le chargement newCountry à partir de la DB.

alors, est-ce que cela est conforme à la spécification JPA? De toute évidence, oui, aucune règle particulière de L'app n'est enfreinte.

pourquoi n'est-ce pas portatif?

spécification JPA (comme toute autre spécification après tout) donne aux fournisseurs la liberté de définir de nombreux détails qui ne sont pas couverts par la spécification.

Cela dépend aussi de votre point de vue sur la "portabilité". orphanRemoval fonctionnalité et les autres JPA caractéristiques sont portable quand il s'agit de leurs définitions formelles. Cependant, cela dépend de la façon dont vous les utilisez en combinaison avec les spécificités de votre fournisseur D'accès.

Par le chemin, la section 2,9 du spec recommande (mais pas clairement définir) pour le orphanRemoval:

applications Portables doivent pas dépendre d'une commande spécifique et ne doit pas réaffecter une entité orpheline à une autre entité. une autre relation ou autrement tenter de persister.

mais ce n'est qu'un exemple de recommandations vagues ou mal définies dans la spécification, parce que la persistance des entités enlevées est permise par d'autres énoncés dans la spécification.

11
répondu Dragan Bozanovic 2017-05-23 11:45:00

dès que votre entité référencée peut être utilisée dans d'autres parents, cela devient compliqué de toute façon. Pour le rendre vraiment propre, l'ORM a dû rechercher dans la base de données toutes les autres utilisations de l'entité enlevée avant de la supprimer (collecte de déchets persistante). Cela prend du temps et n'est donc pas vraiment utile et n'est donc pas mis en œuvre en hibernation.

Supprimer orphelins ne fonctionne que si votre enfant est utilisé pour un parent seul et n'a jamais été réutilisé ailleurs. Vous pouvez même obtenir un exception lorsque vous essayez de le réutiliser pour mieux détecter l'utilisation abusive de cette fonctionnalité.

Décider si vous voulez garder de supprimer les orphelins ou non. Si vous souhaitez le conserver, vous devez créer un nouvel enfant pour le parent au lieu de la déplacer.

si vous abandonnez supprimer orphelins, vous devez supprimer les enfants vous-même dès qu'ils ne sont plus référencés.

4
répondu Stefan Steinegger 2015-12-11 09:17:21