何时将 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) 会执行恶意攻击吗?

EntityManager.find 对象包含 2 个 id 作为主键的行为

JPA学习笔记(11)——使用二级缓存

Eclipselink 未检测到脏实体

如何查看 JPA 发出的 SQL 查询?