web-dev-qa-db-ja.com

JPA-create-if-not-existsエンティティ?

JPA/Hibernateアプリケーションにいくつかのマップされたオブジェクトがあります。ネットワーク上で、これらのオブジェクトの更新を表すパケット、または実際には新しいオブジェクト全体を表すパケットを受信します。

次のようなメソッドを記述したい

<T> T getOrCreate(Class<T> klass, Object primaryKey)

pk primaryKeyを使用してデータベースにオブジェクトが存在する場合は、指定されたクラスのオブジェクトを返し、そうでない場合は、そのクラスの新しいオブジェクトを作成して永続化して返します。

次にオブジェクトで行うことは、トランザクション内ですべてのフィールドを更新することです。

JPAでこれを行う慣用的な方法はありますか、または私の問題を解決するより良い方法はありますか?

23
HenryR

<T> T getOrCreate(Class<T> klass, Object primaryKey)のようなメソッドを書きたい

これは簡単ではありません。

素朴なアプローチは次のようなことです(メソッドがトランザクション内で実行されていると仮定します)。

public <T> T findOrCreate(Class<T> entityClass, Object primaryKey) {
    T entity = em.find(entityClass, primaryKey);
    if ( entity != null ) {
        return entity;
    } else {
        try {
            entity = entityClass.newInstance();
            /* use more reflection to set the pk (probably need a base entity) */
            return entity;
        } catch ( Exception e ) {
            throw new RuntimeException(e);
        }
    }
}

ただし、並行環境では、このコードは競合状態のために失敗する可能性があります。

 T1:BEGIN TX; 
 T2:BEGIN TX; 
 
 T1:SELECT w/id = 123; //null
T2を返します:SELECT w/id = 123; // nullを返します
 
 T1:INSERT w/id = 123; 
 T1:COMMIT; //行が挿入されました
 
 T2:INSERT w/name = 123; 
 T2:COMMIT; //制約違反

また、複数のJVMを実行している場合、同期は役に立ちません。テーブルロックを取得しないと(これはかなり恐ろしいことです)、これを解決する方法がわかりません。

そのような場合、最初に体系的に挿入し、可能な例外を処理して後続の選択を(新しいトランザクションで)実行する方が良いのではないかと思います。

上記の制約に関する詳細を追加する必要があります(マルチスレッド?分散環境?)。

20
Pascal Thivent

純粋なJPAを使用すると、ネストされたエンティティマネージャーを備えたマルチスレッドソリューションでこれを楽観的に解決できます(実際には、ネストされたトランザクションが必要ですが、純粋なJPAでは可能ではないと思います)。基本的に、検索または作成操作をカプセル化するマイクロトランザクションを作成する必要があります。このパフォーマンスは素晴らしいものではなく、大規模な一括作成には適していませんが、ほとんどの場合十分です。

前提条件:

  • エンティティには、2つのインスタンスが作成された場合に失敗する一意の制約違反が必要です
  • エンティティを検索するためのFinderの種類があります(EntityManager.findの主キーまたはクエリによって検索できます)。これをFinderと呼びます。
  • 探しているエンティティが存在しない場合に備えて、新しいエンティティを作成するための何らかのファクトリメソッドがあります。これをfactoryと呼びます。
  • 与えられたfindOrCreateメソッドがいくつかのリポジトリオブジェクトに存在し、既存のエンティティマネージャーと既存のトランザクションのコンテキストで呼び出されると想定しています。
  • トランザクション分離レベルがシリアライズ可能またはスナップショットの場合、これは機能しません。トランザクションが繰り返し可能な読み取りの場合、現在のトランザクションでエンティティの読み取りを試みてはなりません。
  • 保守性を高めるために、以下のロジックを複数の方法に分割することをお勧めします。

コード:

public <T> T findOrCreate(Supplier<T> Finder, Supplier<T> factory) {
    EntityManager innerEntityManager = entityManagerFactory.createEntityManager();
    innerEntityManager.getTransaction().begin();
    try {
        //Try the naive find-or-create in our inner entity manager
        if(Finder.get() == null) {
            T newInstance = factory.get();
            innerEntityManager.persist(newInstance);
        }
        innerEntityManager.getTransaction().commit();
    } catch (PersistenceException ex) {
        //This may be a unique constraint violation or it could be some
        //other issue.  We will attempt to determine which it is by trying
        //to find the entity.  Either way, our attempt failed and we
        //roll back the tx.
        innerEntityManager.getTransaction().rollback();
        T entity = Finder.get();
        if(entity == null) {
            //Must have been some other issue
            throw ex;
        } else {
            //Either it was a unique constraint violation or we don't
            //care because someone else has succeeded
            return entity;
        }
    } catch (Throwable t) {
        innerEntityManager.getTransaction().rollback();
        throw t;
    } finally {
        innerEntityManager.close();
    }
    //If we didn't hit an exception then we successfully created it
    //in the inner transaction.  We now need to find the entity in
    //our outer transaction.
    return Finder.get();
}
6
Pace