何时将 EntityManager.find() 与 EntityManager.getReference() 与 JPA 一起使用
Posted
技术标签:
【中文标题】何时将 EntityManager.find() 与 EntityManager.getReference() 与 JPA 一起使用【英文标题】:When to use EntityManager.find() vs EntityManager.getReference() with JPA 【发布时间】:2010-12-09 02:47:01 【问题描述】:我遇到了一种情况(我认为这很奇怪,但可能很正常),我使用 EntityManager.getReference(LObj.getClass(), LObj.getId()) 来获取数据库实体,然后通过返回的对象被持久化到另一个表中。
所以基本上流程是这样的:
类 TFacade 创建T(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 类FFacade 编辑F(FObj) L LObj = FObj.getL(); LObj = EntityManager.getReference(LObj.getClass(), LObj.getId()); ... EntityManager.merge(FObj); ... FLHFacade.create(FObj, LObj); @TransactionAttributeType.REQUIRED FLHFacade 类 创建FLH(FObj,LObj) FLH FLHObj = 新 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() 方法,那么您可能最终会遇到与我类似的情况,但上述异常除外。
【问题讨论】:
忘了说,我使用 Hibernate 作为 JPA 提供者。 【参考方案1】:假设您有一个父实体 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。我们只想将外键传播到底层表记录,因此对于这个用例来说,加载一个代理就足够了。
加载 Proxy 时,您需要注意,如果您在 EntityManager 关闭后尝试访问 Proxy 引用,可能会抛出 LazyInitializationException
。
【讨论】:
感谢弗拉德让我们知道这一点!但根据 javadoc,这似乎令人不安:“当调用 getReference 时,允许持久性提供程序运行时抛出 EntityNotFoundException”。如果没有 SELECT(至少用于检查行是否存在),这是不可能的,是吗?所以最终的 SELECT 取决于实现。 对于您描述的用例,Hibernate 提供了hibernate.jpa.compliance.proxy
configuration property,因此您可以选择 JPA 合规性或更好的数据访问性能。
@VladMihalcea 为什么需要getReference
,如果它足以设置具有 PK 集的模型的新实例。我错过了什么?
这仅在 Hibernarea 中支持,如果遍历将不允许您加载关联。【参考方案2】:
我不同意所选的答案,正如 davidxxx 正确指出的那样,getReference 不提供这种没有选择的动态更新行为。我问了一个关于此答案有效性的问题,请参阅此处 - cannot update without issuing select on using setter after getReference() of hibernate JPA。
老实说,我还没有看到任何人真正使用过该功能。任何地方。我不明白为什么它如此受欢迎。
现在首先,无论您在休眠代理对象、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);
【讨论】:
【参考方案3】:这让我想知道,什么时候使用 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 首次访问实例状态时抛出。 (坚持 允许提供程序运行时抛出 EntityNotFoundException 调用 getReference 时。)应用程序不应期望 实例状态将在分离时可用,除非它是 实体管理器打开时由应用程序访问。
EntityManager.getReference()
在两种情况下不使用查询来检索实体:
1) 如果实体存储在 Persistence 上下文中,即
一级缓存。
而且这种行为并不是EntityManager.getReference()
特有的,
如果实体存储在 Persistence 上下文中,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()
在大多数情况下都应该受到青睐。
【讨论】:
与接受的回复 + Vlad Mihalcea 回复 + 我对 Vlad Mihalcea 的评论相比,您的理由具有误导性(最后一个 + 可能不太重要)。 Pro JPA2 确实声明:“鉴于可以使用 getReference() 的非常具体的情况,几乎所有情况下都应该使用 find()”。 赞成这个问题,因为它是对已接受答案的必要补充,并且因为我的测试表明,在设置实体代理的属性时,它是从数据库中获取的,这与接受的答案所说的相反。只有弗拉德陈述的案例通过了我的测试。【参考方案4】:因为引用是“托管”的,但不是水合的,它还可以让您按 ID 删除实体,而无需先将其加载到内存中。
由于您无法删除非托管实体,因此使用 find(...) 或 createQuery(...) 加载所有字段非常愚蠢,然后立即将其删除。
MyLargeObject myObject = em.getReference(MyLargeObject.class, objectId);
em.remove(myObject);
【讨论】:
【参考方案5】:当我不需要访问数据库状态时,我通常使用 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 个人实体。如果 update 语句后没有更新任何行,JPA 提供程序将根据 JPA 规范抛出 EntityNotFoundException。
问候,
【讨论】:
我使用的是 EclipseLink 2.5.0,上述查询不正确。无论我使用find()
/ getReference()
中的哪一个,它总是在UPDATE
之前发出SELECT
。更糟糕的是,SELECT
遍历 NON-LAZY 关系(发出新的SELECTS
),尽管我只想更新一个实体中的单个字段。
@Arthur Ronald 如果 getReference 调用的实体中有版本注释会发生什么?
我和@DejanMilosevic 有同样的问题:删除通过 getReference() 获得的实体时,会在该实体上发出 SELECT 并遍历该实体的所有 LAZY 关系,从而发出许多 SELECTS (使用 EclipseLink 2.5.0)。以上是关于何时将 EntityManager.find() 与 EntityManager.getReference() 与 JPA 一起使用的主要内容,如果未能解决你的问题,请参考以下文章
EntityManager.find() 和 EntityManger.getReference() 有啥区别?
EntityManager.find(id) 会执行恶意攻击吗?