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

StackOverflow https://stackoverflow.com/questions/1607532

  •  05-07-2019
  •  | 
  •  

Вопрос

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

Что касается границ транзакции, мне кажется, что вам нужно будет использовать метод find() перед передачей вновь найденного объекта в новую транзакцию.Если вы используете метод 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 = ?

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

с уважением,

Другие советы

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

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

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

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

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

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

Это заставляет меня задуматься, когда целесообразно использовать   Метод 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 () также избавит от запроса на получение объекта, если объект хранится в контексте постоянства.

Первый пример можно проверить на любом примере.
Вы также можете положиться на реальную реализацию Hibernate.
Действительно, EntityManager.getReference () использует метод createProxyIfNeeded () класса 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 () не имеет

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

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

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

}
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top