Wann wird EntityManager.find () vs EntityManager.getReference () mit JPA verwendet?

103

Ich bin auf eine Situation gestoßen (die ich für seltsam halte, die aber möglicherweise ganz normal ist), in der ich EntityManager.getReference (LObj.getClass (), LObj.getId ()) verwende, um eine Datenbankentität abzurufen und das zurückgegebene Objekt an zu übergeben in einer anderen Tabelle bestehen bleiben.

Im Grunde war der Fluss also so:

Klasse TFacade {

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

@ TransactionAttributeType.REQUIRES_NEW
Klasse FFacade {

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

@ TransactionAttributeType.REQUIRED
Klasse FLHFacade {

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

Ich habe die folgende Ausnahme "java.lang.IllegalArgumentException: Unbekannte Entität: com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0" erhalten.

Nachdem ich mich eine Weile damit befasst hatte, stellte ich schließlich fest, dass ich aufgrund der Verwendung der EntityManager.getReference () -Methode die obige Ausnahme erhielt, da die Methode einen Proxy zurückgab.

Ich frage mich daher, wann es ratsam ist, die EntityManager.getReference () -Methode anstelle der EntityManager.find () -Methode zu verwenden .

EntityManager.getReference () löst eine EntityNotFoundException aus, wenn die gesuchte Entität nicht gefunden werden kann, was an sich sehr praktisch ist. Die EntityManager.find () -Methode gibt lediglich null zurück, wenn die Entität nicht gefunden werden kann.

In Bezug auf Transaktionsgrenzen klingt es für mich so, als müssten Sie die find () -Methode verwenden, bevor Sie die neu gefundene Entität an eine neue Transaktion übergeben. Wenn Sie die Methode getReference () verwenden, werden Sie wahrscheinlich mit der obigen Ausnahme in einer ähnlichen Situation wie ich enden.

SibzTer
quelle
Ich habe vergessen zu erwähnen, dass ich Hibernate als JPA-Anbieter verwende.
SibzTer

Antworten:

152

Normalerweise verwende ich die getReference- Methode, wenn ich nicht auf den Datenbankstatus zugreifen muss (ich meine die Getter-Methode). Nur um den Zustand zu ändern (ich meine Setter-Methode). Wie Sie wissen sollten, gibt getReference ein Proxy-Objekt zurück, das eine leistungsstarke Funktion namens automatische Dirty Checking verwendet. Angenommen, das Folgende

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

}

Wenn ich die Methode find aufrufe, ruft der JPA-Anbieter hinter den Kulissen auf

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Wenn ich die Methode getReference aufrufe , ruft der JPA-Anbieter hinter den Kulissen auf

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Und du weißt warum ???

Wenn Sie getReference aufrufen, erhalten Sie ein Proxy-Objekt. So etwas wie dieses (JPA-Anbieter kümmert sich um die Implementierung dieses Proxys)

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

}

Vor dem Festschreiben der Transaktion wird dem JPA-Anbieter das Flag stateChanged angezeigt, um die Entität OR NOT person zu aktualisieren. Wenn nach der Update-Anweisung keine Zeilen aktualisiert werden, löst der JPA-Anbieter EntityNotFoundException gemäß der JPA-Spezifikation aus.

Grüße,

Arthur Ronald
quelle
4
Ich verwende EclipseLink 2.5.0 und die oben angegebenen Abfragen sind nicht korrekt. Es gibt immer ein SELECTVorher aus UPDATE, egal welches von find()/ getReference()ich benutze. Was noch schlimmer ist, SELECTdurchläuft NON-LAZY-Beziehungen (Ausgabe neuer SELECTS), obwohl ich nur ein einzelnes Feld in einer Entität aktualisieren möchte.
Dejan Milosevic
1
@Arthur Ronald Was passiert, wenn die von getReference aufgerufene Entität eine Versionsanmerkung enthält?
David Hofmann
Ich habe das gleiche Problem wie @DejanMilosevic: Wenn Sie eine über getReference () erhaltene Entität entfernen, wird eine SELECT für diese Entität ausgegeben, die alle LAZY-Beziehungen dieser Entität durchläuft und somit viele SELECTS (mit EclipseLink 2.5.0) ausgibt.
Stéphane Appercel
27

Wie in diesem Artikel erläutert , wird davon ausgegangen, dass Sie eine übergeordnete PostEntität und ein untergeordnetes Element haben, PostCommentwie in der folgenden Abbildung dargestellt:

Geben Sie hier die Bildbeschreibung ein

Wenn Sie anrufen, findwenn Sie versuchen, die @ManyToOne postZuordnung festzulegen:

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

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

entityManager.persist(comment);

Hibernate führt die folgenden Anweisungen aus:

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)

Die SELECT-Abfrage ist diesmal nutzlos, da die Post-Entität nicht abgerufen werden muss. Wir möchten nur die zugrunde liegende Spalte post_id Foreign Key festlegen.

Wenn Sie getReferencestattdessen Folgendes verwenden :

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

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

entityManager.persist(comment);

Dieses Mal gibt Hibernate nur die INSERT-Anweisung aus:

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

Im Gegensatz dazu findgibt der getReferenceeinzige einen Entitätsproxy zurück, für den nur der Bezeichner festgelegt ist. Wenn Sie auf den Proxy zugreifen, wird die zugehörige SQL-Anweisung ausgelöst, solange der EntityManager noch geöffnet ist.

In diesem Fall müssen wir jedoch nicht auf den Entitätsproxy zugreifen. Wir möchten den Fremdschlüssel nur an den zugrunde liegenden Tabellendatensatz weitergeben, sodass das Laden eines Proxys für diesen Anwendungsfall ausreicht.

Beim Laden eines Proxys müssen Sie beachten, dass eine LazyInitializationException ausgelöst werden kann, wenn Sie versuchen, auf die Proxy-Referenz zuzugreifen, nachdem der EntityManager geschlossen wurde. Weitere Informationen zum Umgang LazyInitializationExceptionmit finden Sie in diesem Artikel .

Vlad Mihalcea
quelle
1
Danke Vlad, dass du uns das mitgeteilt hast! Laut javadoc scheint dies jedoch beunruhigend: "Die Laufzeit des Persistenzanbieters darf die EntityNotFoundException auslösen, wenn getReference aufgerufen wird." Dies ist ohne SELECT nicht möglich (zumindest zur Überprüfung der Zeilenexistenz), oder? Ein eventuelles SELECT hängt also von der Implementierung ab.
Adrrc
3
Für den von Ihnen beschriebenen Anwendungsfall bietet Hibernate die hibernate.jpa.compliance.proxyKonfigurationseigenschaft , sodass Sie entweder die JPA-Konformität oder eine bessere Datenzugriffsleistung auswählen können.
Vlad Mihalcea
@VladMihalcea warum getReferencewird überhaupt benötigt, wenn es gerade ausreicht, eine neue Modellinstanz mit gesetztem PK zu setzen. Was vermisse ich?
Rilaby
Dies wird nur in Hibernarea unterstützt. Wenn Sie die Zuordnung durchlaufen, können Sie die Zuordnung nicht laden.
Vlad Mihalcea
8

Da eine Referenz "verwaltet", aber nicht hydratisiert ist, können Sie eine Entität auch nach ID entfernen, ohne sie zuerst in den Speicher laden zu müssen.

Da Sie eine nicht verwaltete Entität nicht entfernen können, ist es einfach albern, alle Felder mit find (...) oder createQuery (...) zu laden, um sie dann sofort zu löschen.

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

Ich frage mich daher, wann es ratsam ist, die EntityManager.getReference () -Methode anstelle der EntityManager.find () -Methode zu verwenden.

EntityManager.getReference()ist wirklich eine fehleranfällige Methode und es gibt wirklich sehr wenige Fälle, in denen ein Client-Code sie verwenden muss.
Persönlich musste ich es nie benutzen.

EntityManager.getReference () und EntityManager.find (): Kein Unterschied in Bezug auf den Overhead

Ich bin mit der akzeptierten Antwort nicht einverstanden und insbesondere:

Wenn ich die Methode find aufrufe, ruft der JPA-Anbieter hinter den Kulissen auf

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Wenn ich die Methode getReference aufrufe , ruft der JPA-Anbieter hinter den Kulissen auf

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Es ist nicht das Verhalten, das ich mit Hibernate 5 bekomme, und der Javadoc von getReference()sagt so etwas nicht:

Holen Sie sich eine Instanz, deren Status möglicherweise träge abgerufen wird. Wenn die angeforderte Instanz nicht in der Datenbank vorhanden ist, wird beim ersten Zugriff auf den Instanzstatus die EntityNotFoundException ausgelöst. (Die Laufzeit des Persistenzanbieters darf die EntityNotFoundException auslösen, wenn getReference aufgerufen wird.) Die Anwendung sollte nicht erwarten, dass der Instanzstatus beim Trennen verfügbar ist, es sei denn, die Anwendung hat auf ihn zugegriffen, während der Entitätsmanager geöffnet war.

EntityManager.getReference() spart eine Abfrage, um die Entität in zwei Fällen abzurufen:

1) Wenn die Entität im Persistenzkontext gespeichert ist, ist dies der Cache der ersten Ebene.
Und dieses Verhalten ist nicht spezifisch für EntityManager.getReference(), EntityManager.find()erspart auch eine Abfrage zum Abrufen der Entität, wenn die Entität im Persistenzkontext gespeichert ist.

Sie können den ersten Punkt mit einem beliebigen Beispiel überprüfen.
Sie können sich auch auf die eigentliche Hibernate-Implementierung verlassen.
Verlässt EntityManager.getReference()sich in der Tat auf die createProxyIfNecessary()Methode der org.hibernate.event.internal.DefaultLoadEventListenerKlasse, um die Entität zu laden.
Hier ist seine Implementierung:

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

Der interessante Teil ist:

Object existing = persistenceContext.getEntity( keyToLoad );

2) Wenn wir die Entität nicht effektiv manipulieren, wird das träge Abrufen des Javadoc wiederholt .
Um das effektive Laden der Entität sicherzustellen, muss eine Methode aufgerufen werden.
Der Gewinn würde also mit einem Szenario zusammenhängen, in dem wir eine Entität laden möchten, ohne sie verwenden zu müssen? Im Rahmen von Anwendungen ist dieser Bedarf wirklich ungewöhnlich und außerdem ist das getReference()Verhalten sehr irreführend, wenn Sie den nächsten Teil lesen.

Warum sollten Sie EntityManager.find () gegenüber EntityManager.getReference () bevorzugen?

In Bezug auf den Overhead getReference()ist nicht besser als find()wie im vorherigen Punkt diskutiert.
Warum also den einen oder anderen benutzen?

Das Aufrufen getReference()kann eine träge abgerufene Entität zurückgeben.
Hier bezieht sich das verzögerte Abrufen nicht auf Beziehungen der Entität, sondern auf die Entität selbst. Dies
bedeutet, dass wenn wir aufrufen getReference()und dann der Persistenzkontext geschlossen wird, die Entität möglicherweise nie geladen wird und das Ergebnis daher wirklich unvorhersehbar ist. Wenn das Proxy-Objekt beispielsweise serialisiert ist, können Sie eine nullReferenz als serialisiertes Ergebnis erhalten, oder wenn eine Methode für das Proxy-Objekt aufgerufen wird, wird eine Ausnahme LazyInitializationExceptionausgelöst.

Dies bedeutet, dass der Wurf davon EntityNotFoundExceptionder Hauptgrund für die getReference()Behandlung einer Instanz ist, die nicht in der Datenbank vorhanden ist, da eine Fehlersituation möglicherweise niemals ausgeführt wird, solange die Entität nicht vorhanden ist.

EntityManager.find()hat nicht den Ehrgeiz zu werfen, EntityNotFoundExceptionwenn die Entität nicht gefunden wird. Sein Verhalten ist sowohl einfach als auch klar. Sie werden nie überrascht sein, da es immer eine geladene Entität oder null(falls die Entität nicht gefunden wird) zurückgibt, aber niemals eine Entität in Form eines Proxys, die möglicherweise nicht effektiv geladen wird.
Sollte EntityManager.find()also in den meisten Fällen bevorzugt werden.

davidxxx
quelle
Ihr Grund ist irreführend im Vergleich zu der akzeptierten Antwort + Vlad Mihalcea Antwort + meinem Kommentar zu Vlad Mihalcea (vielleicht ein weniger wichtiger letzteres +).
Adrrc
1
In Pro JPA2 heißt es: "Angesichts der sehr spezifischen Situation, in der getReference () verwendet werden kann, sollte find () in praktisch allen Fällen verwendet werden."
JL_SO
Bewerten Sie diese Frage, weil sie eine notwendige Ergänzung zur akzeptierten Antwort darstellt und weil meine Tests gezeigt haben, dass beim Festlegen einer Eigenschaft des Entitäts-Proxys diese entgegen der Aussage der akzeptierten Antwort aus der Datenbank abgerufen wird. Nur der von Vlad angegebene Fall hat meine Tests bestanden.
João Fé
2

Ich bin mit der ausgewählten Antwort nicht einverstanden, und wie davidxxx richtig hervorhob, bietet getReference kein solches Verhalten für dynamische Aktualisierungen ohne Auswahl. Ich habe eine Frage zur Gültigkeit dieser Antwort gestellt, siehe hier - kann nicht aktualisiert werden, ohne select on using setter nach getReference () von JPA im Ruhezustand auszugeben .

Ich habe ehrlich gesagt niemanden gesehen, der diese Funktionalität tatsächlich genutzt hat. IRGENDWO. Und ich verstehe nicht, warum es so positiv ist.

Unabhängig davon, was Sie für ein Proxy-Objekt im Ruhezustand, einen Setter oder einen Getter aufrufen, wird zunächst eine SQL ausgelöst und das Objekt geladen.

Aber dann dachte ich mir, was ist, wenn der JPA-Proxy getReference () diese Funktionalität nicht bietet? Ich kann einfach meinen eigenen Proxy schreiben.

Jetzt können wir alle argumentieren, dass die Auswahl von Primärschlüsseln so schnell ist, wie eine Abfrage nur sein kann, und dass es nicht wirklich etwas ist, große Anstrengungen zu unternehmen, um dies zu vermeiden. Aber für diejenigen von uns, die aus dem einen oder anderen Grund nicht damit umgehen können, ist unten eine Implementierung eines solchen Proxys aufgeführt. Aber bevor Sie die Implementierung sehen, sehen Sie, wie sie verwendet wird und wie einfach sie zu verwenden ist.

VERWENDUNG

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

Und dies würde die folgende Abfrage auslösen -

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

und selbst wenn Sie einfügen möchten, können Sie PersistenceService.save (neue Reihenfolge ("a", 2)) ausführen; und es würde einen Einsatz abfeuern, wie es sollte.

IMPLEMENTIERUNG

Fügen Sie dies Ihrer pom.xml hinzu -

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

Erstellen Sie diese Klasse, um einen dynamischen Proxy zu erstellen.

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

Erstellen Sie eine Schnittstelle mit allen Methoden -

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

Erstellen Sie nun einen Interceptor, mit dem Sie diese Methoden auf Ihrem Proxy implementieren können.

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

Und die Ausnahmeklasse -

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

Ein Dienst zum Speichern mit diesem 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
quelle