何时使用EntityManager.find()与EntityManager.getReference()一起使用JPA

时间:2009-10-22 14:15:07

标签: java java-ee jpa persistence

我遇到过一种情况(我觉得很奇怪,但可能很正常),我使用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()方法,那么你可能会遇到类似于我的情况,并带有上述异常。

5 个答案:

答案 0 :(得分:142)

当我不需要访问数据库状态时(我的意思是getter方法),我通常使用 getReference 方法。只是为了改变状态(我的意思是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实体。如果在update语句之后没有更新行,则JPA提供程序将根据JPA规范抛出EntityNotFoundException。

的问候,

答案 1 :(得分:9)

正如我在this article中所解释的,假设您有一个父Post实体和一个子PostComment,如下图所示:

enter image description here

如果您尝试设置find @ManyToOne关联时呼叫post

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仅返回仅设置了标识符的实体代理。如果您访问代理服务器,则只要EntityManager仍处于打开状态,就会触发关联的SQL语句。

但是,在这种情况下,我们不需要访问实体代理。我们只想将外键传播到基础表记录,因此在这种情况下加载代理就足够了。

加载代理时,需要注意的是,如果在关闭EntityManager后尝试访问代理引用,则可能引发LazyInitializationException。有关处理LazyInitializationException的更多详细信息,请查看this article

答案 2 :(得分:8)

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

由于无法删除非托管实体,使用find(...)或createQuery(...)加载所有字段只是为了立即将其删除,这简直太愚蠢了。

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

答案 3 :(得分:3)

  

这让我很奇怪,什么时候建议使用   EntityManager.getReference()方法代替   EntityManager.find()方法?

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

EntityManager.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   当调用getReference时。)应用程序不应该期望这样   除非是分离,否则实例状态将在分离时可用   在实体经理开放时由应用程序访问。

EntityManager.getReference()在两种情况下不使用查询来检索实体:

1)如果实体存储在Persistence上下文中,即  第一级缓存 此行为并非特定于EntityManager.getReference(), 如果实体存储在Persistence上下文中,EntityManager.find()还将保留查询以检索实体。

您可以通过任何示例检查第一点 您还可以依赖实际的Hibernate实现 实际上,EntityManager.getReference()依赖于createProxyIfNecessary()类的org.hibernate.event.internal.DefaultLoadEventListener方法来加载实体。
这是它的实现:

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.getReference()

上使用EntityManager.find()

就开销而言,getReference()并不比前一点所讨论的find()好。
那么为什么要使用这一个呢?

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

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

如果找不到实体,

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

答案 4 :(得分:0)

我不同意所选的答案,并且正如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);
    }

}
}