Quand utiliser EntityManager.find () vs EntityManager.getReference () avec JPA

103

Je suis tombé sur une situation (que je trouve étrange mais qui est peut-être tout à fait normale) où j'utilise EntityManager.getReference (LObj.getClass (), LObj.getId ()) pour obtenir une entité de base de données, puis passer l'objet retourné à être persisté dans une autre table.

Donc, fondamentalement, le flux était comme ceci:

classe TFacade {

  createT (FObj, AObj) {
    T TObj = nouveau T ();
    TObj.setF (FObj);
    TObj.setA (AObj);
    ...
    EntityManager.persist (TObj);
    ...
    L LObj = A.getL ();
    FObj.setL (LObj);
    FFacade.editF (FObj);
  }
}

@ TransactionAttributeType.REQUIRES_NEW
classe FFacade {

  editF (FObj) {
    L LObj = FObj.getL ();
    LObj = EntityManager.getReference (LObj.getClass (), LObj.getId ());
    ...
    EntityManager.merge (FObj);
    ...
    FLHFacade.create (FObj, LObj);
  }
}

@ TransactionAttributeType.REQUIRED
classe FLHFacade {

  createFLH (FObj, LObj) {
    FLH FLHObj = nouveau FLH ();
    FLHObj.setF (FObj);
    FLHObj.setL (LObj);
    ....
    EntityManager.persist (FLHObj);
    ...
  }
}

J'obtenais l'exception suivante "java.lang.IllegalArgumentException: Unknown entity: com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0"

Après l'avoir examiné pendant un moment, j'ai finalement compris que c'était parce que j'utilisais la méthode EntityManager.getReference () que j'obtenais l'exception ci-dessus car la méthode renvoyait un proxy.

Cela me fait me demander, quand est-il conseillé d'utiliser la méthode EntityManager.getReference () au lieu de la méthode EntityManager.find () ?

EntityManager.getReference () lève une EntityNotFoundException s'il ne trouve pas l'entité recherchée, ce qui est très pratique en soi. La méthode EntityManager.find () renvoie simplement null si elle ne trouve pas l'entité.

En ce qui concerne les limites des transactions, il me semble que vous auriez besoin d'utiliser la méthode find () avant de passer la nouvelle entité trouvée à une nouvelle transaction. Si vous utilisez la méthode getReference (), vous vous retrouverez probablement dans une situation similaire à la mienne avec l'exception ci-dessus.

SibzTer
la source
J'ai oublié de mentionner, j'utilise Hibernate comme fournisseur JPA.
SibzTer

Réponses:

152

J'utilise généralement la méthode getReference lorsque je n'ai pas besoin d'accéder à l'état de la base de données (je veux dire la méthode getter). Juste pour changer d'état (je veux dire la méthode setter). Comme vous devez le savoir, getReference renvoie un objet proxy qui utilise une fonctionnalité puissante appelée vérification automatique des informations incorrectes. Supposons ce qui suit

public class Person {

    private String name;
    private Integer age;

}


public class PersonServiceImpl implements PersonService {

    public void changeAge(Integer personId, Integer newAge) {
        Person person = em.getReference(Person.class, personId);

        // person is a proxy
        person.setAge(newAge);
    }

}

Si j'appelle la méthode de recherche , le fournisseur JPA, dans les coulisses, appellera

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Si j'appelle la méthode getReference , le fournisseur JPA, dans les coulisses, appellera

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Et vous savez pourquoi ???

Lorsque vous appelez getReference, vous obtiendrez un objet proxy. Quelque chose comme celui-ci (le fournisseur JPA se charge de l'implémentation de ce proxy)

public class PersonProxy {

    // JPA provider sets up this field when you call getReference
    private Integer personId;

    private String query = "UPDATE PERSON SET ";

    private boolean stateChanged = false;

    public void setAge(Integer newAge) {
        stateChanged = true;

        query += query + "AGE = " + newAge;
    }

}

Ainsi, avant la validation de la transaction, le fournisseur JPA verra l'indicateur stateChanged afin de mettre à jour OU NON l'entité personne. Si aucune ligne n'est mise à jour après l'instruction de mise à jour, le fournisseur JPA lèvera EntityNotFoundException conformément à la spécification JPA.

Cordialement,

Arthur Ronald
la source
4
J'utilise EclipseLink 2.5.0 et les requêtes mentionnées ci-dessus ne sont pas correctes. Il émet toujours un SELECTavant UPDATE, peu importe lequel des find()/ getReference()j'utilise. Ce qui est pire, SELECTtraverse les relations NON-LAZY (émission de nouvelles SELECTS), bien que je veuille simplement mettre à jour un seul champ dans une entité.
Dejan Milosevic
1
@Arthur Ronald que se passe-t-il s'il y a une annotation Version dans l'entité appelée par getReference?
David Hofmann
J'ai le même problème que @DejanMilosevic: lors de la suppression d'une entité obtenue via getReference (), un SELECT est émis sur cette entité et il traverse toutes les relations LAZY de cette entité, émettant ainsi de nombreux SELECTS (avec EclipseLink 2.5.0).
Stéphane Appercel
27

Comme je l'ai expliqué dans cet article , en supposant que vous ayez une Postentité parente et un enfant, PostCommentcomme illustré dans le diagramme suivant:

entrez la description de l'image ici

Si vous appelez findlorsque vous essayez de définir l' @ManyToOne postassociation:

PostComment comment = new PostComment();
comment.setReview("Just awesome!");

Post post = entityManager.find(Post.class, 1L);
comment.setPost(post);

entityManager.persist(comment);

Hibernate exécutera les instructions suivantes:

SELECT p.id AS id1_0_0_,
       p.title AS title2_0_0_
FROM   post p
WHERE p.id = 1

INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Just awesome!', 1)

La requête SELECT est inutile cette fois car nous n'avons pas besoin de récupérer l'entité Post. Nous voulons uniquement définir la colonne de clé étrangère post_id sous-jacente.

Maintenant, si vous utilisez à la getReferenceplace:

PostComment comment = new PostComment();
comment.setReview("Just awesome!");

Post post = entityManager.getReference(Post.class, 1L);
comment.setPost(post);

entityManager.persist(comment);

Cette fois, Hibernate émettra uniquement l'instruction INSERT:

INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Just awesome!', 1)

Contrairement à find, le getReferencene renvoie qu'une entité Proxy qui n'a que l'identifiant défini. Si vous accédez au proxy, l'instruction SQL associée sera déclenchée tant que l'EntityManager est toujours ouvert.

Cependant, dans ce cas, nous n'avons pas besoin d'accéder à l'entité Proxy. Nous voulons uniquement propager la clé étrangère à l'enregistrement de table sous-jacent, donc le chargement d'un proxy est suffisant pour ce cas d'utilisation.

Lors du chargement d'un proxy, vous devez être conscient qu'une exception LazyInitializationException peut être levée si vous essayez d'accéder à la référence du proxy après la fermeture de EntityManager. Pour plus de détails sur la gestion de LazyInitializationException, consultez cet article .

Vlad Mihalcea
la source
1
Merci Vlad de nous l'avoir fait savoir! Mais selon javadoc cela semble dérangeant: "Le moteur d'exécution du fournisseur de persistance est autorisé à lancer l'exception EntityNotFoundException lorsque getReference est appelé". Ce n'est pas possible sans SELECT (au moins pour vérifier l'existence de la ligne), n'est-ce pas? Un SELECT finalement dépend de l'implémentation.
adrhc
3
Pour le cas d'utilisation que vous avez décrit, Hibernate propose la hibernate.jpa.compliance.proxypropriété de configuration , vous pouvez donc choisir la conformité JPA ou de meilleures performances d'accès aux données.
Vlad Mihalcea
@VladMihalcea pourquoi getReferenceest nécessaire du tout s'il suffit juste de définir une nouvelle instance de modèle avec PK set. Qu'est-ce que je rate?
rilaby le
Ceci n'est pris en charge que dans Hibernarea ne vous permettra pas de charger l'association si elle est traversée.
Vlad Mihalcea le
8

Comme une référence est «gérée», mais pas hydratée, elle peut également vous permettre de supprimer une entité par ID, sans avoir besoin de la charger en mémoire au préalable.

Comme vous ne pouvez pas supprimer une entité non gérée, il est tout simplement ridicule de charger tous les champs en utilisant find (...) ou createQuery (...), uniquement pour le supprimer immédiatement.

MyLargeObject myObject = em.getReference(MyLargeObject.class, objectId);
em.remove(myObject);
Steven Spungin
la source
7

Cela me fait me demander, quand est-il conseillé d'utiliser la méthode EntityManager.getReference () au lieu de la méthode EntityManager.find ()?

EntityManager.getReference() est vraiment une méthode sujette aux erreurs et il y a vraiment très peu de cas où un code client a besoin de l'utiliser.
Personnellement, je n'ai jamais eu besoin de l'utiliser.

EntityManager.getReference () et EntityManager.find (): pas de différence en termes de surcharge

Je ne suis pas d'accord avec la réponse acceptée et en particulier:

Si j'appelle la méthode de recherche , le fournisseur JPA, dans les coulisses, appellera

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Si j'appelle la méthode getReference , le fournisseur JPA, dans les coulisses, appellera

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Ce n'est pas le comportement que j'obtiens avec Hibernate 5 et le javadoc de getReference()ne dit pas une telle chose:

Obtenez une instance dont l'état peut être récupéré paresseusement. Si l'instance demandée n'existe pas dans la base de données, l'exception EntityNotFoundException est levée lors du premier accès à l'état de l'instance. (Le moteur d'exécution du fournisseur de persistance est autorisé à lancer l'exception EntityNotFoundException lorsque getReference est appelé.) L'application ne doit pas s'attendre à ce que l'état de l'instance soit disponible lors du détachement, à moins qu'il n'ait été accédé par l'application alors que le gestionnaire d'entités était ouvert.

EntityManager.getReference() épargne une requête pour récupérer l'entité dans deux cas:

1) si l'entité est stockée dans le contexte de persistance, c'est le cache de premier niveau.
Et ce comportement n'est pas spécifique à EntityManager.getReference(), EntityManager.find() épargnera également une requête pour récupérer l'entité si l'entité est stockée dans le contexte de persistance.

Vous pouvez vérifier le premier point avec n'importe quel exemple.
Vous pouvez également vous fier à l'implémentation réelle d'Hibernate.
En effet, EntityManager.getReference()s'appuie sur la createProxyIfNecessary()méthode de la org.hibernate.event.internal.DefaultLoadEventListenerclasse pour charger l'entité.
Voici sa mise en œuvre:

private Object createProxyIfNecessary(
        final LoadEvent event,
        final EntityPersister persister,
        final EntityKey keyToLoad,
        final LoadEventListener.LoadType options,
        final PersistenceContext persistenceContext) {
    Object existing = persistenceContext.getEntity( keyToLoad );
    if ( existing != null ) {
        // return existing object or initialized proxy (unless deleted)
        if ( traceEnabled ) {
            LOG.trace( "Entity found in session cache" );
        }
        if ( options.isCheckDeleted() ) {
            EntityEntry entry = persistenceContext.getEntry( existing );
            Status status = entry.getStatus();
            if ( status == Status.DELETED || status == Status.GONE ) {
                return null;
            }
        }
        return existing;
    }
    if ( traceEnabled ) {
        LOG.trace( "Creating new proxy for entity" );
    }
    // return new uninitialized proxy
    Object proxy = persister.createProxy( event.getEntityId(), event.getSession() );
    persistenceContext.getBatchFetchQueue().addBatchLoadableEntityKey( keyToLoad );
    persistenceContext.addProxy( keyToLoad, proxy );
    return proxy;
}

La partie intéressante est:

Object existing = persistenceContext.getEntity( keyToLoad );

2) Si nous ne manipulons pas efficacement l'entité, en écho à la récupération paresseuse du javadoc.
En effet, pour assurer le chargement efficace de l'entité, il est nécessaire d'invoquer une méthode sur celle-ci.
Le gain serait donc lié à un scénario où l'on souhaite charger une entité sans avoir besoin de l'utiliser? Dans le cadre des applications, ce besoin est vraiment rare et en plus legetReference() comportement est également très trompeur si vous lisez la partie suivante.

Pourquoi privilégier EntityManager.find () à EntityManager.getReference ()

En termes de frais généraux, ce getReference()n'est pas mieux que find()comme discuté au point précédent.
Alors pourquoi utiliser l'un ou l'autre?

L'appel getReference()peut renvoyer une entité extraite paresseusement.
Ici, la récupération paresseuse ne fait pas référence aux relations de l'entité mais à l'entité elle-même.
Cela signifie que si nous invoquons getReference()et que le contexte de persistance est fermé, l'entité peut ne jamais être chargée et le résultat est donc vraiment imprévisible. Par exemple, si l'objet proxy est sérialisé, vous pouvez obtenir une nullréférence en tant que résultat sérialisé ou si une méthode est appelée sur l'objet proxy, une exception telle que LazyInitializationExceptionlevée.

Cela signifie que le rejet de EntityNotFoundExceptioncela est la principale raison à utiliser getReference()pour gérer une instance qui n'existe pas dans la base de données, car une situation d'erreur peut ne jamais être effectuée alors que l'entité n'existe pas.

EntityManager.find()n'a pas l'ambition de jeter EntityNotFoundExceptionsi l'entité n'est pas trouvée. Son comportement est à la fois simple et clair. Vous n'aurez jamais de surprise car elle renvoie toujours une entité chargée ou null(si l'entité n'est pas trouvée) mais jamais une entité sous la forme d'un proxy qui risque de ne pas être chargée efficacement.
Il EntityManager.find()devrait donc être favorisé dans la plupart des cas.

davidxxx
la source
Votre raison est trompeuse par rapport à la réponse acceptée + la réponse de Vlad Mihalcea + mon commentaire à Vlad Mihalcea (peut-être un moins important ce dernier +).
adrhc
1
Pro JPA2 déclare: "Etant donné la situation très spécifique dans laquelle getReference () peut être utilisé, find () devrait être utilisé dans pratiquement tous les cas".
JL_SO
Donnez un vote positif à cette question car c'est un complément nécessaire à la réponse acceptée et parce que mes tests ont montré que, lors de la définition d'une propriété du proxy d'entité, elle est extraite de la base de données, contrairement à ce que dit la réponse acceptée. Seul le cas présenté par Vlad a passé mes tests.
João Fé
2

Je ne suis pas d'accord avec la réponse sélectionnée, et comme davidxxx l'a correctement souligné, getReference ne fournit pas un tel comportement de mises à jour dynamiques sans select. J'ai posé une question concernant la validité de cette réponse, voir ici - ne peut pas mettre à jour sans émettre select on using setter après getReference () of hibernate JPA .

Honnêtement, je n'ai vu personne qui ait réellement utilisé cette fonctionnalité. NULLE PART. Et je ne comprends pas pourquoi il est si voté.

Maintenant tout d'abord, peu importe ce que vous appelez sur un objet proxy d'hibernation, un setter ou un getter, un SQL est déclenché et l'objet est chargé.

Mais alors j'ai pensé, alors que se passe-t-il si le proxy JPA getReference () ne fournit pas cette fonctionnalité. Je peux juste écrire mon propre proxy.

Maintenant, nous pouvons tous affirmer que les sélections sur les clés primaires sont aussi rapides qu'une requête peut être obtenue et que ce n'est pas vraiment quelque chose à éviter. Mais pour ceux d'entre nous qui ne peuvent pas le gérer pour une raison ou une autre, vous trouverez ci-dessous une implémentation d'un tel proxy. Mais avant de voir l'implémentation, voyez son utilisation et sa simplicité d'utilisation.

USAGE

Order example = ProxyHandler.getReference(Order.class, 3);
example.setType("ABCD");
example.setCost(10);
PersistenceService.save(example);

Et cela déclencherait la requête suivante -

UPDATE Order SET type = 'ABCD' and cost = 10 WHERE id = 3;

et même si vous souhaitez insérer, vous pouvez toujours faire PersistenceService.save (new Order ("a", 2)); et il déclencherait un insert comme il se doit.

LA MISE EN OEUVRE

Ajoutez ceci à votre pom.xml -

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.10</version>
</dependency>

Faites de cette classe pour créer un proxy dynamique -

@SuppressWarnings("unchecked")
public class ProxyHandler {

public static <T> T getReference(Class<T> classType, Object id) {
    if (!classType.isAnnotationPresent(Entity.class)) {
        throw new ProxyInstantiationException("This is not an entity!");
    }

    try {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(classType);
        enhancer.setCallback(new ProxyMethodInterceptor(classType, id));
        enhancer.setInterfaces((new Class<?>[]{EnhancedProxy.class}));
        return (T) enhancer.create();
    } catch (Exception e) {
        throw new ProxyInstantiationException("Error creating proxy, cause :" + e.getCause());
    }
}

Créez une interface avec toutes les méthodes -

public interface EnhancedProxy {
    public String getJPQLUpdate();
    public HashMap<String, Object> getModifiedFields();
}

Maintenant, faites un intercepteur qui vous permettra d'implémenter ces méthodes sur votre proxy -

import com.anil.app.exception.ProxyInstantiationException;
import javafx.util.Pair;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import javax.persistence.Id;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
/**
* @author Anil Kumar
*/
public class ProxyMethodInterceptor implements MethodInterceptor, EnhancedProxy {

private Object target;
private Object proxy;
private Class classType;
private Pair<String, Object> primaryKey;
private static HashSet<String> enhancedMethods;

ProxyMethodInterceptor(Class classType, Object id) throws IllegalAccessException, InstantiationException {
    this.classType = classType;
    this.target = classType.newInstance();
    this.primaryKey = new Pair<>(getPrimaryKeyField().getName(), id);
}

static {
    enhancedMethods = new HashSet<>();
    for (Method method : EnhancedProxy.class.getDeclaredMethods()) {
        enhancedMethods.add(method.getName());
    }
}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    //intercept enhanced methods
    if (enhancedMethods.contains(method.getName())) {
        this.proxy = obj;
        return method.invoke(this, args);
    }
    //else invoke super class method
    else
        return proxy.invokeSuper(obj, args);
}

@Override
public HashMap<String, Object> getModifiedFields() {
    HashMap<String, Object> modifiedFields = new HashMap<>();
    try {
        for (Field field : classType.getDeclaredFields()) {

            field.setAccessible(true);

            Object initialValue = field.get(target);
            Object finalValue = field.get(proxy);

            //put if modified
            if (!Objects.equals(initialValue, finalValue)) {
                modifiedFields.put(field.getName(), finalValue);
            }
        }
    } catch (Exception e) {
        return null;
    }
    return modifiedFields;
}

@Override
public String getJPQLUpdate() {
    HashMap<String, Object> modifiedFields = getModifiedFields();
    if (modifiedFields == null || modifiedFields.isEmpty()) {
        return null;
    }
    StringBuilder fieldsToSet = new StringBuilder();
    for (String field : modifiedFields.keySet()) {
        fieldsToSet.append(field).append(" = :").append(field).append(" and ");
    }
    fieldsToSet.setLength(fieldsToSet.length() - 4);
    return "UPDATE "
            + classType.getSimpleName()
            + " SET "
            + fieldsToSet
            + "WHERE "
            + primaryKey.getKey() + " = " + primaryKey.getValue();
}

private Field getPrimaryKeyField() throws ProxyInstantiationException {
    for (Field field : classType.getDeclaredFields()) {
        field.setAccessible(true);
        if (field.isAnnotationPresent(Id.class))
            return field;
    }
    throw new ProxyInstantiationException("Entity class doesn't have a primary key!");
}
}

Et la classe d'exception -

public class ProxyInstantiationException extends RuntimeException {
public ProxyInstantiationException(String message) {
    super(message);
}

Un service à sauvegarder en utilisant ce proxy -

@Service
public class PersistenceService {

@PersistenceContext
private EntityManager em;

@Transactional
private void save(Object entity) {
    // update entity for proxies
    if (entity instanceof EnhancedProxy) {
        EnhancedProxy proxy = (EnhancedProxy) entity;
        Query updateQuery = em.createQuery(proxy.getJPQLUpdate());
        for (Entry<String, Object> entry : proxy.getModifiedFields().entrySet()) {
            updateQuery.setParameter(entry.getKey(), entry.getValue());
        }
        updateQuery.executeUpdate();
    // insert otherwise
    } else {
        em.persist(entity);
    }

}
}
Anil kumar
la source