Quando utilizzare EntityManager.find () rispetto a EntityManager.getReference () con JPA


103

Mi sono imbattuto in una situazione (che penso sia strana ma forse abbastanza normale) in cui utilizzo EntityManager.getReference (LObj.getClass (), LObj.getId ()) per ottenere un'entità di database e poi passare l'oggetto restituito a essere persistito in un'altra tabella.

Quindi fondamentalmente il flusso era così:

class TFacade {

  createT (FObj, AObj) {
    T TObj = nuovo 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 = nuovo FLH ();
    FLHObj.setF (fobj);
    FLHObj.setL (LObj);
    ....
    EntityManager.persist (FLHObj);
    ...
  }
}

Ricevo la seguente eccezione "java.lang.IllegalArgumentException: entità sconosciuta: com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0"

Dopo averlo esaminato per un po ', ho finalmente capito che era perché stavo usando il metodo EntityManager.getReference () che stavo ottenendo l'eccezione sopra poiché il metodo stava restituendo un proxy.

Questo mi fa chiedere, quando è consigliabile utilizzare il metodo EntityManager.getReference () invece del metodo EntityManager.find () ?

EntityManager.getReference () genera un'EntityNotFoundException se non riesce a trovare l'entità cercata, il che è molto conveniente di per sé. Il metodo EntityManager.find () restituisce semplicemente null se non riesce a trovare l'entità.

Per quanto riguarda i confini delle transazioni, mi sembra che dovresti usare il metodo find () prima di passare l'entità appena trovata a una nuova transazione. Se usi il metodo getReference (), probabilmente ti ritroverai in una situazione simile alla mia con l'eccezione precedente.


Ho dimenticato di menzionare, sto usando Hibernate come provider JPA.
SibzTer

Risposte:


152

Di solito utilizzo il metodo getReference quando non ho bisogno di accedere allo stato del database (intendo il metodo getter). Solo per cambiare stato (intendo il metodo setter). Come dovresti sapere, getReference restituisce un oggetto proxy che utilizza una potente funzionalità chiamata controllo sporco automatico. Supponiamo quanto segue

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

}

Se chiamo il metodo find , il provider JPA, dietro le quinte, chiamerà

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Se chiamo il metodo getReference , il provider JPA, dietro le quinte, chiamerà

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

E sai perché ???

Quando chiami getReference, otterrai un oggetto proxy. Qualcosa di simile (il provider JPA si occupa di implementare questo 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;
    }

}

Quindi, prima del commit della transazione, il provider JPA vedrà il flag stateChanged per aggiornare O NON l'entità persona. Se nessuna riga viene aggiornata dopo l'istruzione di aggiornamento, il provider JPA genererà EntityNotFoundException in base alla specifica JPA.

Saluti,


4
Sto usando EclipseLink 2.5.0 e le query sopra indicate non sono corrette. Emette sempre una SELECTprima UPDATE, indipendentemente da quale find()/ getReference()io uso. Quel che è peggio, SELECTattraversa le relazioni NON-LAZY (emettendo nuove SELECTS), anche se voglio solo aggiornare un singolo campo in un'entità.
Dejan Milosevic

1
@Arthur Ronald cosa succede se c'è un'annotazione di versione nell'entità chiamata da getReference?
David Hofmann

Ho lo stesso problema di @DejanMilosevic: quando rimuovi un'entità ottenuta tramite getReference (), viene emesso un SELECT su quell'entità e attraversa tutte le relazioni LAZY di quell'entità, emettendo così molti SELECTS (con EclipseLink 2.5.0).
Stéphane Appercel

27

Come ho spiegato in questo articolo , supponendo che tu abbia Postun'entità genitore e un figlio PostCommentcome illustrato nel diagramma seguente:

inserisci qui la descrizione dell'immagine

Se chiami findquando provi a impostare l' @ManyToOne postassociazione:

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

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

entityManager.persist(comment);

Hibernate eseguirà le seguenti istruzioni:

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 query SELECT è inutile questa volta perché non è necessario che l'entità Post venga recuperata. Vogliamo solo impostare la colonna della chiave esterna post_id sottostante.

Ora, se usi getReferenceinvece:

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

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

entityManager.persist(comment);

Questa volta, Hibernate emetterà solo l'istruzione INSERT:

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

A differenza find, getReferencerestituisce solo un'entità Proxy che ha solo l'identificatore impostato. Se si accede al proxy, l'istruzione SQL associata verrà attivata finché EntityManager è ancora aperto.

Tuttavia, in questo caso, non è necessario accedere all'entità Proxy. Vogliamo solo propagare la chiave esterna al record della tabella sottostante, quindi il caricamento di un proxy è sufficiente per questo caso d'uso.

Quando si carica un proxy, è necessario tenere presente che è possibile generare un'eccezione LazyInitializationException se si tenta di accedere al riferimento proxy dopo la chiusura di EntityManager. Per maggiori dettagli sulla gestione del LazyInitializationException, consulta questo articolo .


1
Grazie Vlad per averci fatto sapere questo! Ma secondo javadoc questo sembra inquietante: "Il runtime del provider di persistenza è autorizzato a lanciare EntityNotFoundException quando viene chiamato getReference". Ciò non è possibile senza SELECT (almeno per verificare l'esistenza della riga), vero? Quindi un eventuale SELECT dipende dall'implementazione.
adrhc

3
Per il caso d'uso che hai descritto, Hibernate offre la hibernate.jpa.compliance.proxyproprietà di configurazione , quindi puoi scegliere la conformità JPA o migliori prestazioni di accesso ai dati.
Vlad Mihalcea

@VladMihalcea why getReferenceè assolutamente necessario se è appena sufficiente per impostare una nuova istanza di modello con PK set. Cosa mi sto perdendo?
rilaby

Questo è supportato solo in Hibernarea non ti permetterà di caricare l'associazione se attraversata.
Vlad Mihalcea

8

Poiché un riferimento è "gestito", ma non idratato, può anche consentire di rimuovere un'entità per ID, senza bisogno di caricarla prima in memoria.

Poiché non è possibile rimuovere un'entità non gestita, è semplicemente sciocco caricare tutti i campi utilizzando find (...) o createQuery (...), solo per eliminarlo immediatamente.

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

7

Questo mi fa chiedere, quando è consigliabile utilizzare il metodo EntityManager.getReference () invece del metodo EntityManager.find ()?

EntityManager.getReference()è davvero un metodo soggetto a errori e ci sono davvero pochissimi casi in cui un codice client deve usarlo.
Personalmente, non ho mai avuto bisogno di usarlo.

EntityManager.getReference () e EntityManager.find (): nessuna differenza in termini di overhead

Non sono d'accordo con la risposta accettata e in particolare:

Se chiamo il metodo find , il provider JPA, dietro le quinte, chiamerà

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Se chiamo il metodo getReference , il provider JPA, dietro le quinte, chiamerà

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Non è il comportamento che ottengo con Hibernate 5 e il javadoc di getReference()non dice una cosa del genere:

Ottieni un'istanza, il cui stato può essere recuperato pigramente. Se l'istanza richiesta non esiste nel database, EntityNotFoundException viene generata quando si accede per la prima volta allo stato dell'istanza. (Il runtime del provider di persistenza è autorizzato a lanciare EntityNotFoundException quando viene chiamato getReference.) L'applicazione non dovrebbe aspettarsi che lo stato dell'istanza sarà disponibile al momento del distacco, a meno che non sia stato acceduto dall'applicazione mentre il gestore entità era aperto.

EntityManager.getReference() risparmia una query per recuperare l'entità in due casi:

1) se l'entità è archiviata nel contesto Persistence, quella è la cache di primo livello.
E questo comportamento non è specifico per EntityManager.getReference(), EntityManager.find() risparmierà anche una query per recuperare l'entità se l'entità è archiviata nel contesto Persistence.

Puoi controllare il primo punto con qualsiasi esempio.
Puoi anche fare affidamento sull'attuale implementazione di Hibernate.
Infatti, EntityManager.getReference()si affida al createProxyIfNecessary()metodo della org.hibernate.event.internal.DefaultLoadEventListenerclasse per caricare l'entità.
Ecco la sua implementazione:

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 parte interessante è:

Object existing = persistenceContext.getEntity( keyToLoad );

2) Se non manipoliamo efficacemente l'entità, facendo eco al pigramente recuperato del javadoc.
Infatti, per garantire l'effettivo caricamento dell'entità, è necessario invocare un metodo su di essa.
Quindi il guadagno sarebbe correlato a uno scenario in cui vogliamo caricare un'entità senza avere la necessità di usarla? Nel quadro delle applicazioni, questa esigenza è davvero rara e inoltre ilgetReference() comportamento è anche molto fuorviante se leggi la parte successiva.

Perché preferire EntityManager.find () su EntityManager.getReference ()

In termini di overhead, getReference()non è migliore di find()quanto discusso nel punto precedente.
Allora perché usare l'uno o l'altro?

L'invocazione getReference()può restituire un'entità recuperata pigramente.
Qui, il recupero pigro non si riferisce alle relazioni dell'entità ma all'entità stessa.
Significa che se invochiamo getReference()e poi il contesto Persistence è chiuso, l'entità potrebbe non essere mai caricata e quindi il risultato è davvero imprevedibile. Ad esempio, se l'oggetto proxy è serializzato, è possibile ottenere un nullriferimento come risultato serializzato o se un metodo viene richiamato sull'oggetto proxy, un'eccezione comeLazyInitializationException viene generata .

Significa che il lancio di EntityNotFoundExceptionquesto è il motivo principale per utilizzaregetReference() per gestire un'istanza che non esiste nel database poiché una situazione di errore potrebbe non essere mai eseguita mentre l'entità non esiste.

EntityManager.find()non ha l'ambizione di lanciare EntityNotFoundExceptionse l'entità non viene trovata. Il suo comportamento è sia semplice che chiaro. Non avrai mai sorprese in quanto restituisce sempre un'entità caricata o null(se l'entità non viene trovata) ma mai un'entità sotto forma di proxy che potrebbe non essere caricata efficacemente.
Quindi EntityManager.find()dovrebbe essere favorito nella maggior parte dei casi.


La tua ragione è fuorviante se confrontata con la risposta accettata + la risposta di Vlad Mihalcea + il mio commento a Vlad Mihalcea (forse un meno importante quest'ultimo +).
adrhc

1
Pro JPA2 afferma: "Data la situazione molto specifica in cui può essere utilizzato getReference (), find () dovrebbe essere utilizzato praticamente in tutti i casi".
JL_SO

Vota questa domanda perché è un complemento necessario alla risposta accettata e perché i miei test hanno mostrato che, quando si imposta una proprietà del proxy di entità, viene recuperata dal database, contrariamente a quanto dice la risposta accettata. Solo il caso dichiarato da Vlad ha superato i miei test.
João Fé,

2

Non sono d'accordo con la risposta selezionata e, come ha correttamente sottolineato davidxxx, getReference non fornisce tale comportamento degli aggiornamenti dinamici senza selezione. Ho posto una domanda riguardante la validità di questa risposta, vedi qui - non posso aggiornare senza emettere select sull'uso di setter dopo getReference () di hibernate JPA .

Onestamente non ho visto nessuno che abbia effettivamente utilizzato quella funzionalità. DOVUNQUE. E non capisco perché sia ​​così votato.

Ora prima di tutto, non importa quello che chiami su un oggetto proxy di ibernazione, un setter o un getter, un SQL viene attivato e l'oggetto viene caricato.

Ma poi ho pensato, quindi cosa succede se il proxy getReference () di JPA non fornisce quella funzionalità. Posso semplicemente scrivere il mio proxy.

Ora, possiamo tutti sostenere che le selezioni sulle chiavi primarie sono veloci quanto una query può ottenere e non è davvero qualcosa da evitare. Ma per quelli di noi che non possono gestirlo per un motivo o per l'altro, di seguito è riportata un'implementazione di tale proxy. Ma prima di vedere l'implementazione, guarda il suo utilizzo e quanto è semplice da usare.

USO

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

E questo attiverà la seguente query:

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

e anche se vuoi inserire, puoi ancora fare PersistenceService.save (new Order ("a", 2)); e sparerebbe un inserto come dovrebbe.

IMPLEMENTAZIONE

Aggiungi questo al tuo pom.xml -

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

Crea questa classe per creare un proxy dinamico -

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

Crea un'interfaccia con tutti i metodi -

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

Ora, crea un intercettore che ti permetta di implementare questi metodi sul tuo 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!");
}
}

E la classe di eccezione -

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

Un servizio per salvare utilizzando questo 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);
    }

}
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.