何时在JPA中使用EntityManager.find()与EntityManager.getReference()


103

我遇到了一种情况(我认为这很奇怪,但可能很正常),在这种情况下,我使用EntityManager.getReference(LObj.getClass(),LObj.getId())获取数据库实体,然后将返回的对象传递给保留在另一个表中。

所以基本上流程是这样的:

TFacade类{

  createT(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类{

  editF(FObj){
    L LObj = FObj.getL();
    LObj = EntityManager.getReference(LObj.getClass(),LObj.getId());
    ...
    EntityManager.merge(FObj);
    ...
    FLHFacade.create(FObj,LObj);
  }
}

@ TransactionAttributeType.REQUIRED
FLHFacade类{

  createFLH(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提供程序。
2009年

Answers:


152

当我不需要访问数据库状态时,我通常使用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。

问候,


4
我正在使用EclipseLink 2.5.0,并且上述查询不正确。无论使用哪个/ 我,它总是发出一个SELECTbefore 。更糟糕的是,尽管我只想更新一个实体中的单个字段,但是遍历NON-LAZY关系(发出new )。UPDATEfind()getReference()SELECTSELECTS
Dejan Milosevic

1
@Arthur Ronald如果getReference调用的实体中存在版本注释,会发生什么情况?
David Hofmann

我有一个与@DejanMilosevic相同的问题:删除通过getReference()获得的实体时,将对该实体执行SELECT,它遍历该实体的所有LAZY关系,从而发出许多SELECTS(使用EclipseLink 2.5.0)。
斯特凡Appercel

27

正如我在本文中所解释的,假设您具有父Post实体和子实体,PostComment如下图所示:

在此处输入图片说明

如果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)

与不同findgetReference只会返回仅设置了标识符的实体Proxy。如果您访问代理,则只要EntityManager仍处于打开状态,就会触发关联的SQL语句。

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

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


1
谢谢弗拉德让我们知道这一点!但是根据javadoc,这似乎令人不安:“在调用getReference时,允许持久性提供程序运行时抛出EntityNotFoundException”。如果没有SELECT,这是不可能的(至少用于检查行是否存在),对吗?因此,最终的SELECT取决于实现。
adrhc

3
对于您描述的用例,Hibernate提供了hibernate.jpa.compliance.proxyconfiguration属性,因此您可以选择JPA合规性或更好的数据访问性能。
Vlad Mihalcea '18

@VladMihalcea为什么getReference只需要设置带有PK设置的新模型实例就足够了。我想念什么?
rilaby

仅在Hibernarea中受支持,如果遍历,将不允许您加载关联。
Vlad Mihalcea

8

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

由于您无法删除非托管实体,因此使用find(...)或createQuery(...)加载所有字段,只是立即删除它,这很愚蠢。

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

7

这使我感到奇怪,什么时候才建议使用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中得到的行为,而javadoc getReference()却没有这样说:

获取一个实例,其状态可能会延迟获取。如果请求的实例在数据库中不存在,则在首次访问实例状态时将引发EntityNotFoundException。(当调用getReference时,允许持久性提供程序运行时引发EntityNotFoundException。)应用程序不应期望实例状态在分离后可用,除非在打开实体管理器时由应用程序访问了实例状态。

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

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

您可以使用任何示例检查第一点。
您还可以依赖实际的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。
实际上,为了确保实体的有效加载,需要在其上调用方法。
那么,这种收益是否与我们想加载实体而不需要使用它的情况有关?在应用程序框架中,这种需求确实很少见,此外,getReference()如果您阅读下一部分的内容,那么这种行为也很容易引起误解。

为什么比EntityManager.getReference()更偏爱EntityManager.find()

在开销方面,getReference()并不比find()上一点更好。
那么为什么要使用一个或另一个呢?

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

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

EntityManager.find()EntityNotFoundException如果找不到该实体,则不会抛出该异常。它的行为既简单又清晰。您永远不会感到惊讶,因为它总是返回一个已加载的实体或null(如果未找到该实体),但永远不会返回未有效加载的代理形式的实体。
因此EntityManager.find(),在大多数情况下都应受到青睐。


与接受的答复+ Vlad Mihalcea答复+我对Vlad Mihalcea的评论相比,您的原因具有误导性(此后+可能不那么重要)。
adrhc

1
Pro JPA2确实声明:“鉴于可以使用getReference()的非常特殊的情况,因此几乎在所有情况下都应使用find()”。
JL_SO

支持该问题,因为它是对已接受答案的必要补充,并且因为我的测试表明,设置实体代理的属性时,会从数据库中获取该属性,这与已接受答案相反。只有弗拉德所说的案子才通过了我的考验。
若奥·菲

2

我不同意所选的答案,正如davidxxx正确指出的那样,如果没有选择,getReference不会提供这种动态更新的行为。我问了一个有关此答案是否有效的问题,请参见此处- 如果在休眠JPA的getReference()之后不使用setter发出select,就无法更新

老实说,我还没有看到任何实际使用过该功能的人。任何地方。而且我不明白为什么会这么反对。

现在,首先,无论您对休眠代理对象,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);
    }

}
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.