【问题标题】:When to use EntityManager.find() vs EntityManager.getReference() with JPA何时将 EntityManager.find() 与 EntityManager.getReference() 与 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 提供者。

标签: java hibernate jpa jakarta-ee persistence


【解决方案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)。
      猜你喜欢
      • 2017-09-27
      • 1970-01-01
      • 1970-01-01
      • 2021-08-30
      • 2017-09-02
      • 2023-03-18
      • 2017-08-26
      • 2014-03-10
      • 2018-12-05
      相关资源
      最近更新 更多