web-dev-qa-db-ja.com

レコードを削除してから作成すると、Spring DataJPAで重複キー違反が発生します

したがって、このシナリオでは、ヘッダーレコードを取得し、その詳細を削除してから、別の方法で詳細を再作成する必要があります。詳細を更新するのは非常に面倒です。

私は基本的に持っています:

@Transactional
public void create(Integer id, List<Integer> customerIDs) {

    Header header = headerService.findOne(id);
    // header is found, has multiple details

    // Remove the details
    for(Detail detail : header.getDetails()) {
        header.getDetails().remove(detail);
    }

    // Iterate through list of ID's and create Detail with other objects
    for(Integer id : customerIDs) {
        Customer customer = customerService.findOne(id);

        Detail detail = new Detail();
        detail.setCustomer(customer);

        header.getDetails().add(detail);
    }

    headerService.save(header);
}

これで、データベースには次のような制約があります。

Header
=================================
ID, other columns...

Detail
=================================
ID, HEADER_ID, CUSTOMER_ID

Customer
=================================
ID, other columns...

Constraint:  Details must be unique by HEADER_ID and CUSTOMER_ID so:

Detail  (VALID)
=================================
1, 123, 10
2, 123, 12

Detail  (IN-VALID)
=================================
1, 123, 10
1, 123, 10

OK、これを実行して2、3、20などの顧客を渡すと、以前に存在しなかった限り、すべてのDetailレコードが正常に作成されます。

別の顧客リストを渡して再度実行すると、最初にALLの詳細が削除され、次にNEWの詳細のリストが作成されると思います。

しかし、何が起こっているのかというと、作成前に削除が尊重されていないようです。エラーは重複キー制約であるためです。重複するキーは、上記の「無効」シナリオです。

データベースに一連の詳細を手動で入力し、CREATE details部分をコメントアウトすると(削除のみを実行)、レコードは問題なく削除されます。したがって、削除は機能します。作成は機能します。両方が一緒に機能しないというだけです。

より多くのコードが必要です。 Spring Data JPAを使用しています。

ありがとう

[〜#〜] update [〜#〜]

私のエンティティには、基本的に次の注釈が付けられています。

@Entity
@Table
public class Header {
...
    @OneToMany(mappedBy = "header", orphanRemoval = true, cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
    private Set<Detail> Details = new HashSet<>();

...
}

@Entity
@Table
public class Detail {
...
    @ManyToOne(optional = false)
    @JoinColumn(name = "HEADER_ID", referencedColumnName = "ID", nullable = false)
    private Header header;
...
}

更新2

@Klaus Groenbaek

実は、もともとは触れていませんでしたが、初めてそうしました。また、PERSISTが含まれていると思われるCascading.ALLを使用しています。

テストのために、コードを次のように更新しました。

@Transactional
public void create(Integer id, List<Integer> customerIDs) {

    Header header = headerService.findOne(id);

    // Remove the details
    detailRepository.delete(header.getDetails());       // Does not work

    // I've also tried this:
    for(Detail detail : header.getDetails()) {
        detailRepository.delete(detail);
    }


    // Iterate through list of ID's and create Detail with other objects
    for(Integer id : customerIDs) {
        Customer customer = customerService.findOne(id);

        Detail detail = new Detail();
        detail.setCustomer(customer);
        detail.setHeader(header);

        detailRepository.save(detail)
    }
}

繰り返しますが...繰り返しますが、直後に作成がない場合は削除が機能します。直前に削除がない場合、作成は機能します。ただし、データベースからの重複キー制約エラーのために、それらが一緒になっている場合はどちらも機能しません。

カスケード削除ありとなしで同じシナリオを試しました。

10
cbmeeks

これはかなり長い説明なので、帽子を握ってください。しかし、私があなたのコードを見ると、JPAがどのように機能するかについてのいくつかの重要な概念が欠けているように見えます。

まず、エンティティをコレクションに追加したり、エンティティをコレクションから削除したりしても、カスケードまたはorphanRemovalを使用して永続化操作が伝播されない限り、データベースで同じ操作が発生するわけではありません。

エンティティをデータベースに追加するには、EntityManager.persist()を直接呼び出すか、カスケード永続化を介して呼び出す必要があります。これは基本的にJPARepository.save()内で起こることです

エンティティを削除する場合は、EntityManager.remove()を直接呼び出すか、操作をカスケードするか、JpaRepository.delete()を介して呼び出す必要があります。

管理対象エンティティ(永続コンテキストにロードされるエンティティ)があり、トランザクション内の基本フィールド(非エンティティ、非コレクション)を変更した場合、この変更は、トランザクションがコミットされたときにデータベースに書き込まれます。 persist/saveを呼び出さなかった場合。永続性コンテキストは、ロードされたすべてのエンティティの内部コピーを保持し、トランザクションがコミットされると、内部コピーをループして現在の状態と比較し、基本的なファイルの変更が更新クエリをトリガーします。

新しいエンティティ(A)を別のエンティティ(B)のコレクションに追加したが、Aでpersistを呼び出していない場合、Aはデータベースに保存されません。 Bでpersistを呼び出すと、次の2つのいずれかが発生します。persist操作がカスケードされると、Aもデータベースに保存されます。永続化がカスケードされていない場合、管理対象エンティティが管理対象外エンティティを参照しているため、エラーが発生します。これにより、EclipseLinkで次のエラーが発生します。「同期中に、カスケードPERSISTとマークされていない関係を通じて新しいオブジェクトが見つかりました」。親エンティティとその子を同時に作成することが多いため、カスケード永続化は理にかなっています。

別のエンティティBのコレクションからエンティティAを削除する場合、Bを削除しないため、カスケードに依存することはできません。代わりに、Aでremoveを直接呼び出す必要がありますが、Bのコレクションから削除しても、 EntityManagerで永続化操作が呼び出されていないため、効果があります。 orphanRemovalを使用して削除をトリガーすることもできますが、この機能を使用するときは注意することをお勧めします。特に、永続化操作の仕組みに関する基本的な知識が不足しているようです。

通常、永続化操作と、それを適用する必要のあるエンティティについて考えると役立ちます。これは、私がコードを書いた場合のコードの外観です。

@Transactional
public void create(Integer id, List<Integer> customerIDs) {

    Header header = headerService.findOne(id);
    // header is found, has multiple details

    // Remove the details
    for(Detail detail : header.getDetails()) {
        em.remove(detail);
    }

    // em.flush(); // In some case you need to flush, see comments below

    // Iterate through list of ID's and create Detail with other objects
    for(Integer id : customerIDs) {
        Customer customer = customerService.findOne(id);

        Detail detail = new Detail();
        detail.setCustomer(customer);
        detail.setHeader(header);  // did this happen inside you service?
        em.persist(detail);
    }
}

まず、ヘッダーを永続化する理由はありません。ヘッダーは管理対象エンティティであり、変更する基本フィールドは、トランザクションがコミットされるときに変更されます。ヘッダーは、Detailsエンティティの外部キーです。つまり、すべての外部関係を設定し、新しいDetailsを保持する必要があるため、重要なのはdetail.setHeader(header);em.persist(details)です。 。同様に、ヘッダーから既存の詳細を削除することは、ヘッダーとは関係ありません。定義関係(外部キー)は詳細にあるため、永続コンテキストから詳細を削除すると、データベースから詳細が削除されます。 orphanRemovalを使用することもできますが、これにはトランザクションごとに追加のロジックが必要です。私の意見では、各永続化操作が明示的である場合、コードが読みやすくなります。そうすれば、アノテーションを読み取るためにエンティティに戻る必要がありません。

最後に、コード内の永続化操作のシーケンスは、データベースに対して実行されるクエリの順序に変換されません。 HibernateとEclipseLinkはどちらも、最初に新しいエンティティを挿入してから、既存のエンティティを削除します。私の経験では、これが「主キーがすでに存在する」という最も一般的な理由です。特定の主キーを持つエンティティを削除してから、同じ主キーを持つ新しいエンティティを追加すると、最初に挿入が行われ、キー違反が発生します。これは、JPAに現在の永続性状態をデータベースにフラッシュするように指示することで修正できます。 em.flush()は削除クエリをデータベースにプッシュするため、削除したものと同じ主キーを持つ別の行を挿入できます。

たくさんの情報でしたので、わからないことがあったら教えてください。

14
Klaus Groenbaek

まず、header.getDetails().remove(detail);を実行するだけでは、DBに対していかなる種類の操作も実行されません。 headerService.save(header);session.saveOrUpdate(header)のようなものを呼び出すと思います。

Hibernateは1回の操作で重複キーを持つエンティティを削除および作成する必要があるため、基本的にはある種の論理衝突ですが、順序はわかりませんこれらの操作を実行する必要があります。

少なくともheaderService.save(header);beforeを呼び出して、新しい詳細を追加することをお勧めします。つまり、次のようになります。

    // Remove the details
    for(Detail detail : header.getDetails()) {
        header.getDetails().remove(detail);
    }

    headerService.save(header);

    // Iterate through list of ID's and create Detail with other objects
    for(Integer id : customerIDs) {
        // ....
    }

    headerService.save(header);

hibernateに伝えるために:はい、コレクションから削除したこのエンティティを削除し、その後、新しいエンティティを追加します。

1
Andremoniy

原因は@ klaus-groenbaekによって説明されていますが、それを回避しているときに何か面白いことに気づきました。

Spring JpaRepositoryを使用しているときに、派生メソッドを使用しているときにこれを機能させることができませんでした。

したがって、以下は機能しません。

void deleteByChannelId(Long channelId);

ただし、明示的な(ModifyingQueryを指定すると正しく機能するため、次のように機能します。

@Modifying
@Query("delete from ClientConfigValue v where v.channelId = :channelId")
void deleteByChannelId(@Param("channelId") Long channelId);

この場合、ステートメントは正しい順序でコミット/永続化されます。