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

我曾经遇到过这样一种情况(我认为这很奇怪,但可能很正常) : 我使用 EntityManager.getReference (LObjec.getClass () ,LObjec.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: Unknown tity: com.my.persence.L $EnancerByCGLIB $$3e7987d0”

在研究了一段时间之后,我最终发现这是因为我正在使用 EntityManager.getReference ()方法,当该方法返回一个代理时,我得到了上面的异常。

这让我很好奇,什么时候应该使用 EntityManager.getReference ()方法而不是 EntityManager.find ()方法

GetReference ()如果无法找到正在搜索的实体,则抛出一个 EntityNotFoundException,这本身非常方便。Find ()方法在找不到实体时仅返回 null。

关于事务边界,在我看来,在将新发现的实体传递给新事务之前,您需要使用 find ()方法。如果您使用 getReference ()方法,那么您可能会遇到类似于上述异常的情况。

167428 次浏览

当我不需要访问数据库状态时,我通常使用 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);
}


}

如果我调用 找到方法,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 实体。如果在更新语句之后没有更新任何行,JPA 提供程序将根据 JPA 规范抛出 EntityNotFoundException。

regards,

因为引用是“托管的”,但不是水合的,所以它还允许您通过 ID 删除实体,而无需首先将其加载到内存中。

因为不能删除非托管实体,所以使用 find (...)或 createQuery (...)加载所有字段,只是为了立即删除它,这是非常愚蠢的。

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

这让我想知道,什么时候使用 GetReference ()方法,而不是 EntityManager.find() method?

EntityManager.getReference()实际上是一种容易出错的方法,客户机代码需要使用它的情况真的很少。
就我个人而言,我从不需要使用它。

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中得到的行为,而且 getReference()的 javadoc 也没有这样说:

获取一个实例,该实例的状态可能被懒惰地获取 实例在数据库中不存在,则 EntityNotFoundException 第一次访问实例状态时引发 允许提供程序运行库引发 EntityNotFoundException 应用程序不应该期望 实例状态将在分离时可用,除非 在实体管理器打开时由应用程序访问。

EntityManager.getReference()在以下两种情况下保留一个查询来检索实体:

1)如果实体存储在持久性上下文中,即 第一级缓存。
这种行为不是 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 的 lazily fetched
事实上,为了确保实体的有效加载,需要对其调用一个方法。
所以收益与我们想要加载一个实体而不需要使用它的场景有关?在应用程序的框架中,这种需求确实不常见,此外,如果您阅读下一部分,getReference()的行为也是非常具有误导性的。

为什么偏爱 EntityManager.find ()而不是 EntityManager.getReference ()

就开销而言,getReference()并不比前面讨论的 find()好。
那为什么要选择其中一个呢?

调用 getReference()可能会返回一个延迟获取的实体。
在这里,延迟抓取不是指实体的关系,而是指实体本身。
这意味着,如果我们调用 getReference(),然后持久化上下文关闭,实体可能永远不会被加载,因此结果真的是不可预测的。例如,如果代理对象是序列化的,您可以获得一个 null引用作为序列化的结果,或者如果在代理对象上调用了一个方法,则会引发一个异常,比如 LazyInitializationException

这意味着,当实体不存在时,可能永远不会执行 EntityNotFoundException抛出操作,而 EntityNotFoundException抛出操作是使用 getReference()处理数据库中不存在的实例的主要原因。

如果找不到实体,EntityManager.find()没有抛出 EntityNotFoundException的野心。它的行为既简单又清晰。您永远不会感到惊讶,因为它总是返回一个加载的实体或 null(如果没有找到该实体) ,但从来没有一个代理形状下的实体,可能无法有效加载。
因此,在大多数情况下,EntityManager.find()应该受到青睐。

假设您有一个父 Post实体和一个子 PostComment,如下图所示:

enter image description here

如果在设置 @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)

The SELECT query is useless this time because we don’t need the Post entity to be fetched. We only want to set the underlying post_id Foreign Key column.

现在,如果使用 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 关闭后访问代理引用,则可能会引发 LazyInitializationException

我不同意所选择的答案,并且正如 davidxxx 正确指出的那样,getReference 不提供没有 select 的动态更新行为。我问了一个关于这个答案有效性的问题,见这里 -在 hibernate JPA 的 getReference ()之后使用 setter 时,如果不发出 select,就不能更新

老实说,我从来没有见过任何人真正使用过这个功能。任何地方。我不明白为什么这么赞成。

现在首先,不管您对休眠代理对象、 setter 或 getter 调用什么,都会触发一个 SQL 并加载该对象。

But then i thought, so what if JPA getReference() proxy doesn't provide that functionality. I can just write my own proxy.

现在,我们都可以认为主键上的选择已经是查询所能达到的最快速度了,而且这并不是真正需要竭尽全力去避免的事情。但是对于那些由于这样或那样的原因不能处理它的人来说,下面是这样一个代理的实现。但是在我看到这个实现之前,先看看它的用法和它的使用简单程度。

USAGE

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;

即使您想要插入,您仍然可以执行 PersisenceService.save (new Order (“ a”,2)) ; 它将按照应该的方式激发一个插入。

IMPLEMENTATION

Add this to your 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());
}
}

Make an interface with all the methods -

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

And the exception class -

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


}
}