Когда использовать EntityManager.find () против EntityManager.getReference () с JPA - PullRequest
95 голосов
/ 22 октября 2009

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

Так что в основном поток был такой:

class TFacade{

  createT(FObj, AObj) {
    T TObj = new 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 = new 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 () просто возвращает ноль, если не может найти сущность.

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

Ответы [ 5 ]

142 голосов
/ 22 октября 2009

Я обычно использую метод 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);
    }

}

Если я позвоню найду метод , провайдер 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, чтобы обновить сущность OR NOT person. Если после оператора update строки не обновляются, поставщик JPA сгенерирует исключение EntityNotFoundException в соответствии со спецификацией JPA.

С уважением,

11 голосов
/ 20 июня 2018

Как я объяснил в этой статье , предполагая, что у вас есть родительская Post сущность и дочерний PostComment, как показано на следующей диаграмме:

enter image description here

Если вы звоните 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, getReference возвращает только прокси-объект сущности, для которого установлен только идентификатор. Если вы обращаетесь к прокси-серверу, соответствующий оператор SQL будет запускаться, пока EntityManager все еще открыт.

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

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

8 голосов
/ 07 ноября 2016

Поскольку ссылка «управляемая», но не гидратированная, она также может позволить вам удалить объект по идентификатору, не загружая его сначала в память.

Поскольку вы не можете удалить неуправляемую сущность, просто глупо загружать все поля, используя find (...) или createQuery (...), только чтобы немедленно удалить ее.

MyLargeObject myObject = em.getReference(MyLargeObject.class, objectId);
em.remove(myObject);
3 голосов
/ 02 июня 2018

Это заставляет меня задуматься, когда целесообразно использовать Метод 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 = ?

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

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

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

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

Вы можете проверить первую точку на любом примере.
Вы также можете положиться на фактическую реализацию 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(), а затем контекст постоянства закроется, сущность может никогда не загрузиться, и поэтому результат действительно непредсказуем. Например, если прокси-объект сериализуется, вы можете получить ссылку null в качестве сериализованного результата или, если метод вызывается для прокси-объекта, возникает исключение, например LazyInitializationException.

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

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

0 голосов
/ 27 декабря 2018

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

Честно говоря, я не видел никого, кто бы на самом деле использовал эту функциональность. В ЛЮБОМ МЕСТЕ. И я не понимаю, почему это так проголосовало.

Теперь, во-первых, независимо от того, что вы вызываете для прокси-объекта hibernate, установщика или получателя, запускается SQL и объект загружается.

Но потом я подумал, что если JPA getReference () proxy не предоставляет такую ​​функциональность. Я могу просто написать свой собственный прокси.

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

* USAGE 1014 *

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

}
}
...