21

一意だが主キーではないキーに基づいて Hibernate オブジェクトを返すメソッドを作成しようとしています。エンティティがデータベースに既に存在する場合はそれを返したいのですが、そうでない場合は、新しいインスタンスを作成して保存してから返したいと思います。

更新:これを書いているアプリケーションは、基本的に入力ファイルのバッチプロセッサであることを明確にしましょう。システムは、ファイルを 1 行ずつ読み取り、データベースにレコードを挿入する必要があります。ファイル形式は基本的に、スキーマ内の複数のテーブルの非正規化ビューであるため、親レコードを解析してデータベースに挿入し、新しい合成キーを取得するか、既に存在する場合はそれを選択する必要があります。次に、そのレコードに戻る外部キーを持つ他のテーブルに関連するレコードを追加できます。

これが難しい理由は、各ファイルを完全にインポートするか、まったくインポートしない必要があるためです。つまり、特定のファイルに対して行われるすべての挿入と更新は、1 つのトランザクションの一部である必要があります。すべてのインポートを実行するプロセスが 1 つしかない場合、これは簡単ですが、可能であれば、これを複数のサーバーに分割したいと考えています。これらの制約のため、1 つのトランザクション内にとどまることができる必要がありますが、レコードが既に存在する例外を処理する必要があります。

親レコードのマップされたクラスは次のようになります。

@Entity
public class Foo {
    @Id
    @GeneratedValue(strategy = IDENTITY)
    private int id;
    @Column(unique = true)
    private String name;
    ...
}

このメソッドを書く最初の試みは次のとおりです。

public Foo findOrCreate(String name) {
    Foo foo = new Foo();
    foo.setName(name);
    try {
        session.save(foo)
    } catch(ConstraintViolationException e) {
        foo = session.createCriteria(Foo.class).add(eq("name", name)).uniqueResult();
    }
    return foo;
}

問題は、探している名前が存在する場合、uniqueResult() の呼び出しによって org.hibernate.AssertionFailure 例外がスローされることです。完全なスタック トレースは以下のとおりです。

org.hibernate.AssertionFailure: null id in com.searchdex.linktracer.domain.LinkingPage entry (don't flush the Session after an exception occurs)
    at org.hibernate.event.def.DefaultFlushEntityEventListener.checkId(DefaultFlushEntityEventListener.java:82) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.event.def.DefaultFlushEntityEventListener.getValues(DefaultFlushEntityEventListener.java:190) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.event.def.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:147) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.event.def.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:219) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:99) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.event.def.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:58) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.impl.SessionImpl.autoFlushIfRequired(SessionImpl.java:1185) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1709) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.impl.CriteriaImpl.list(CriteriaImpl.java:347) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.impl.CriteriaImpl.uniqueResult(CriteriaImpl.java:369) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]

この例外がスローされる原因を知っている人はいますか? 休止状態はこれを達成するためのより良い方法をサポートしていますか?

また、最初に挿入してから、それが失敗したかどうか、いつ失敗したかを選択する理由を先制的に説明しましょう。これは分散環境で機能する必要があるため、チェック全体で同期して、レコードが既に存在するかどうかと挿入を確認できません。これを行う最も簡単な方法は、すべての挿入で制約違反をチェックして、データベースにこの同期を処理させることです。

4

9 に答える 9

12

複数の JVM でプロセスを実行する、同様のバッチ処理要件がありました。そのために私が取ったアプローチは次のとおりです。jtahlborn の提案と非常によく似ています。ただし、vbence が指摘したように、NESTED トランザクションを使用すると、制約違反の例外が発生すると、セッションが無効になります。代わりに、現在のトランザクションを中断し、新しい独立したトランザクションを作成する REQUIRES_NEW を使用します。新しいトランザクションがロールバックされても、元のトランザクションには影響しません。

私は Spring の TransactionTemplate を使用していますが、Spring に依存したくない場合は簡単に翻訳できると確信しています。

public T findOrCreate(final T t) throws InvalidRecordException {
   // 1) look for the record
   T found = findUnique(t);
   if (found != null)
     return found;
   // 2) if not found, start a new, independent transaction
   TransactionTemplate tt = new TransactionTemplate((PlatformTransactionManager)
                                            transactionManager);
   tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
   try {
     found = (T)tt.execute(new TransactionCallback<T>() {
        try {
            // 3) store the record in this new transaction
            return store(t);
        } catch (ConstraintViolationException e) {
            // another thread or process created this already, possibly
            // between 1) and 2)
            status.setRollbackOnly();
            return null;
        }
     });
     // 4) if we failed to create the record in the second transaction, found will
     // still be null; however, this would happy only if another process
     // created the record. let's see what they made for us!
     if (found == null)
        found = findUnique(t);
   } catch (...) {
     // handle exceptions
   }
   return found;
}
于 2011-04-28T17:20:23.780 に答える
11

You need to use UPSERT or MERGE to achieve this goal.

However, Hibernate does not offer support for this construct, so you need to use jOOQ instead.

private PostDetailsRecord upsertPostDetails(
        DSLContext sql, Long id, String owner, Timestamp timestamp) {
    sql
    .insertInto(POST_DETAILS)
    .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON)
    .values(id, owner, timestamp)
    .onDuplicateKeyIgnore()
    .execute();

    return sql.selectFrom(POST_DETAILS)
    .where(field(POST_DETAILS.ID).eq(id))
    .fetchOne();
}

Calling this method on PostgreSQL:

PostDetailsRecord postDetailsRecord = upsertPostDetails(
    sql, 
    1L, 
    "Alice",
    Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))
);

Yields the following SQL statements:

INSERT INTO "post_details" ("id", "created_by", "created_on") 
VALUES (1, 'Alice',  CAST('2016-08-11 12:56:01.831' AS timestamp))
ON CONFLICT  DO NOTHING;
    
SELECT "public"."post_details"."id",
       "public"."post_details"."created_by",
       "public"."post_details"."created_on",
       "public"."post_details"."updated_by",
       "public"."post_details"."updated_on"
FROM "public"."post_details"
WHERE "public"."post_details"."id" = 1

On Oracle and SQL Server, jOOQ will use MERGE while on MySQL it will use ON DUPLICATE KEY.

The concurrency mechanism is ensured by the row-level locking mechanism employed when inserting, updating, or deleting a record, which you can view in the following diagram:

enter image description here

Code avilable on GitHub.

于 2017-11-03T11:51:59.053 に答える
8

2つの解決策が思い浮かびます:

それがTABLE LOCKSの目的です

Hibernate はテーブル ロックをサポートしていませんが、便利な場合はこのような状況です。幸いなことに、ネイティブ SQL を を通じて使用できますSession.createSQLQuery()。例 (MySQL の場合):

// no access to the table for any other clients
session.createSQLQuery("LOCK TABLES foo WRITE").executeUpdate();

// safe zone
Foo foo = session.createCriteria(Foo.class).add(eq("name", name)).uniqueResult();
if (foo == null) {
    foo = new Foo();
    foo.setName(name)
    session.save(foo);
}

// releasing locks
session.createSQLQuery("UNLOCK TABLES").executeUpdate();

このようにして、セッション (クライアント接続) がロックを取得すると、操作が終了してロックが解放されるまで、他のすべての接続がブロックされます。読み取り操作は他の接続に対してもブロックされるため、言うまでもなく、これはアトミック操作の場合にのみ使用してください。

Hibernate のロックはどうですか?

Hibernate は行レベルのロックを使用します。存在しない行をロックできないため、直接使用することはできません。しかし、単一のレコードを持つダミーテーブルを作成し、それを ORM にマップしてから、SELECT ... FOR UPDATEそのオブジェクトのスタイル ロックを使用してクライアントを同期することができます。基本的に、作業中に他のクライアント (同じソフトウェアを同じ規則で実行している) が競合する操作を実行しないことを確認する必要があるだけです。

// begin transaction
Transaction transaction = session.beginTransaction();

// blocks until any other client holds the lock
session.load("dummy", 1, LockOptions.UPGRADE);

// virtual safe zone
Foo foo = session.createCriteria(Foo.class).add(eq("name", name)).uniqueResult();
if (foo == null) {
    foo = new Foo();
    foo.setName(name)
    session.save(foo);
}

// ends transaction (releasing locks)
transaction.commit();

データベースはSELECT ... FOR UPDATE構文を知っている必要があり (Hibernate はそれを使用するつもりです)、もちろん、これはすべてのクライアントが同じ規則を持っている場合にのみ機能します (同じダミーエンティティをロックする必要があります)。

于 2011-04-27T23:53:54.527 に答える
2

数人の人々が全体的な戦略のさまざまな部分について言及しました。一般に、新しいオブジェクトを作成するよりも、既存のオブジェクトを頻繁に見つけることを期待していると仮定します。

  • 名前で既存のオブジェクトを検索します。見つかった場合は、
  • ネストされた(個別の)トランザクションを開始する
    • 新しいオブジェクトを挿入してみてください
    • ネストされたトランザクションをコミットする
  • ネストされたトランザクションからの失敗をキャッチし、制約違反以外の場合は、再スローします
  • それ以外の場合は、既存のオブジェクトを名前で検索して返します

明確にするために、別の回答で指摘されているように、「ネストされた」トランザクションは実際には別個のトランザクションです(多くのデータベースは真のネストされたトランザクションさえサポートしていません)。

于 2011-04-28T00:20:40.103 に答える
2

トランザクションと例外に関する Hibernateのドキュメントには、すべての HibernateExceptions は回復不能であり、現在のトランザクションは発生したらすぐにロールバックする必要があると記載されています。これは、上記のコードが機能しない理由を説明しています。最終的に、トランザクションを終了してセッションを閉じることなく HibernateException をキャッチするべきではありません。

これを実現する唯一の現実的な方法は、古いセッションの終了と新しいセッションの再開をメソッド内で管理することです。既存のトランザクションに参加でき、分散環境内で安全な findOrCreate メソッドを実装することは、私が見つけたものに基づいて、Hibernate を使用して不可能に思われます。

于 2011-02-16T23:41:40.000 に答える
2

解決策は実際には非常に簡単です。まず、名前の値を使用して選択を実行します。結果が見つかった場合は、それを返します。そうでない場合は、新しいものを作成します。作成が失敗した場合 (例外あり)、これは別のクライアントが select ステートメントと insert ステートメントの間にまったく同じ値を追加したためです。これは、例外があることは論理的です。それをキャッチし、トランザクションをロールバックして、同じコードを再度実行してください。行は既に存在するため、select ステートメントはそれを検出し、オブジェクトを返します。

ここで、hibernate を使用した楽観的および悲観的ロックの戦略の説明を参照できます: http://docs.jboss.org/hibernate/core/3.3/reference/en/html/transactions.html

于 2011-04-28T09:15:39.407 に答える
1

それを行う 1 つの方法を次に示しますが、すべての状況に適しているわけではありません。

  • Foo で、 の「unique = true」属性を削除しますname。挿入ごとに更新されるタイムスタンプを追加します。
  • ではfindOrCreate()、指定された名前のエンティティが既に存在するかどうかを確認する必要はありません。毎回新しいエンティティを挿入するだけです。
  • で Foo インスタンスを検索するnameと、同じ名前のインスタンスが 0 個以上存在する場合があるため、最新のものを選択するだけです。

この方法の良いところは、ロックを必要としないため、すべてが非常に高速に実行されることです。欠点は、データベースに古くなったレコードが散らばっていることです。そのため、それらを処理するために別の場所で何かをしなければならない場合があります。また、他のテーブルがその によって Foo を参照する場合id、これはそれらの関係を台無しにします。

于 2011-02-16T23:42:11.620 に答える
0

戦略を変更する必要があるかもしれません。最初にその名前のユーザーを見つけ、そのユーザーが存在しない場合にのみ作成します。

于 2011-02-16T23:06:03.400 に答える
0

次の戦略を試してみます。

A. _ メイン トランザクションを開始します (時点 1)
B . サブトランザクションを開始します (時間 2)

これで、時間 1 の後に作成されたオブジェクトはメイン トランザクションで表示されなくなります。だからあなたがするとき

C. _ 新しい競合状態オブジェクトを作成し、サブトランザクション
Dをコミットします。新しいサブトランザクションを開始し (時点 3)、クエリからオブジェクトを取得することによって競合を処理します (ポイント B からのサブトランザクションは現在スコープ外です)。

オブジェクトの主キーのみを返し、EntityManager.getReference(..) を使用して、メイン トランザクションで使用するオブジェクトを取得します。または、D の後にメイン トランザクションを開始します。メイン トランザクション内でいくつの競合状態が発生するかは完全にはわかりませんが、上記では「大規模な」トランザクションで BCD の n 倍が許容されるはずです。

マルチスレッド (CPU ごとに 1 つのスレッド) を実行したい場合は、この種の競合に対して共有静的キャッシュを使用することで、この問題を大幅に軽減できることに注意してください。ポイント 2 は「楽観的」に保つことができます。最初に .find(..) 。

編集: 新しいトランザクションの場合、トランザクション タイプREQUIRES_NEWで注釈が付けられた EJB インターフェイス メソッド呼び出しが必要です。

編集: getReference(..) が思ったとおりに機能することを再確認してください。

于 2011-05-02T01:05:41.547 に答える