web-dev-qa-db-fra.com

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

J'ai rencontré une situation (que je trouve étrange mais peut-être tout à fait normale) où j'utilise EntityManager.getReference (LObj.getClass (), LObj.getId ()) pour obtenir une entité de base de données, puis passe l'objet renvoyé à être persisté dans une autre table.

Donc, fondamentalement, le flux était comme ça:

 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 
 Class FFacade {

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

 @ TransactionAttributeType.REQUIRED 
 Class FLHFacade {

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

Je recevais l'exception suivante "Java.lang.IllegalArgumentException: entité inconnue: com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0"

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

Cela me fait penser, quand est-il conseillé d'utiliser la méthode EntityManager.getReference () à la place de la méthode EntityManager.find ()

EntityManager.getReference () lève une exception EntityNotFoundException si elle 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 peut pas trouver l'entité.

En ce qui concerne les limites des transactions, il me semble que vous devriez utiliser la méthode find () avant de transmettre la nouvelle entité à 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.

91
SibzTer

J'utilise habituellement la méthode getReference lorsque je n'ai pas besoin d'accéder à l'état de la base de données (je parle de la méthode getter). Juste pour changer d'état (je veux dire méthode setter). Comme vous devez le savoir, getReference renvoie un objet proxy qui utilise une fonctionnalité puissante appelée vérification automatique. 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 find method, le fournisseur JPA, en coulisse, appellera

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Si j'appelle getReference method, le fournisseur JPA, en coulisse, appellera

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Et tu sais pourquoi ???

Lorsque vous appelez getReference, vous obtenez 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 l'entité OR PAS. Si aucune ligne n'est mise à jour après l'instruction de mise à jour, le fournisseur JPA lève une exception EntityNotFoundException conformément à la spécification JPA.

cordialement,

137
Arthur Ronald

Dans la mesure où une référence est «gérée», mais non hydratée, elle peut également vous permettre de supprimer une entité par ID sans avoir à 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 à l'aide de find (...) ou de createQuery (...), puis de le supprimer immédiatement.

MyLargeObject myObject = em.getReference(MyLargeObject.class, objectId);
em.remove(myObject);
8
Steven Spungin

Comme je l'ai expliqué dans cet article , en supposant que vous avez une entité parent Post et un enfant PostComment, comme l'illustre le diagramme suivant:

 enter image description here

Si vous appelez find lorsque vous essayez de définir l'association @ManyToOnepost:

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 que l’entité Post soit récupérée. Nous voulons uniquement définir la colonne de clé étrangère post_id sous-jacente.

Maintenant, si vous utilisez getReference à la place:

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

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

entityManager.persist(comment);

Cette fois, Hibernate n’émettra que la déclaration INSERT:

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

Contrairement à find, la getReference renvoie uniquement un proxy d'entité dont l'identifiant est uniquement défini. Si vous accédez au proxy, l'instruction SQL associée sera déclenchée tant que EntityManager est toujours ouvert.

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

Lors du chargement d'un proxy, vous devez savoir 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 manipulation de LazyInitializationException, consultez cet article .

5
Vlad Mihalcea

Cela me fait me demander, quand est-il conseillé d'utiliser le Méthode EntityManager.getReference () à la place de 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 doit l'utiliser.
Personnellement, je n'ai jamais eu besoin de l'utiliser. 

EntityManager.getReference () et EntityManager.find (): pas de différence en termes de temps système

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

Si j'appelle find method, le fournisseur JPA, en coulisse, appellera

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Si j'appelle getReference method, le fournisseur JPA, en coulisse, le sera appel

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Ce n’est pas le comportement que j’ai avec Hibernate 5 et le javadoc de getReference() ne dit pas une chose pareille: 

Obtenez une instance dont l'état peut être récupéré paresseusement. Si le demandé instance n'existe pas dans la base de données, l'entité EntityNotFoundException est levé lors du premier accès à l'état de l'instance. (Le moteur d'exécution de la persistance Fournisseur est autorisé à lancer l'entité EntityNotFoundException Lorsque getReference est appelée.) L'application ne doit pas s'attendre à ce que l'état d'instance sera disponible lors du détachement, à moins qu'il ne s'agisse de accessible 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-à-dire le cache de premier niveau.
Et ce comportement n'est pas spécifique à EntityManager.getReference(), EntityManager.find() épargnera également une requête pour extraire 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 compter sur l’implémentation d’Hibernate.
En effet, EntityManager.getReference() s'appuie sur la méthode createProxyIfNecessary() de la classe org.hibernate.event.internal.DefaultLoadEventListener pour charger l'entité.
Voici sa mise en oeuvre:

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é, faisant écho au récupéré paresseusement du javadoc.
En effet, pour assurer le chargement effectif de l’entité, il est nécessaire d’appeler une méthode dessus.
Le gain serait donc lié à un scénario dans lequel nous souhaitons charger une entité sans avoir à l'utiliser. Dans le cadre des applications, ce besoin est vraiment rare et, de plus, le comportement de getReference() est également très trompeur si vous lisez la partie suivante. 

Pourquoi préférer EntityManager.find () à EntityManager.getReference () 

En termes de frais généraux, getReference() n'est pas meilleur que find() comme indiqué dans le point précédent.
Alors pourquoi utiliser l’un ou l’autre? 

Invoquer 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 appelons getReference() et que le contexte de persistance est fermé, l'entité peut ne jamais être chargée et le résultat est réellement imprévisible. Par exemple, si l'objet proxy est sérialisé, vous pouvez obtenir une référence null sous la forme d'un résultat sérialisé ou si une méthode est appelée sur l'objet proxy, une exception telle que LazyInitializationException est levée. 

Cela signifie que la valeur de EntityNotFoundException qui est la principale raison d'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 exécutée tant que l'entité n'existe pas. 

EntityManager.find() n'a pas l'ambition de lancer EntityNotFoundException si l'entité n'est pas trouvée. Son comportement est à la fois simple et clair. Vous n'aurez jamais de surprise car il retourne 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 peut ne pas être chargé efficacement.
Donc, EntityManager.find() devrait être privilégié dans la plupart des cas. 

3
davidxxx

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

En toute honnêteté, je n'ai vu personne utiliser cette fonctionnalité. NULLE PART. Et je ne comprends pas pourquoi il est si élevé.

Tout d’abord, peu importe ce que vous appelez un objet proxy hibernate, un séparateur ou un getter, un SQL est déclenché et l’objet est chargé.

Mais j’ai alors pensé que le proxy JPA getReference () ne fournirait pas cette fonctionnalité. Je peux juste écrire mon propre proxy.

Nous pouvons tous maintenant affirmer que les sélections sur des clés primaires sont aussi rapides qu'une requête et que ce n'est pas vraiment une chose à éviter. Mais pour ceux d'entre nous qui ne peuvent pas le gérer pour une raison ou une autre, voici une implémentation d'un tel proxy. Mais avant de voir l'implémentation, voir 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 voulez insérer, vous pouvez toujours faire PersistenceService.save (new Order ("a", 2)); et il tirerait 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());
    }
}

Faire une interface avec toutes les méthodes - 

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

Maintenant, fabriquez 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);
    }

}
}
0
Anil Kumar