web-dev-qa-db-ja.com

JPAでEntityManager.find()とEntityManager.getReference()を使用する場合

私は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:Unknown entity:com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0」という例外が発生していました

しばらく調べてみると、EntityManager.getReference()メソッドを使用していたために、メソッドがプロキシを返しているときに上記の例外が発生していることがわかりました。

EntityManager.find()メソッドの代わりにEntityManager.getReference()メソッドを使用することをお勧めします

EntityManager.getReference()は、それ自体で非常に便利な検索対象のエンティティが見つからない場合、EntityNotFoundExceptionをスローします。 EntityManager.find()メソッドは、エンティティを見つけることができない場合、単にnullを返します。

トランザクションの境界に関しては、新しく見つかったエンティティを新しいトランザクションに渡す前にfind()メソッドを使用する必要があるように思えます。 getReference()メソッドを使用すると、おそらく上記の例外を除いて、私のような状況になります。

96
SibzTer

データベースの状態にアクセスする必要がないときは、通常getReferenceメソッドを使用します(getterメソッドを意味します)。状態を変更するだけです(セッターメソッドを意味します)。ご存じのとおり、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 methodを呼び出すと、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プロバイダーはOR NOT個人エンティティを更新するためにstateChangedフラグを確認します。 updateステートメントの後に行が更新されない場合、JPAプロバイダーはJPA仕様に従ってEntityNotFoundExceptionをスローします。

よろしく、

145
Arthur Ronald

この記事 で説明したように、次の図に示すように、親Postエンティティと子PostCommentがあると仮定します。

enter image description here

@ManyToOnefind関連付けを設定しようとするときに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の処理の詳細については、 この記事 をご覧ください。

12
Vlad Mihalcea

参照は「管理」されていますが、水和されていないため、最初にメモリにロードする必要なく、IDによってエンティティを削除することもできます。

管理されていないエンティティを削除することはできないため、find(...)またはcreateQuery(...)を使用してすべてのフィールドをロードし、ただそれをすぐに削除するのは単純に愚かなことです。

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

これは、EntityManager.find()メソッドの代わりにEntityManager.getReference()メソッドを使用することが推奨されるのはいつですか?

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()は、次の2つの場合にエンティティを取得するクエリを省略します。

1)エンティティが永続コンテキストに保存されている場合、それは一次キャッシュです。
この動作は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のlazily fetchedにエコーします。
実際、エンティティの効果的なロードを確実に行うには、エンティティのメソッドを呼び出す必要があります。
それで、ゲインは、エンティティを使用する必要なしにロードするシナリオに関連しますか?アプリケーションのフレームでは、この必要性は非常にまれであり、さらに次のパートを読むとgetReference()動作も非常に誤解を招く可能性があります。

EntityManager.getReference()よりもEntityManager.find()を優先する理由

オーバーヘッドの観点では、getReference()は前のポイントで説明したfind()よりも優れていません。
では、どちらを使用するのですか?

getReference()を呼び出すと、遅延フェッチされたエンティティが返される場合があります。
ここで、遅延フェッチはエンティティの関係ではなく、エンティティ自体を指します。
これは、getReference()を呼び出した後、永続コンテキストが閉じられると、エンティティがロードされない可能性があるため、結果が実際に予測不能であることを意味します。たとえば、プロキシオブジェクトがシリアル化されている場合、シリアル化された結果としてnull参照を取得できます。または、プロキシオブジェクトでメソッドが呼び出された場合、LazyInitializationExceptionなどの例外がスローされます。

これは、getReference()を使用してデータベースに存在しないインスタンスをエラー状態として処理する主な理由であるEntityNotFoundExceptionのスローは、エンティティが存在しない間は実行されない可能性があることを意味します。

EntityManager.find()には、エンティティが見つからない場合にEntityNotFoundExceptionをスローするという野望がありません。その動作は単純で明確です。常にロードされたエンティティまたはnull(エンティティが見つからない場合)を返すため、驚くことはありませんが、プロキシの形状の下にあるエンティティが効果的にロードされない可能性があります。
したがって、ほとんどの場合、EntityManager.find()を優先する必要があります。

3
davidxxx

私は選択した答えに同意せず、davidxxxが正しく指摘しているように、getReferenceは動的な更新の動作を選択なしで提供しません。この回答の有効性について質問しました。こちらをご覧ください- hibernate JPA のgetReference()の後にセッターを使用してselectを発行しないと更新できません。

正直言って、実際にその機能を使った人は誰もいません。どこでも。そして、どうしてそんなに支持されているのか分かりません。

まず、Hibernateプロキシオブジェクト、セッター、またはゲッターで何を呼び出しても、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);
    }

}
}
0
Anil Kumar