Kiedy używać EntityManager.find (), a kiedy EntityManager.getReference () z JPA

103

Natknąłem się na sytuację (która moim zdaniem jest dziwna, ale prawdopodobnie jest całkiem normalna), w której używam EntityManager.getReference (LObj.getClass (), LObj.getId ()), aby uzyskać jednostkę bazy danych, a następnie przekazać zwrócony obiekt do być utrwalone w innej tabeli.

Zasadniczo przepływ był taki:

class TFacade {

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

Otrzymuję następujący wyjątek „java.lang.IllegalArgumentException: Nieznany podmiot: com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0”

Po dłuższym przyjrzeniu się temu w końcu zorientowałem się, że to dlatego, że korzystałem z metody EntityManager.getReference (), otrzymałem powyższy wyjątek, ponieważ metoda zwracała proxy.

To sprawia, że ​​zastanawiam się, kiedy zaleca się użycie metody EntityManager.getReference () zamiast metody EntityManager.find () ?

EntityManager.getReference () zgłasza EntityNotFoundException, jeśli nie może znaleźć poszukiwanej jednostki, co samo w sobie jest bardzo wygodne. Metoda EntityManager.find () zwraca tylko wartość null, jeśli nie może znaleźć jednostki.

Jeśli chodzi o granice transakcji, wydaje mi się, że przed przekazaniem nowo znalezionej jednostki do nowej transakcji trzeba użyć metody find (). Jeśli użyjesz metody getReference (), prawdopodobnie znajdziesz się w sytuacji podobnej do mojej z powyższym wyjątkiem.

SibzTer
źródło
Zapomniałem wspomnieć, że używam Hibernate jako dostawcę JPA.
SibzTer

Odpowiedzi:

152

Zwykle używam metody getReference , gdy nie potrzebuję dostępu do stanu bazy danych (mam na myśli metodę getter). Wystarczy zmienić stan (mam na myśli metodę ustawiającą). Jak powinieneś wiedzieć, getReference zwraca obiekt proxy, który używa potężnej funkcji zwanej automatycznym sprawdzaniem brudnym. Załóżmy, co następuje

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

}

Jeśli wywołam metodę znajdowania, zadzwoni dostawca JPA w tle

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Jeśli wywołam metodę getReference , dostawca JPA w tle zadzwoni

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

I wiesz dlaczego ???

Gdy wywołasz getReference, otrzymasz obiekt proxy. Coś takiego (dostawca JPA zajmuje się implementacją tego 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;
    }

}

Tak więc przed zatwierdzeniem transakcji dostawca JPA zobaczy flagę stateChanged w celu zaktualizowania podmiotu osoby lub NIE. Jeśli żadne wiersze nie zostaną zaktualizowane po instrukcji aktualizacji, dostawca JPA zgłosi EntityNotFoundException zgodnie ze specyfikacją JPA.

pozdrowienia,

Arthur Ronald
źródło
4
Używam EclipseLink 2.5.0, a powyższe zapytania są nieprawidłowe. To zawsze wystawia SELECTprzed UPDATE, bez względu na to, która z find()/ getReference()używam. Co gorsza, SELECTprzemierza relacje NON-LAZY (wydawanie nowych SELECTS), chociaż chcę tylko zaktualizować pojedyncze pole w jednym encji.
Dejan Milosevic
1
@Arthur Ronald co się stanie, jeśli w encji wywoływanej przez getReference znajduje się adnotacja o wersji?
David Hofmann
Mam ten sam problem, co @DejanMilosevic: podczas usuwania jednostki uzyskanej za pomocą getReference () na tej jednostce jest wydawany SELECT i przechodzi przez wszystkie LENIWE relacje tej jednostki, wydając w ten sposób wiele SELECTS (z EclipseLink 2.5.0).
Stéphane Appercel
27

Jak wyjaśniłem w tym artykule , zakładając, że masz Postpodmiot nadrzędny i dziecko, PostCommentjak pokazano na poniższym diagramie:

wprowadź opis obrazu tutaj

Jeśli dzwonisz find, próbując ustawić @ManyToOne postskojarzenie:

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

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

entityManager.persist(comment);

Hibernate wykona następujące instrukcje:

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)

Zapytanie SELECT jest tym razem bezużyteczne, ponieważ nie potrzebujemy do pobrania jednostki Post. Chcemy tylko ustawić bazową kolumnę klucza obcego post_id.

Teraz, jeśli getReferencezamiast tego użyjesz :

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

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

entityManager.persist(comment);

Tym razem Hibernate wyda tylko instrukcję INSERT:

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

W przeciwieństwie do tego find, getReferencejedyny zwraca encję Proxy, która ma tylko ustawiony identyfikator. Jeśli uzyskasz dostęp do serwera proxy, skojarzona instrukcja SQL zostanie wyzwolona, ​​dopóki EntityManager będzie nadal otwarty.

Jednak w tym przypadku nie musimy uzyskiwać dostępu do jednostki Proxy. Chcemy tylko propagować klucz obcy do bazowego rekordu tabeli, więc załadowanie serwera proxy jest wystarczające w tym przypadku użycia.

Podczas ładowania serwera proxy należy mieć świadomość, że wyjątek LazyInitializationException może zostać zgłoszony, jeśli spróbujesz uzyskać dostęp do odwołania do serwera proxy po zamknięciu obiektu EntityManager. Aby uzyskać więcej informacji na temat obsługi LazyInitializationException, zapoznaj się z tym artykułem .

Vlad Mihalcea
źródło
1
Dziękuję Vlad za poinformowanie nas o tym! Jednak według javadoc wydaje się to niepokojące: „Środowisko wykonawcze dostawcy trwałości może zgłosić wyjątek EntityNotFoundException po wywołaniu metody getReference”. Nie jest to możliwe bez SELECT (przynajmniej do sprawdzenia istnienia wiersza), prawda? Zatem ostatecznie SELECT zależy od implementacji.
adrhc
3
W opisanym przez ciebie przypadku użycia Hibernate oferuje hibernate.jpa.compliance.proxywłaściwość konfiguracyjną , dzięki czemu możesz wybrać zgodność JPA lub lepszą wydajność dostępu do danych.
Vlad Mihalcea
@VladMihalcea, dlaczego getReferencew ogóle jest potrzebne, jeśli wystarczy ustawić nową instancję modelu z zestawem PK. czego mi brakuje?
rilaby
Jest to obsługiwane tylko w Hibernarea i nie pozwala na załadowanie powiązania po przejściu.
Vlad Mihalcea
8

Ponieważ referencja jest „zarządzana”, ale nie jest nawodniona, może również pozwolić na usunięcie jednostki według identyfikatora, bez konieczności uprzedniego ładowania jej do pamięci.

Ponieważ nie można usunąć niezarządzanej jednostki, po prostu głupio jest ładować wszystkie pola za pomocą funkcji find (...) lub createQuery (...), tylko po to, aby ją natychmiast usunąć.

MyLargeObject myObject = em.getReference(MyLargeObject.class, objectId);
em.remove(myObject);
Steven Spungin
źródło
7

To sprawia, że ​​zastanawiam się, kiedy zaleca się użycie metody EntityManager.getReference () zamiast metody EntityManager.find ()?

EntityManager.getReference()jest metodą podatną na błędy i jest bardzo niewiele przypadków, w których kod klienta musi jej użyć.
Osobiście nigdy nie musiałem go używać.

EntityManager.getReference () i EntityManager.find (): brak różnicy w zakresie kosztów ogólnych

Nie zgadzam się z zaakceptowaną odpowiedzią, aw szczególności:

Jeśli wywołam metodę znajdowania, zadzwoni dostawca JPA w tle

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Jeśli wywołam metodę getReference , dostawca JPA w tle zadzwoni

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

To nie jest zachowanie, które otrzymuję w Hibernate 5, a javadoc getReference()nie mówi czegoś takiego:

Uzyskaj instancję, której stan można leniwie pobrać. Jeśli żądane wystąpienie nie istnieje w bazie danych, EntityNotFoundException jest generowany podczas pierwszego dostępu do stanu wystąpienia. (Środowisko wykonawcze dostawcy trwałości może zgłosić wyjątek EntityNotFoundException po wywołaniu metody getReference). Aplikacja nie powinna oczekiwać, że stan wystąpienia będzie dostępny po odłączeniu, chyba że aplikacja uzyskała do niego dostęp, gdy menedżer jednostek był otwarty.

EntityManager.getReference() oszczędza kwerendę w celu pobrania jednostki w dwóch przypadkach:

1) jeśli jednostka jest przechowywana w kontekście trwałości, jest to pamięć podręczna pierwszego poziomu.
I to zachowanie nie jest specyficzne dla EntityManager.getReference(), EntityManager.find()oszczędza również kwerendę w celu pobrania jednostki, jeśli jednostka jest przechowywana w kontekście trwałości.

Możesz sprawdzić pierwszy punkt na dowolnym przykładzie.
Możesz również polegać na rzeczywistej implementacji Hibernacji.
Rzeczywiście, EntityManager.getReference()polega na createProxyIfNecessary()metodzie org.hibernate.event.internal.DefaultLoadEventListenerklasy, aby załadować jednostkę.
Oto jego realizacja:

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

Ciekawą częścią jest:

Object existing = persistenceContext.getEntity( keyToLoad );

2) Jeśli nie będziemy efektywnie manipulować bytem, ​​powielając leniwie pobrane javadoc.
Rzeczywiście, aby zapewnić efektywne ładowanie jednostki, wymagane jest wywołanie metody.
Czyli zysk byłby związany ze scenariuszem, w którym chcemy załadować jednostkę bez potrzeby jej używania? W ramach aplikacji taka potrzeba jest naprawdę rzadka, a ponadto getReference()zachowanie jest również bardzo mylące, jeśli przeczytasz następną część.

Dlaczego warto faworyzować EntityManager.find () zamiast EntityManager.getReference ()

Pod względem kosztów ogólnych getReference()nie jest lepszy niż find()omówiony w poprzednim punkcie.
Więc po co używać jednego lub drugiego?

Wywołanie getReference()może zwrócić leniwie pobraną jednostkę.
Tutaj leniwe pobieranie nie odnosi się do relacji jednostki, ale do samej jednostki.
Oznacza to, że jeśli wywołamy, getReference()a następnie kontekst Persistence zostanie zamknięty, jednostka może nigdy nie zostać załadowana, a więc wynik jest naprawdę nieprzewidywalny. Na przykład, jeśli obiekt proxy jest serializowany, można uzyskać nullodwołanie jako wynik zserializowany lub jeśli metoda jest wywoływana dla obiektu proxy, zostanie zgłoszony wyjątek, taki jak LazyInitializationException.

Oznacza to, że wyrzucenie EntityNotFoundExceptiontego jest głównym powodem do getReference()obsługi instancji, która nie istnieje w bazie danych, ponieważ sytuacja błędu może nigdy nie zostać wykonana, gdy jednostka nie istnieje.

EntityManager.find()nie ma ambicji rzucania, EntityNotFoundExceptionjeśli istota nie zostanie znaleziona. Jego zachowanie jest zarówno proste, jak i jasne. Nigdy nie będziesz zaskoczony, ponieważ zawsze zwraca załadowaną jednostkę lub null(jeśli jednostka nie zostanie znaleziona), ale nigdy nie jest to jednostka w postaci proxy, która może nie zostać skutecznie załadowana.
Dlatego EntityManager.find()w większości przypadków powinno to być preferowane.

davidxxx
źródło
Twój powód jest mylący w porównaniu z zaakceptowaną odpowiedzią + odpowiedzią Vlada Mihalcei + moim komentarzem do Vlada Mihalcei (może mniej ważny ten ostatni +).
adrhc
1
Pro JPA2 stwierdza: „Biorąc pod uwagę bardzo specyficzną sytuację, w której można użyć metody getReference (), funkcja find () powinna być używana praktycznie we wszystkich przypadkach”.
JL_SO
Zagłosuj za to pytanie, ponieważ jest niezbędnym uzupełnieniem zaakceptowanej odpowiedzi i ponieważ moje testy wykazały, że podczas ustawiania właściwości proxy encji jest ona pobierana z bazy danych, w przeciwieństwie do tego, co mówi zaakceptowana odpowiedź. Tylko przypadek przedstawiony przez Vlada przeszedł moje testy.
João Fé
2

Nie zgadzam się z wybraną odpowiedzią i jak słusznie zauważył davidxxx, getReference nie zapewnia takiego zachowania dynamicznych aktualizacji bez select. Zadałem pytanie dotyczące poprawności tej odpowiedzi, patrz tutaj - nie można zaktualizować bez wydania polecenia select przy użyciu settera po getReference () hibernacji JPA .

Szczerze mówiąc, nie widziałem nikogo, kto faktycznie korzystał z tej funkcji. GDZIEKOLWIEK. I nie rozumiem, dlaczego tak się cieszy.

Po pierwsze, bez względu na to, co wywołasz obiekt proxy z hibernacją, setter lub getter, uruchamiany jest kod SQL i ładowany jest obiekt.

Ale potem pomyślałem, co z tego, jeśli proxy getReference () JPA nie zapewnia takiej funkcjonalności. Mogę po prostu napisać własne proxy.

Teraz wszyscy możemy argumentować, że selekcje kluczy głównych są tak szybkie, jak może uzyskać zapytanie, i tak naprawdę nie jest to coś, czego należy unikać. Ale dla tych z nas, którzy nie mogą sobie z tym poradzić z jakiegoś powodu, poniżej znajduje się implementacja takiego proxy. Ale zanim zobaczysz implementację, zobacz, jak jest używana i jak prosta jest w użyciu.

STOSOWANIE

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

Spowoduje to uruchomienie następującego zapytania -

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

a nawet jeśli chcesz wstawić, nadal możesz zrobić PersistenceService.save (new Order ("a", 2)); i wystrzeliłby wkładkę tak, jak powinna.

REALIZACJA

Dodaj to do swojego pom.xml -

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

Utwórz tę klasę, aby utworzyć dynamiczne proxy -

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

Utwórz interfejs ze wszystkimi metodami -

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

Teraz utwórz przechwytywacz, który pozwoli ci zaimplementować te metody na twoim 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!");
}
}

I klasa wyjątkowa -

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

Usługa do zapisywania przy użyciu tego serwera 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
źródło