Когда использовать EntityManager.find () против EntityManager.getReference () с JPA

103

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

Итак, в основном поток был таким:

class 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
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 = новый FLH ();
    FLHObj.setF (FObj);
    FLHObj.setL (LObj);
    ....
    EntityManager.persist (FLHObj);
    ...
  }
}

Я получал следующее исключение «java.lang.IllegalArgumentException: Unknown entity: com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0»

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

Это заставляет меня задаться вопросом, когда целесообразно использовать метод EntityManager.getReference () вместо метода EntityManager.find () ?

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

Что касается границ транзакций, мне кажется, что вам нужно будет использовать метод find () перед передачей вновь найденной сущности в новую транзакцию. Если вы используете метод getReference (), вы, вероятно, окажетесь в ситуации, аналогичной моей, с указанным выше исключением.

СибзТер
источник
Забыл упомянуть, что я использую Hibernate в качестве поставщика JPA.
SibzTer

Ответы:

152

Я обычно использую метод getReference, когда мне не нужно получать доступ к состоянию базы данных (я имею в виду метод получения). Просто чтобы изменить состояние (я имею в виду метод установки). Как вы должны знать, 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);
    }

}

Если я вызову метод 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 = ?

А знаете почему ???

Когда вы вызываете 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проходит NON-LAZY отношения (выдача new SELECTS), хотя я просто хочу обновить одно поле в одной сущности.
Деян Милошевич
1
@Arthur Ronald, что произойдет, если в объекте, вызываемом getReference, есть аннотация Version?
Дэвид Хофманн
У меня та же проблема, что и у @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. Мы только хотим установить базовый столбец внешнего ключа 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, getReferenceonly возвращает прокси-объект сущности, у которого только установлен идентификатор. Если вы обращаетесь к прокси-серверу, связанный оператор SQL будет запускаться, пока EntityManager все еще открыт.

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

При загрузке прокси вам необходимо знать, что при попытке доступа к ссылке на прокси после закрытия EntityManager может возникнуть исключение LazyInitializationException. Для получения дополнительных сведений об обработке LazyInitializationExceptionфайлов см. Эту статью .

Влад Михалча
источник
1
Спасибо, Влад, что сообщили нам об этом! Но, согласно javadoc, это кажется тревожным: «Среда выполнения поставщика сохраняемости может генерировать исключение EntityNotFoundException при вызове getReference». Это невозможно без SELECT (по крайней мере, для проверки существования строки), не так ли? Таким образом, в конечном итоге SELECT зависит от реализации.
adrhc 09
3
Для описанного вами варианта использования Hibernate предлагает hibernate.jpa.compliance.proxyсвойство конфигурации , поэтому вы можете выбрать соответствие JPA или более высокую производительность доступа к данным.
Влад Михалча 09
@VladMihalcea зачем getReferenceвообще нужен, если достаточно просто установить новый экземпляр модели с установленным PK. Что мне не хватает?
рилаби
Это поддерживается только в 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 (): нет разницы в накладных расходах

Я не согласен с принятым ответом, в частности:

Если я вызову метод 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 = ?

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

Получить экземпляр, состояние которого может быть получено лениво. Если запрошенный экземпляр не существует в базе данных, EntityNotFoundException выбрасывается при первом доступе к состоянию экземпляра. (Среда выполнения поставщика сохраняемости может генерировать исключение EntityNotFoundException при вызове getReference.) Приложению не следует ожидать, что состояние экземпляра будет доступно после отсоединения, если только оно не было получено приложением, когда диспетчер сущностей был открыт.

EntityManager.getReference() избавляет запрос для извлечения сущности в двух случаях:

1) если объект хранится в контексте постоянства, это кеш первого уровня.
И это поведение не является специфическим для EntityManager.getReference(), EntityManager.find()также избавит от запроса для получения объекта, если объект хранится в контексте Persistence.

Вы можете проверить первый пункт на любом примере.
Вы также можете положиться на реальную реализацию Hibernate.
Действительно, для загрузки объекта 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) Если мы не можем эффективно манипулировать сущностью, повторяется лениво загруженная документация javadoc.
Действительно, чтобы обеспечить эффективную загрузку объекта, требуется вызов метода для него.
Значит, выигрыш будет связан со сценарием, когда мы хотим загрузить сущность без необходимости ее использовать? В рамках приложений такая потребность действительно необычна, и, кроме того, getReference()поведение также вводит в заблуждение, если вы прочитаете следующую часть.

Почему лучше EntityManager.find () чем EntityManager.getReference ()

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

Вызов getReference()может вернуть ленивую полученную сущность.
Здесь ленивая выборка относится не к отношениям сущности, а к самой сущности.
Это означает, что если мы вызываем, getReference()а затем контекст Persistence закрывается, объект может никогда не загрузиться, и поэтому результат действительно непредсказуем. Например, если прокси-объект сериализован, вы можете получить nullссылку как сериализованный результат или, если метод вызывается для прокси-объекта, возникает исключение, такое как LazyInitializationException.

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

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

davidxxx
источник
Ваша причина вводит в заблуждение по сравнению с принятым ответом + ответ Влада Михалчи + мой комментарий Владу Михалче (возможно, менее важный этот последний +).
adrhc 09
1
Pro JPA2 действительно заявляет: «Учитывая очень специфическую ситуацию, в которой можно использовать getReference (), find () следует использовать практически во всех случаях».
JL_SO
Проголосуйте за этот вопрос, потому что это необходимое дополнение к принятому ответу, а также потому, что мои тесты показали, что при установке свойства прокси-сервера объекта оно извлекается из базы данных, в отличие от того, что говорится в принятом ответе. Только случай, изложенный Владом, прошел мои испытания.
Жуан Фе,
2

Я не согласен с выбранным ответом, и, как правильно указал davidxxx, getReference не обеспечивает такое поведение динамических обновлений без выбора. Я задал вопрос относительно действительности этого ответа, см. Здесь - невозможно обновить, не выполнив select при использовании установщика после getReference () hibernate JPA .

Честно говоря, я не видел никого, кто действительно использовал бы эту функцию. ВЕЗДЕ. И я не понимаю, почему за него так проголосовали.

Теперь, во-первых, независимо от того, что вы вызываете для прокси-объекта гибернации, установщика или получателя, запускается 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 (new Order ("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);
    }

}
}
Анил Кумар
источник