Коли використовувати EntityManager.find () проти EntityManager.getReference () з JPA


103

Я зіткнувся з ситуацією (яка, на мою думку, дивна, але, можливо, цілком нормальна), коли я використовую EntityManager.getReference (LObj.getClass (), LObj.getId ()), щоб отримати об'єкт бази даних, а потім передати повернутий об'єкт зберігатись в іншій таблиці.

Таким чином, потік був таким:

клас TFacade {

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

@ TransactionAttributeType.REQUIRES_NEW
клас FFacade {

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

@ TransactionAttributeType.REQUIRED
клас FLHFacade {

  createFLH (FObj, LObj) {
    FLH FLHObj = новий FLH ();
    FLHObj.setF (FObj);
    FLHObj.setL (LObj);
    ….
    EntityManager.persist (FLHObj);
    ...
  }
}

Я отримував такий виняток "java.lang.IllegalArgumentException: Невідомий об'єкт: com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0"

Поглянувши на це деякий час, я нарешті зрозумів, що саме тому, що я використовував метод EntityManager.getReference (), я отримував вище виняток, оскільки метод повертав проксі.

Це змушує мене замислитися, коли доцільно використовувати метод EntityManager.getReference () замість методу EntityManager.find () ?

EntityManager.getReference () кидає EntityNotFoundException, якщо він не може знайти потрібну сутність, яка сама по собі дуже зручна. Метод EntityManager.find () просто повертає null, якщо він не може знайти сутність.

Що стосується меж транзакцій, то мені здається, що вам потрібно скористатися методом find (), перш ніж передати новостворену особу до нової транзакції. Якщо ви використовуєте метод getReference (), ви, ймовірно, опинитесь у ситуації, подібній моїй, за винятком вище.


Забув згадати, я використовую сплячку в якості постачальника JPA.
SibzTer

Відповіді:


152

Зазвичай я використовую метод getReference, коли мені не потрібно отримувати доступ до стану бази даних (я маю на увазі метод getter). Просто для зміни стану (я маю на увазі метод сеттера). Як ви повинні знати, getReference повертає проксі-об'єкт, який використовує потужну функцію під назвою автоматична брудна перевірка. Припустимо наступне

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

}

Якщо я зателефоную за способом пошуку , постачальник JPA за кадром зателефонує

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Якщо я зателефоную на метод getReference , постачальник JPA за кадром зателефонує

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

А ви знаєте, чому ???

Коли ви зателефонуєте на getReference, ви отримаєте проксі-об'єкт. Щось подібне (постачальник JPA піклується про реалізацію цього проксі)

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

}

Тому перед здійсненням транзакції провайдер JPA побачить прапор StateChanged, щоб оновити АБО НЕ БУДЬ суб'єкт. Якщо після оператора оновлення жодні рядки не оновлюються, постачальник JPA викине EntityNotFoundException відповідно до специфікації JPA.

З повагою,


4
Я використовую EclipseLink 2.5.0, і вказані вище запити невірні. Вона завжди видає SELECTдо того UPDATE, незалежно від того , який з find()/ getReference()я використовую. Що ще гірше, SELECTперетинає НЕЗАЛЕЗНІ відносини (видаючи нові SELECTS), хоча я просто хочу оновити одне поле в одній сутності.
Деян Мілошевич

1
@ Артур Рональд, що станеться, якщо в об'єкті, що називається getReference, є анотація до версії?
Девід Хофманн

У мене є те саме питання, що і у @DejanMilosevic: при видаленні об'єкта, отриманого через getReference (), на цьому об'єкті видається SELECT, який переміщує всі відносини LAZY цього об'єкта, видаючи таким чином багато SELECTS (з EclipseLink 2.5.0).
Стефан Апперсел

27

Як я пояснив у цій статті , якщо припустити, що у вас є батьківська Postособа та дитина, PostCommentяк показано на наступній схемі:

введіть тут опис зображення

Якщо ви телефонуєте findпри спробі встановити @ManyToOne postасоціацію:

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

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

entityManager.persist(comment);

Hibernate виконає такі заяви:

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)

Цього разу запит SELECT марний, оскільки нам не потрібно отримувати об'єкт "Пошта". Ми хочемо лише встановити нижній стовпчик зовнішнього ключа post_id.

Тепер, якщо ви використовуєте getReferenceзамість цього:

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

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

entityManager.persist(comment);

Цього разу Hibernate випустить лише твердження INSERT:

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

На відміну від цього find, getReferenceєдиний повертає сутність-проксі, яка має лише ідентифікатор. Якщо ви отримуєте доступ до проксі, пов'язаний оператор SQL буде запускатися до тих пір, поки EntityManager ще відкритий.

Однак у цьому випадку нам не потрібно звертатися до сутності-проксі. Ми хочемо лише поширити зовнішній ключ до базової таблиці таблиці, тому завантаження проксі достатньо для цього випадку використання.

Завантажуючи проксі, ви повинні знати, що LazyInitializationException може бути кинуто, якщо ви спробуєте отримати доступ до посилання проксі після закриття EntityManager. Щоб отримати докладніші відомості про поводження LazyInitializationException, ознайомтеся з цією статтею .


1
Дякую Владу, що дав нам знати про це! Але, на думку javadoc, це здається тривожним: "Виконання завзятості постачальника може кидати EntityNotFoundException, коли викликається getReference". Це неможливо без SELECT (принаймні для перевірки існування рядка), чи не так? Отже, врешті-решт SELECT залежить від реалізації.
adrhc

3
Для описаного вами випадку використання Hibernate пропонує hibernate.jpa.compliance.proxyвластивість конфігурації , тож ви можете вибрати відповідність JPA або кращу ефективність доступу до даних.
Влад Міхалча

@VladMihalcea навіщо getReferenceвзагалі потрібен, якщо достатньо лише встановити новий екземпляр моделі з набором ПК. Що я пропускаю?
rilaby

Це підтримується лише в Hibernarea і не дозволяє завантажувати асоціацію, якщо вона проходить.
Влад Михальча

8

Оскільки посилання "керується", але не є гідратованим, воно також дозволяє вам видалити об'єкт за ідентифікатором, не потрібно спочатку завантажувати його в пам'ять.

Оскільки ви не можете видалити некерований об'єкт, просто нерозумно завантажувати всі поля за допомогою find (...) або createQuery (...), лише негайно видалити його.

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

7

Це змушує мене замислитися, коли доцільно використовувати метод EntityManager.getReference () замість методу EntityManager.find ()?

EntityManager.getReference()це дійсно схильний до помилок метод, і дійсно дуже мало випадків, коли клієнтському коду потрібно його використовувати.
Особисто мені ніколи не потрібно було ним користуватися.

EntityManager.getReference () та EntityManager.find (): немає різниці в термінах накладних витрат

Я не згоден з прийнятою відповіддю, зокрема:

Якщо я зателефоную за способом пошуку , постачальник JPA за кадром зателефонує

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Якщо я зателефоную на метод getReference , постачальник JPA за кадром зателефонує

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Це не та поведінка, яку я отримую зі сплячки 5, і javadoc getReference()не говорить такого:

Отримайте екземпляр, стан якого може бути ліниво підібраний. Якщо запитуваний екземпляр не існує в базі даних, EntityNotFoundException передається під час першого доступу до стану екземпляра. (Час виконання постачальника наполегливості дозволено викидати EntityNotFoundException, коли викликається getReference.) Додаток не повинен очікувати, що стан екземпляра буде доступним після від'єднання, якщо тільки до нього не звертався додаток, поки менеджер суб'єкта був відкритий.

EntityManager.getReference() економить запит для отримання об'єкта у двох випадках:

1) якщо сутність зберігається в контексті персистенції, це кеш першого рівня.
І ця поведінка не є специфічною EntityManager.getReference(), EntityManager.find()вона також буде зайвою для запиту для отримання об'єкта, якщо сутність зберігається в контексті Персистентності.

Ви можете перевірити першу точку на будь-якому прикладі.
Ви також можете покластися на фактичну реалізацію в режимі глибокого сну.
Дійсно, EntityManager.getReference()покладається на createProxyIfNecessary()метод org.hibernate.event.internal.DefaultLoadEventListenerкласу для завантаження сутності.
Ось його реалізація:

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

Цікава частина:

Object existing = persistenceContext.getEntity( keyToLoad );

2) Якщо ми не ефективно маніпулюємо сутністю, перегукуючись на ліниво зібраний явадок.
Дійсно, щоб забезпечити ефективне завантаження суб'єкта господарювання, потрібно використовувати метод на ньому.
Отже, виграш буде пов'язаний зі сценарієм, коли ми хочемо завантажити суб'єкт господарювання без необхідності його використання? У рамках програм ця потреба справді нечаста, і крім того, getReference()поведінка також дуже вводить в оману, якщо ви читаєте наступну частину.

Чому перевагу EntityManager.find () над EntityManager.getReference ()

З точки зору накладних витрат, getReference()це не краще, ніж було find()сказано в попередньому пункті.
То навіщо використовувати те чи інше?

Викликання getReference()може повернути ліниво зібрану сутність.
Тут лінивий збір стосується не відносин суб'єкта, а самої сутності.
Це означає, що якщо ми посилаємося getReference()і тоді контекст Постійності закритий, сутність може ніколи не завантажуватися, і тому результат дійсно непередбачуваний. Наприклад, якщо проксі-об'єкт серіалізований, ви можете отримати nullпосилання як серіалізований результат або якщо метод викликається на об'єкті проксі, викид, такий як LazyInitializationExceptionвикинутий.

Це означає, що кидок EntityNotFoundExceptionцього є основною причиною використання getReference()для обробки екземпляра, який не існує в базі даних, оскільки ситуація з помилками ніколи не може бути виконана, поки сутність не існує.

EntityManager.find()не має амбіцій кидати, EntityNotFoundExceptionякщо сутність не знайдена. Його поведінка одночасно проста і зрозуміла. Ви ніколи не здивуєтеся, оскільки він повертає завжди завантажену сутність або null(якщо сутність не знайдена), але ніколи суб'єкт не має форми проксі, який може бути ефективно завантажений.
Тому EntityManager.find()слід віддавати перевагу в більшості випадків.


Ваша причина вводить в оману в порівнянні з прийнятою відповіддю + відповідь Влада Міхалчея + мій коментар до Влада Міхалчея (можливо, менш важливий цей останній +).
adrhc

1
Pro JPA2 констатує: "З огляду на дуже специфічну ситуацію, в якій getReference () можна використовувати, find () слід використовувати практично у всіх випадках".
JL_SO

Оновіть це питання тому, що воно є необхідним доповненням до прийнятої відповіді і тому, що мої тести показали, що при встановленні властивості проксі-сервера сутності воно виймається з бази даних, всупереч тому, що йдеться у прийнятій відповіді. Тільки випадок, про який говорив Влад, пройшов мої тести.
Жоао Фе

2

Я не погоджуюся з вибраною відповіддю, і як правильно вказав davidxxx, getReference не забезпечує таку поведінку динамічних оновлень без вибору. Я задав питання щодо обгрунтованості цієї відповіді, дивіться тут - не вдається оновити без вибору на сетері після getReference () сплячого JPA .

Я, чесно кажучи, не бачив нікого, хто насправді використовував цю функціональність. ВСЕ. І я не розумію, чому це так схвально.

Тепер, перш за все, незалежно від того, що ви називаєте на сплячому проксі-об'єкті, сеттері або getter, SQL запускається і об'єкт завантажується.

Але тоді я подумав, то що робити, якщо проксі-сервер JPA getReference () не забезпечує цю функціональність. Я можу просто написати власний проксі.

Тепер ми можемо стверджувати, що вибір первинних ключів є настільки ж швидким, наскільки може отримати запит, і це насправді не те, щоб уникнути великих зусиль, щоб уникнути. Але для тих із нас, хто не може це впоратися з тих чи інших причин, нижче наведено реалізацію такого проксі. Але перш ніж я побачу реалізацію, перегляньте її використання та наскільки це просто у використанні.

ВИКОРИСТАННЯ

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

І це запустить наступний запит -

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

і навіть якщо ви хочете вставити, ви все одно можете зробити PersistenceService.save (новий Порядок ("a", 2)); і він вистрілив би вкладиш як слід.

ВПРОВАДЖЕННЯ

Додайте це до свого pom.xml -

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

Зробіть цей клас для створення динамічного проксі -

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

Створіть інтерфейс з усіма методами -

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

Тепер зробіть перехоплювач, який дозволить вам реалізувати ці методи на своєму проксі -

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

І клас винятку -

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

Служба збереження за допомогою цього проксі -

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

}
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.