簡體   English   中英

何時將 EntityManager.find() 與 EntityManager.getReference() 與 JPA 一起使用

[英]When to use EntityManager.find() vs EntityManager.getReference() with JPA

我遇到了一種情況(我認為這很奇怪但可能很正常),我使用 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方法(我的意思是 getter 方法)。 只是為了改變狀態(我的意思是 setter 方法)。 您應該知道,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 標志以更新 OR NOT 人實體。 如果在更新語句后沒有更新行,JPA 提供程序將根據 JPA 規范拋出 EntityNotFoundException。

問候,

假設您有一個父Post實體和一個子PostComment ,如下圖所示:

在此處輸入圖片說明

如果在嘗試設置@ManyToOne post關聯時調用find

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只返回一個只有標識符集的實體代理。 如果訪問了 Proxy,只要 EntityManager 仍然打開,就會觸發關聯的 SQL 語句。

但是,在這種情況下,我們不需要訪問實體代理。 我們只想將外鍵傳播到基礎表記錄,因此對於此用例加載代理就足夠了。

在加載 Proxy 時,您需要注意,如果您在 EntityManager 關閉后嘗試訪問 Proxy 引用,則可能會拋出LazyInitializationException

這讓我想知道,什么時候最好使用 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 中得到的行為, getReference()的 javadoc 沒有說這樣的話:

獲取一個實例,其狀態可能會被延遲獲取。 如果請求的實例在數據庫中不存在,則在第一次訪問實例狀態時拋出 EntityNotFoundException。 (當調用 getReference 時,持久性提供程序運行時被允許拋出 EntityNotFoundException。)應用程序不應期望實例狀態在分離時可用,除非它在實體管理器打開時被應用程序訪問。

EntityManager.getReference()在兩種情況下通過查詢來檢索實體:

1) 如果實體存儲在 Persistence 上下文中,那就是一級緩存。
並且此行為並非特定於EntityManager.getReference() ,如果實體存儲在持久性上下文中, EntityManager.find()還將節省查詢以檢索實體。

您可以使用任何示例檢查第一點。
您還可以依賴實際的 Hibernate 實現。
實際上, EntityManager.getReference()依賴於org.hibernate.event.internal.DefaultLoadEventListener類的createProxyIfNecessary()方法來加載實體。
這是它的實現:

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()在大多數情況下應該受到青睞。

因為引用是“托管”的,但不是水合的,它也可以允許您通過 ID 刪除實體,而無需先將其加載到內存中。

由於您無法刪除非托管實體,因此使用 find(...) 或 createQuery(...) 加載所有字段只是為了立即將其刪除,這是非常愚蠢的。

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

我不同意所選的答案,正如 davidxxx 正確指出的那樣,getReference 不提供這種沒有選擇的動態更新行為。 我問了一個有關此答案有效性的問題,請參見此處 - 在休眠 JPA 的 getReference() 之后使用 setter 時不發出選擇,則無法更新

老實說,我還沒有看到任何人真正使用過該功能。 任何地方。 我不明白為什么它如此受歡迎。

現在首先,無論您對休眠代理對象、setter 或 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(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);
    }

}
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM