Quelle est la bonne façon de rattacher des objets détachés dans Hibernate?

191

J'ai une situation dans laquelle j'ai besoin de rattacher des objets détachés à une session de mise en veille prolongée, bien qu'un objet de la même identité PEUT déjà exister dans la session, ce qui provoquera des erreurs.

En ce moment, je peux faire deux choses.

  1. getHibernateTemplate().update( obj ) Cela fonctionne si et seulement si un objet n'existe pas déjà dans la session de mise en veille prolongée. Des exceptions sont levées indiquant qu'un objet avec l'identifiant donné existe déjà dans la session lorsque j'en ai besoin plus tard.

  2. getHibernateTemplate().merge( obj ) Cela fonctionne si et seulement si un objet existe dans la session de mise en veille prolongée. Des exceptions sont levées lorsque j'ai besoin que l'objet soit dans une session plus tard si j'utilise ceci.

Compte tenu de ces deux scénarios, comment puis-je attacher de manière générique des sessions à des objets? Je ne veux pas utiliser d'exceptions pour contrôler le flux de la solution de ce problème, car il doit y avoir une solution plus élégante ...

Stefan Kendall
la source

Réponses:

184

Il semble donc qu'il n'y ait aucun moyen de rattacher une entité détachée périmée dans JPA.

merge() poussera l'état périmé vers la base de données et écrasera toutes les mises à jour intervenues.

refresh() ne peut pas être appelé sur une entité détachée.

lock() ne peut pas être appelé sur une entité détachée, et même si elle le pouvait, et elle a réattaché l'entité, appeler 'lock' avec l'argument 'LockMode.NONE' impliquant que vous verrouillez, mais pas verrouillez, est l'élément le plus contre-intuitif de la conception d'API Que j'ai jamais vu.

Alors vous êtes coincé. Il y a une detach()méthode, mais non attach()ou reattach(). Une étape évidente du cycle de vie des objets n'est pas disponible pour vous.

À en juger par le nombre de questions similaires sur JPA, il semble que même si JPA prétend avoir un modèle cohérent, il ne correspond certainement pas au modèle mental de la plupart des programmeurs, qui ont été maudits de perdre de nombreuses heures à essayer de comprendre comment JPA pour faire les choses les plus simples et se retrouver avec du code de gestion du cache partout dans leurs applications.

Il semble que le seul moyen de le faire est de supprimer votre entité détachée périmée et de faire une requête de recherche avec le même identifiant, qui atteindra la L2 ou la base de données.

Mik

mikhailfranco
la source
1
Je me demande s'il y a une raison pour laquelle la spécification JPA n'autorise pas refresh()les entités détachées? En regardant à travers la spécification 2.0, je ne vois aucune justification; juste que ce n'est pas autorisé.
FGreg
12
Ce n'est certainement PAS exact. De JPwH: *Reattaching a modified detached instance* A detached instance may be reattached to a new Session (and managed by this new persistence context) by calling update() on the detached object. In our experience, it may be easier for you to understand the following code if you rename the update() method in your mind to reattach()—however, there is a good reason it’s called updating.Plus d'informations peuvent être trouvées dans la section 9.3.2
cwash
Les objets persistants fonctionnent très bien, l'indicateur modifié est défini en fonction du delta entre la charge initiale et la ou les valeurs au moment de flush (). Les objets détachés ont besoin et n'ont pas actuellement cette fonctionnalité. La manière pour hibernate de le faire est d'ajouter un hash / id supplémentaire pour les objets détachés. Et conservez un instantané du dernier état de l'objet détaché disponible, comme ils le font pour les objets persistants. Ainsi, ils peuvent exploiter tout ce code existant et le faire fonctionner pour les objets détachés. De cette façon, comme @mikhailfranco l'a noté, nous ne "pousserons pas l'état périmé vers la base de données et n'écraserons pas les mises à jour intermédiaires"
tom
2
Selon le javadoc Hibernate (mais pas JPA), lock(LockMode.NONE)peut en fait être appelé sur un objet transitoire, et il rattache l'entité à la session. Voir stackoverflow.com/a/3683370/14379
seanf
lock n'a pas fonctionné pour moi: java.lang.IllegalArgumentException: entité pas dans le contexte de persistance à org.hibernate.internal.SessionImpl.lock (SessionImpl.java:3491) à org.hibernate.internal.SessionImpl.lock (SessionImpl. java: 3482) à com.github.vok.framework.DisableTransactionControlEMDelegate.lock (DB.kt)
Martin Vysny
35

Toutes ces réponses manquent une distinction importante. update () est utilisé pour (re) attacher votre graphe d'objets à une Session. Les objets que vous passez sont ceux qui sont rendus gérés.

merge () n'est en fait pas une API de (re) pièce jointe. Remarquez que merge () a une valeur de retour? C'est parce qu'il vous renvoie le graphique géré, qui n'est peut-être pas le graphique que vous lui avez transmis. merge () est une API JPA et son comportement est régi par la spécification JPA. Si l'objet que vous passez à merge () est déjà géré (déjà associé à la Session), c'est le graphe avec lequel Hibernate fonctionne; l'objet passé est le même objet renvoyé par merge (). Si, cependant, l'objet que vous passez dans merge () est détaché, Hibernate crée un nouveau graphe d'objets qui est géré et copie l'état de votre graphe détaché sur le nouveau graphe géré. Encore une fois, tout cela est dicté et régi par la spécification JPA.

En termes de stratégie générique pour «s'assurer que cette entité est gérée, ou la rendre gérée», cela dépend en quelque sorte de si vous souhaitez également prendre en compte les données non encore insérées. En supposant que vous le faites, utilisez quelque chose comme

if ( session.contains( myEntity ) ) {
    // nothing to do... myEntity is already associated with the session
}
else {
    session.saveOrUpdate( myEntity );
}

Remarquez que j'ai utilisé saveOrUpdate () plutôt que update (). Si vous ne voulez pas que les données non encore insérées soient gérées ici, utilisez plutôt update () ...

Steve Ebersole
la source
3
C'est la bonne réponse à cette question - affaire classée!
cwash le
3
Session.contains(Object)contrôles par référence. S'il existe déjà une autre entité représentant la même ligne dans la session et que vous transmettez une instance détachée, vous obtiendrez une exception.
djmj
Comme Session.contains(Object)contrôle par référence, s'il y a une autre Entité représentant la même ligne en session, elle retournera false et elle la mettra à jour.
AxelWass
19

Réponse peu diplomatique: vous recherchez probablement un contexte de persistance étendue. C'est l'une des principales raisons derrière le framework Seam ... Si vous avez du mal à utiliser Hibernate au printemps en particulier, consultez cette partie de la documentation de Seam.

Réponse diplomatique: Ceci est décrit dans la documentation Hibernate . Si vous avez besoin de plus de précisions, consultez la section 9.3.2 de Java Persistence with Hibernate intitulée «Working with Detached Objects». Je vous recommande vivement d'obtenir ce livre si vous faites autre chose que CRUD avec Hibernate.

cwash
la source
5
De seamframework.org : "Le développement actif de Seam 3 a été interrompu par Red Hat." Le lien "cette partie de la documentation de Seam" est également mort.
badbishop
15

Si vous êtes sûr que votre entité n'a pas été modifiée (ou si vous acceptez que toute modification sera perdue), vous pouvez la rattacher à la session avec verrouillage.

session.lock(entity, LockMode.NONE);

Il ne verrouille rien, mais il récupère l'entité du cache de session ou (s'il n'y est pas trouvé) la lit à partir de la base de données.

Il est très utile d'empêcher LazyInitException lorsque vous naviguez dans les relations à partir d'une "ancienne" entité (de HttpSession par exemple). Vous «reconnectez» d'abord l'entité.

L'utilisation de get peut également fonctionner, sauf lorsque vous obtenez l'héritage mappé (ce qui lèvera déjà une exception sur getId ()).

entity = session.get(entity.getClass(), entity.getId());
John Rizzo
la source
2
Je souhaite réassocier une entité à session. Malheureusement, Session.lock(entity, LockMode.NONE)échoue avec une exception disant: impossible de réassocier la collection transitoire non initialisée. Comment surmonter cela?
dma_k du
1
En fait, je n'avais pas tout à fait raison. L'utilisation de lock () rattache votre entité, mais pas les autres entités qui lui sont liées. Donc, si vous faites entity.getOtherEntity (). GetYetAnotherEntity (), vous pouvez avoir une exception LazyInit. Le seul moyen que je connaisse pour surmonter cela est d'utiliser find. entity = em.find (entity.getClass (), entity.getId ();
John Rizzo
Il n'y a pas de Session.find()méthode API. Peut-être que vous voulez dire Session.load(Object object, Serializable id).
dma_k
13

Comme il s'agit d'une question très courante, j'ai écrit cet article , sur lequel cette réponse est basée.

États d'entité

JPA définit les états d'entité suivants:

Nouveau (transitoire)

Un objet nouvellement créé qui n'a jamais été associé à un Hibernate Session(aka Persistence Context) et n'est mappé à aucune ligne de table de base de données est considéré comme étant dans l'état Nouveau (transitoire).

Pour devenir persistant, nous devons soit appeler explicitement la EntityManager#persistméthode, soit utiliser le mécanisme de persistance transitive.

Persistant (géré)

Une entité persistante a été associée à une ligne de table de base de données et elle est gérée par le contexte de persistance en cours d'exécution. Toute modification apportée à une telle entité va être détectée et propagée à la base de données (pendant le vidage de la session).

Avec Hibernate, nous n'avons plus besoin d'exécuter des instructions INSERT / UPDATE / DELETE. Hibernate utilise un style de travail transactionnel à écriture différée et les modifications sont synchronisées au tout dernier moment responsable, pendant le Sessiontemps de vidage actuel .

Détaché

Une fois le contexte de persistance en cours d'exécution fermé, toutes les entités précédemment gérées sont détachées. Les modifications successives ne seront plus suivies et aucune synchronisation automatique de la base de données ne se produira.

Transitions d'état d'entité

Vous pouvez modifier l'état de l'entité à l'aide de diverses méthodes définies par l' EntityManagerinterface.

Pour mieux comprendre les transitions d'état des entités JPA, considérez le diagramme suivant:

Transitions d'état d'entité JPA

Lorsque vous utilisez JPA, pour réassocier une entité détachée à un actif EntityManager, vous pouvez utiliser l' opération de fusion .

Lors de l'utilisation de l'API Hibernate native merge, vous pouvez également rattacher une entité détachée à une session Hibernate active à l'aide des méthodes de mise à jour, comme le montre le diagramme suivant:

Transitions d'état des entités Hibernate

Fusionner une entité détachée

La fusion va copier l'état de l'entité détachée (source) vers une instance d'entité gérée (destination).

Considérez que nous avons persisté l' Bookentité suivante , et maintenant l'entité est détachée car celle EntityManagerqui a été utilisée pour conserver l'entité s'est fermée:

Book _book = doInJPA(entityManager -> {
    Book book = new Book()
    .setIsbn("978-9730228236")
    .setTitle("High-Performance Java Persistence")
    .setAuthor("Vlad Mihalcea");

    entityManager.persist(book);

    return book;
});

Pendant que l'entité est à l'état détaché, nous la modifions comme suit:

_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);

Maintenant, nous voulons propager les modifications dans la base de données, afin que nous puissions appeler la mergeméthode:

doInJPA(entityManager -> {
    Book book = entityManager.merge(_book);

    LOGGER.info("Merging the Book entity");

    assertFalse(book == _book);
});

Et Hibernate va exécuter les instructions SQL suivantes:

SELECT
    b.id,
    b.author AS author2_0_,
    b.isbn AS isbn3_0_,
    b.title AS title4_0_
FROM
    book b
WHERE
    b.id = 1

-- Merging the Book entity

UPDATE
    book
SET
    author = 'Vlad Mihalcea',
    isbn = '978-9730228236',
    title = 'High-Performance Java Persistence, 2nd edition'
WHERE
    id = 1

Si l'entité fusionnée n'a pas d'équivalent dans le courant EntityManager, un instantané d'entité frais sera extrait de la base de données.

Une fois qu'il y a une entité gérée, JPA copie l'état de l'entité détachée sur celle qui est actuellement gérée, et pendant le contexte de persistanceflush , une MISE À JOUR sera générée si le mécanisme de vérification sale trouve que l'entité gérée a changé.

Ainsi, lors de l'utilisation merge, l'instance d'objet détaché continuera à rester détachée même après l'opération de fusion.

Rattacher une entité détachée

Hibernate, mais pas JPA, prend en charge la reconnexion via la updateméthode.

Un Hibernate Sessionne peut associer qu'un seul objet entité pour une ligne de base de données donnée. Cela est dû au fait que le contexte de persistance agit comme un cache en mémoire (cache de premier niveau) et qu'une seule valeur (entité) est associée à une clé donnée (type d'entité et identificateur de base de données).

Une entité ne peut être rattachée que s'il n'y a pas d'autre objet JVM (correspondant à la même ligne de base de données) déjà associé au Hibernate actuel Session.

Considérant que nous avons persisté l' Bookentité et que nous l'avons modifiée lorsque l' Bookentité était à l'état détaché:

Book _book = doInJPA(entityManager -> {
    Book book = new Book()
    .setIsbn("978-9730228236")
    .setTitle("High-Performance Java Persistence")
    .setAuthor("Vlad Mihalcea");

    entityManager.persist(book);

    return book;
});

_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);

Nous pouvons rattacher l'entité détachée comme ceci:

doInJPA(entityManager -> {
    Session session = entityManager.unwrap(Session.class);

    session.update(_book);

    LOGGER.info("Updating the Book entity");
});

Et Hibernate exécutera l'instruction SQL suivante:

-- Updating the Book entity

UPDATE
    book
SET
    author = 'Vlad Mihalcea',
    isbn = '978-9730228236',
    title = 'High-Performance Java Persistence, 2nd edition'
WHERE
    id = 1

La updateméthode vous oblige à unwrapl' EntityManagerune mise en veille prolongée Session.

Contrairement à merge, l'entité détachée fournie va être réassociée au contexte de persistance actuel et une MISE À JOUR est planifiée pendant le vidage, que l'entité ait été modifiée ou non.

Pour éviter cela, vous pouvez utiliser l' @SelectBeforeUpdateannotation Hibernate qui déclenchera une instruction SELECT qui récupère l'état chargé qui est ensuite utilisée par le mécanisme de vérification sale.

@Entity(name = "Book")
@Table(name = "book")
@SelectBeforeUpdate
public class Book {

    //Code omitted for brevity
}

Méfiez-vous de l'exception NonUniqueObjectException

Un problème qui peut se produire updateest si le contexte de persistance contient déjà une référence d'entité avec le même identifiant et du même type que dans l'exemple suivant:

Book _book = doInJPA(entityManager -> {
    Book book = new Book()
    .setIsbn("978-9730228236")
    .setTitle("High-Performance Java Persistence")
    .setAuthor("Vlad Mihalcea");

    Session session = entityManager.unwrap(Session.class);
    session.saveOrUpdate(book);

    return book;
});

_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);

try {
    doInJPA(entityManager -> {
        Book book = entityManager.find(
            Book.class,
            _book.getId()
        );

        Session session = entityManager.unwrap(Session.class);
        session.saveOrUpdate(_book);
    });
} catch (NonUniqueObjectException e) {
    LOGGER.error(
        "The Persistence Context cannot hold " +
        "two representations of the same entity",
        e
    );
}

Maintenant, lors de l'exécution du cas de test ci-dessus, Hibernate va lancer un NonUniqueObjectExceptioncar le second EntityManagercontient déjà une Bookentité avec le même identifiant que celui auquel nous passons update, et le contexte de persistance ne peut pas contenir deux représentations de la même entité.

org.hibernate.NonUniqueObjectException:
    A different object with the same identifier value was already associated with the session : [com.vladmihalcea.book.hpjp.hibernate.pc.Book#1]
    at org.hibernate.engine.internal.StatefulPersistenceContext.checkUniqueness(StatefulPersistenceContext.java:651)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performUpdate(DefaultSaveOrUpdateEventListener.java:284)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.entityIsDetached(DefaultSaveOrUpdateEventListener.java:227)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:92)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:73)
    at org.hibernate.internal.SessionImpl.fireSaveOrUpdate(SessionImpl.java:682)
    at org.hibernate.internal.SessionImpl.saveOrUpdate(SessionImpl.java:674)

Conclusion

La mergeméthode est à privilégier si vous utilisez un verrouillage optimiste car elle vous permet d'éviter la perte de mises à jour. Pour plus de détails sur ce sujet, consultez cet article .

Il updateconvient aux mises à jour par lots, car il peut empêcher l'instruction SELECT supplémentaire générée par l' mergeopération, réduisant ainsi le temps d'exécution de la mise à jour par lots.

Vlad Mihalcea
la source
Bonne réponse. Je m'interrogeais cependant sur l' @SelectBeforeUpdateannotation. Quand la sélection est-elle déclenchée? Lors de l'appel update, juste avant le vidage ou cela n'a pas vraiment d'importance (cela peut être important si hibernate récupère toutes les entités annotées en un seul appel avant le vidage)?
Andronicus
Le @SelectBeforeUpdatedéclenche le SELECT pendant l' flushopération de contexte de persistance . Consultez la getDatabaseSnapshotméthode dans leDefaultFlushEntityEventListener pour plus de détails.
Vlad Mihalcea le
Que faire si vous souhaitez enregistrerOrUpdate et éviter l'exception NonUniqueObjectException en même temps? Vous souhaitez mettre à jour le contexte si l'entité est là, sinon créez-en une nouvelle. Devez-vous le faire manuellement ou il y a moyen?
giannis christofakis
Merge résout ce problème. C'est la solution la plus simple.
Vlad Mihalcea
10

Je suis retourné au JavaDoc pour org.hibernate.Sessionet j'ai trouvé ce qui suit:

Les instances transitoires peuvent être rendues persistantes en appelant save(), persist()ou saveOrUpdate(). Les instances persistantes peuvent être rendues transitoires en appelant delete(). Toute instance renvoyée par une méthode get()ou load()est persistante. En appelant les instances individuelles peuvent être persistantes update(), saveOrUpdate(), lock()ou replicate(). L'état d'une instance transitoire ou détachée peut également être rendu persistant en tant que nouvelle instance persistante en appelant merge().

Ainsi update(), saveOrUpdate(), lock(), replicate()et merge()sont les options de candidats.

update(): Lèvera une exception s'il existe une instance persistante avec le même identifiant.

saveOrUpdate(): Enregistrer ou mettre à jour

lock(): Obsolète

replicate(): Conserve l'état de l'instance détachée donnée, en réutilisant la valeur d'identificateur actuelle.

merge(): Renvoie un objet persistant avec le même identifiant. L'instance donnée n'est pas associée à la session.

Par conséquent, lock()ne doit pas être utilisé directement et en fonction de l'exigence fonctionnelle, un ou plusieurs d'entre eux peuvent être choisis.

Amitabha Roy
la source
7

Je l'ai fait de cette façon en C # avec NHibernate, mais cela devrait fonctionner de la même manière en Java:

public virtual void Attach()
{
    if (!HibernateSessionManager.Instance.GetSession().Contains(this))
    {
        ISession session = HibernateSessionManager.Instance.GetSession();
        using (ITransaction t = session.BeginTransaction())
        {
            session.Lock(this, NHibernate.LockMode.None);
            t.Commit();
        }
    }
}

First Lock a été appelé sur chaque objet car Contains était toujours false. Le problème est que NHibernate compare les objets par identifiant et type de base de données. Contains utilise la equalsméthode, qui compare par référence si elle n'est pas écrasée. Avec cette equalsméthode, cela fonctionne sans aucune exception:

public override bool Equals(object obj)
{
    if (this == obj) { 
        return true;
    } 
    if (GetType() != obj.GetType()) {
        return false;
    }
    if (Id != ((BaseObject)obj).Id)
    {
        return false;
    }
    return true;
}
Verena Haunschmid
la source
4

Session.contains(Object obj) vérifie la référence et ne détecte pas une instance différente qui représente la même ligne et y est déjà attachée.

Voici ma solution générique pour les entités avec une propriété d'identifiant.

public static void update(final Session session, final Object entity)
{
    // if the given instance is in session, nothing to do
    if (session.contains(entity))
        return;

    // check if there is already a different attached instance representing the same row
    final ClassMetadata classMetadata = session.getSessionFactory().getClassMetadata(entity.getClass());
    final Serializable identifier = classMetadata.getIdentifier(entity, (SessionImplementor) session);

    final Object sessionEntity = session.load(entity.getClass(), identifier);
    // override changes, last call to update wins
    if (sessionEntity != null)
        session.evict(sessionEntity);
    session.update(entity);
}

C'est l'un des rares aspects de .Net EntityFramework que j'aime, les différentes options d'attachement concernant les entités modifiées et leurs propriétés.

djmj
la source
3

J'ai trouvé une solution pour "rafraîchir" un objet du magasin de persistance qui tiendra compte d'autres objets qui peuvent déjà être attachés à la session:

public void refreshDetached(T entity, Long id)
{
    // Check for any OTHER instances already attached to the session since
    // refresh will not work if there are any.
    T attached = (T) session.load(getPersistentClass(), id);
    if (attached != entity)
    {
        session.evict(attached);
        session.lock(entity, LockMode.NONE);
    }
    session.refresh(entity);
}
WhoopP
la source
2

Désolé, impossible d'ajouter des commentaires (encore?).

Utilisation d'Hibernate 3.5.0-Final

Alors que la Session#lockméthode de cette dépréciée, la javadoc ne suggère l' utilisation Session#buildLockRequest(LockOptions)#lock(entity)et si vous assurer que vos associations ont cascade=lock, le chargement paresseux est pas un problème non plus .

Donc, ma méthode d'attachement ressemble un peu à

MyEntity attach(MyEntity entity) {
    if(getSession().contains(entity)) return entity;
    getSession().buildLockRequest(LockOptions.NONE).lock(entity);
    return entity;

Les premiers tests suggèrent que cela fonctionne un régal.

Gwaptiva
la source
2

Il se comporte peut-être légèrement différemment sur Eclipselink. Pour rattacher des objets détachés sans obtenir de données obsolètes, je fais généralement:

Object obj = em.find(obj.getClass(), id);

et en option une deuxième étape (pour invalider les caches):

em.refresh(obj)
Hartmut P.
la source
1

essayez getHibernateTemplate (). replicate (entité, ReplicationMode.LATEST_VERSION)

Pavitar Singh
la source
1

Dans le message original, il existe deux méthodes, update(obj)et merge(obj)qui sont mentionnées pour fonctionner, mais dans des circonstances opposées. Si cela est vraiment vrai, alors pourquoi ne pas tester pour voir si l'objet est déjà dans la session en premier, puis appeler update(obj)si c'est le cas, sinon appeler merge(obj).

Le test d'existence dans la session est session.contains(obj). Par conséquent, je pense que le pseudo-code suivant fonctionnerait:

if (session.contains(obj))
{
    session.update(obj);
}
else 
{
    session.merge(obj);
}
John DeRegnaucourt
la source
3
contains () vérifie la comparaison par référence, mais les fonctions de mise en veille prolongée fonctionnent par identifiant de base de données. session.merge ne sera jamais appelée dans votre code.
Verena Haunschmid
1

pour rattacher cet objet, vous devez utiliser merge ();

cette méthode accepte en paramètre votre entité détachée et retourne une entité sera attachée et rechargée depuis la base de données.

Example :
    Lot objAttach = em.merge(oldObjDetached);
    objAttach.setEtat(...);
    em.persist(objAttach);
Ryuku
la source
0

appeler d'abord merge () (pour mettre à jour l'instance persistante), puis verrouiller (LockMode.NONE) (pour attacher l'instance actuelle, pas celle renvoyée par merge ()) semble fonctionner pour certains cas d'utilisation.

fromage
la source
0

La propriété a hibernate.allow_refresh_detached_entityfait l'affaire pour moi. Mais c'est une règle générale, donc ce n'est pas très approprié si vous ne voulez le faire que dans certains cas. J'espère que cela aide.

Testé sur Hibernate 5.4.9

SessionFactoryOptionsBuilder

Radeck
la source
-6
try getHibernateTemplate().saveOrUpdate()
Ben Hammond
la source