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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1607532/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-12 16:41:06  来源:igfitidea点击:

When to use EntityManager.find() vs EntityManager.getReference() with JPA

javajakarta-eejpapersistence

提问by SibzTer

I have come across a situation (which I think is weird but is possibly quite normal) where I use the EntityManager.getReference(LObj.getClass(), LObj.getId()) to get a database entity and then pass the returned object to be persisted in another table.

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

So basically the flow was like this:

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

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);
    ...
  }
}

I was getting the following exception "java.lang.IllegalArgumentException: Unknown entity: com.my.persistence.L$$EnhancerByCGLIB$$3e7987d0"

我收到以下异常“java.lang.IllegalArgumentException:未知实体:com.my.persistence.L$$EnhancerByCGLIB$$3e7987d0”

After looking into it for a while, I finally figured out that it was because I was using the EntityManager.getReference() method that I was getting the above exception as the method was returning a proxy.

在研究了一段时间后,我终于发现这是因为我正在使用 EntityManager.getReference() 方法,当该方法返回代理时,我收到了上述异常。

This makes me wonder, when is it advisable to use the EntityManager.getReference() method instead of the EntityManager.find() method?

这让我想知道,什么时候最好使用 EntityManager.getReference() 方法而不是 EntityManager.find() 方法

EntityManager.getReference() throws an EntityNotFoundException if it cant find the entity being searched for which is very convenient in itself. EntityManager.find() method merely returns null if it cant find the entity.

如果 EntityManager.getReference() 找不到正在搜索的实体,它会抛出一个 EntityNotFoundException ,这本身就很方便。EntityManager.find() 方法仅在找不到实体时返回 null。

With regards to transaction boundaries, sounds to me like you would need to use the find() method before passing the newly found entity to a new transaction. If you use the getReference() method then you would probably end up in a situation similar to mine with the above exception.

关于事务边界,在我看来,您需要在将新找到的实体传递给新事务之前使用 find() 方法。如果您使用 getReference() 方法,那么您可能会遇到与我类似的情况,但有上述例外情况。

采纳答案by Arthur Ronald

I usually use getReferencemethod when i do not need to access database state (I mean getter method). Just to change state (I mean setter method). As you should know, getReference returns a proxy object which uses a powerful feature called automatic dirty checking. Suppose the following

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

}

If i call findmethod, JPA provider, behind the scenes, will call

如果我调用find方法,幕后的 JPA 提供者将调用

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

If i call getReferencemethod, JPA provider, behind the scenes, will call

如果我调用getReference方法,幕后的 JPA 提供者将调用

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

And you know why ???

你知道为什么吗???

When you call getReference, you will get a proxy object. Something like this one (JPA provider takes care of implementing this proxy)

当您调用 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;
    }

}

So before transaction commit, JPA provider will see stateChanged flag in order to update OR NOT person entity. If no rows is updated after update statement, JPA provider will throw EntityNotFoundException according to JPA specification.

所以在事务提交之前,JPA 提供者将看到 stateChanged 标志以更新 OR NOT 人实体。如果在更新语句后没有更新行,JPA 提供程序将根据 JPA 规范抛出 EntityNotFoundException。

regards,

问候,

回答by Steven Spungin

Because a reference is 'managed', but not hydrated, it can also allow you to remove an entity by ID, without needing to load it into memory first.

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

As you can't remove an unmanaged entity, it's just plain silly to load all fields using find(...) or createQuery(...), only to immediately delete it.

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

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

回答by davidxxx

This makes me wonder, when is it advisable to use the EntityManager.getReference() method instead of the EntityManager.find() method?

这让我想知道,什么时候最好使用 EntityManager.getReference() 方法而不是 EntityManager.find() 方法?

EntityManager.getReference()is really an error prone method and there is really very few cases where a client code needs to use it.
Personally, I never needed to use it.

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

EntityManager.getReference() and EntityManager.find() : no difference in terms of overhead

EntityManager.getReference() 和 EntityManager.find() :在开销方面没有区别

I disagree with the accepted answer and particularly :

我不同意接受的答案,特别是:

If i call findmethod, JPA provider, behind the scenes, will call

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

If i call getReferencemethod, JPA provider, behind the scenes, will call

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

如果我调用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 = ?

It is not the behavior that I get with Hibernate 5 and the javadoc of getReference()doesn't say such a thing :

这不是我在 Hibernate 5 中得到的行为,并且 javadocgetReference()没有说这样的话:

Get an instance, whose state may be lazily fetched. If the requested instance does not exist in the database, the EntityNotFoundException is thrown when the instance state is first accessed. (The persistence provider runtime is permitted to throw the EntityNotFoundException when getReference is called.) The application should not expect that the instance state will be available upon detachment, unless it was accessed by the application while the entity manager was open.

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

EntityManager.getReference()spares a query to retrieve the entity in two cases :

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

1) if the entity is stored in the Persistence context, that is the first level cache.
And this behavior is not specific to EntityManager.getReference(), EntityManager.find()will also spare a query to retrieve the entity if the entity is stored in the Persistence context.

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

You can check the first point with any example.
You can also rely on the actual Hibernate implementation.
Indeed, EntityManager.getReference()relies on the createProxyIfNecessary()method of the org.hibernate.event.internal.DefaultLoadEventListenerclass to load the entity.
Here is its implementation :

您可以使用任何示例检查第一点。
您还可以依赖实际的 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;
}

The interesting part is :

有趣的部分是:

Object existing = persistenceContext.getEntity( keyToLoad );

2) If we don't effectively manipulate the entity, echoing to the lazily fetchedof the javadoc.
Indeed, to ensure the effective loading of the entity, invoking a method on it is required.
So the gain would be related to a scenario where we want to load a entity without having the need to use it ? In the frame of applications, this need is really uncommon and in addition the getReference()behavior is also very misleading if you read the next part.

2)如果我们没有有效地操作实体,就会与javadoc的懒惰获取相呼应。
实际上,为了确保实体的有效加载,需要对其调用方法。
那么增益将与我们想要加载一个实体而不需要使用它的场景有关?在应用程序框架中,这种需求确实不常见,此外,getReference()如果您阅读下一部分,这种行为也非常具有误导性。

Why favor EntityManager.find() over EntityManager.getReference()

为什么偏爱 EntityManager.find() 而不是 EntityManager.getReference()

In terms of overhead, getReference()is not better than find()as discussed in the previous point.
So why use the one or the other ?

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

Invoking getReference()may return a lazily fetched entity.
Here, the lazy fetching doesn't refer to relationships of the entity but the entity itself.
It means that if we invoke getReference()and then the Persistence context is closed, the entity may be never loaded and so the result is really unpredictable. For example if the proxy object is serialized, you could get a nullreference as serialized result or if a method is invoked on the proxy object, an exception such as LazyInitializationExceptionis thrown.

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

It means that the throw of EntityNotFoundExceptionthat is the main reason to use getReference()to handle an instance that does not exist in the database as an error situation may be never performed while the entity is not existing.

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

EntityManager.find()doesn't have the ambition of throwing EntityNotFoundExceptionif the entity is not found. Its behavior is both simple and clear. You will never have surprise as it returns always a loaded entity or null(if the entity is not found) but never an entity under the shape of a proxy that may not be effectively loaded.
So EntityManager.find()should be favored in the very most of cases.

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

回答by Vlad Mihalcea

As I explained in this article, assuming you have a parent Postentity and a child PostCommentas illustrated in the following diagram:

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

enter image description here

在此处输入图片说明

If you call findwhen you try to set the @ManyToOnepostassociation:

如果find在尝试设置@ManyToOnepost关联时调用:

PostComment comment = new PostComment();
comment.setReview("Just awesome!");

Post post = entityManager.find(Post.class, 1L);
comment.setPost(post);

entityManager.persist(comment);

Hibernate will execute the following statements:

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)

The SELECT query is useless this time because we don't need the Post entity to be fetched. We only want to set the underlying post_id Foreign Key column.

这次 SELECT 查询没有用,因为我们不需要获取 Post 实体。我们只想设置底层的 post_id 外键列。

Now, if you use getReferenceinstead:

现在,如果您getReference改为使用:

PostComment comment = new PostComment();
comment.setReview("Just awesome!");

Post post = entityManager.getReference(Post.class, 1L);
comment.setPost(post);

entityManager.persist(comment);

This time, Hibernate will issue just the INSERT statement:

这一次,Hibernate 将只发出 INSERT 语句:

INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Just awesome!', 1)

Unlike find, the getReferenceonly returns an entity Proxy which only has the identifier set. If you access the Proxy, the associated SQL statement will be triggered as long as the EntityManager is still open.

与 不同findgetReferenceonly 返回一个只有标识符集的实体代理。如果访问 Proxy,只要 EntityManager 仍然打开,就会触发关联的 SQL 语句。

However, in this case, we don't need to access the entity Proxy. We only want to propagate the Foreign Key to the underlying table record so loading a Proxy is sufficient for this use case.

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

When loading a Proxy, you need to be aware that a LazyInitializationException can be thrown if you try to access the Proxy reference after the EntityManager is closed. For more details about handling the LazyInitializationException, check out this article.

在加载 Proxy 时,您需要注意,如果您在 EntityManager 关闭后尝试访问 Proxy 引用,则可能会抛出 LazyInitializationException。有关处理的更多详细信息LazyInitializationException,请查看这篇文章

回答by Anil Kumar

I disagree with the selected answer, and as davidxxx correctly pointed out, getReference does not provide such behaviour of dynamic updations without select. I asked a question concerning the validity of this answer, see here - cannot update without issuing select on using setter after getReference() of hibernate JPA.

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

I quite honestly haven't seen anybody who's actually used that functionality. ANYWHERE. And i don't understand why it's so upvoted.

老实说,我还没有看到任何人真正使用过该功能。任何地方。我不明白为什么它如此受欢迎。

Now first of all, no matter what you call on a hibernate proxy object, a setter or a getter, an SQL is fired and the object is loaded.

现在首先,无论您在休眠代理对象、setter 或 getter 上调用什么,都会触发 SQL 并加载对象。

But then i thought, so what if JPA getReference() proxy doesn't provide that functionality. I can just write my own proxy.

但后来我想,如果 JPA getReference() 代理不提供该功能怎么办。我可以编写自己的代理。

Now, we can all argue that selects on primary keys are as fast as a query can get and it's not really something to go to great lengths to avoid. But for those of us who can't handle it due to one reason or another, below is an implementation of such a proxy. But before i you see the implementation, see it's usage and how simple it is to use.

现在,我们都可以争辩说,主键上的选择与查询可以获得的速度一样快,而且实际上并不是要竭尽全力避免。但是对于我们这些由于某种原因无法处理它的人来说,下面是这种代理的实现。但是在您看到实现之前,请先了解它的用法以及使用起来有多简单。

USAGE

用法

Order example = ProxyHandler.getReference(Order.class, 3);
example.setType("ABCD");
example.setCost(10);
PersistenceService.save(example);

And this would fire the following query -

这将触发以下查询 -

UPDATE Order SET type = 'ABCD' and cost = 10 WHERE id = 3;

and even if you want to insert, you can still do PersistenceService.save(new Order("a", 2)); and it would fire an insert as it should.

即使你想插入,你仍然可以做 PersistenceService.save(new Order("a", 2)); 它会按它应该的方式触发插入。

IMPLEMENTATION

执行

Add this to your pom.xml -

将此添加到您的 pom.xml -

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.10</version>
</dependency>

Make this class to create dynamic proxy -

让这个类创建动态代理 -

@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());
    }
}

Make an interface with all the methods -

为所有方法制作一个接口 -

public interface EnhancedProxy {
    public String getJPQLUpdate();
    public HashMap<String, Object> getModifiedFields();
}

Now, make an interceptor which will allow you to implement these methods on your proxy -

现在,制作一个拦截器,它允许您在代理上实现这些方法 -

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!");
}
}

And the exception class -

和异常类 -

public class ProxyInstantiationException extends RuntimeException {
public ProxyInstantiationException(String message) {
    super(message);
}

A service to save using this proxy -

使用此代理保存的服务 -

@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);
    }

}
}