繁体   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