Hibernate / JPAを使用したバッチ挿入/更新

1. 概要

このチュートリアルでは、https://www.baeldung.com/the-persistence-layer-with-spring-and-jpa [Hibernate / JPA]を使用してエンティティをバッチ挿入または更新する方法を見ていきます。
バッチ処理により、1回のネットワーク呼び出しでデータベースにSQLステートメントのグループを送信できます。 このようにして、アプリケーションのネットワークとメモリ使用量を最適化できます。

2. セットアップ

2.1. サンプルデータモデル

例で使用するサンプルデータモデルを見てみましょう。
最初に、_School_エンティティを作成します。
@Entity
public class School {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String name;

    @OneToMany(mappedBy = "school")
    private List<Student> students;

    // Getters and setters...
}
各_School_には、0個以上の__Student__sがあります。
@Entity
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String name;

    @ManyToOne
    private School school;

    // Getters and setters...
}

2.2. SQLクエリのトレース

サンプルを実行するとき、挿入/更新ステートメントが実際にバッチで送信されることを確認する必要があります。 残念ながら、https://www.baeldung.com/sql-logging-spring-boot [Hibernateログステートメント]からSQLステートメントがバッチ処理されているかどうかを理解できません。 このため、データソースプロキシを使用してHibernate / JPA SQLステートメントをトレースします。
private static class ProxyDataSourceInterceptor implements MethodInterceptor {
    private final DataSource dataSource;
    public ProxyDataSourceInterceptor(final DataSource dataSource) {
        this.dataSource = ProxyDataSourceBuilder.create(dataSource)
            .name("Batch-Insert-Logger")
            .asJson().countQuery().logQueryToSysOut().build();
    }

    // Other methods...
}

3. デフォルトの動作

  • Hibernateはデフォルトでバッチ処理を有効にしません*。 これは、挿入/更新操作ごとに個別のSQLステートメントを送信することを意味します。

@Transactional
@Test
public void whenNotConfigured_ThenSendsInsertsSeparately() {
    for (int i = 0; i < 10; i++) {
        School school = createSchool(i);
        entityManager.persist(school);
    }
    entityManager.flush();
}
ここでは、10個の_School_エンティティを永続化しました。 クエリログを見ると、Hibernateが各insertステートメントを個別に送信していることがわかります。
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School1","1"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School2","2"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School3","3"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School4","4"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School5","5"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School6","6"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School7","7"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School8","8"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School9","9"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School10","10"]]
したがって、バッチ処理を有効にするようにHibernateを構成する必要があります。 この目的のために、* _ hibernate.jdbc.batch_size_プロパティを0より大きい数値に設定する必要があります*。
_EntityManager_を手動で作成する場合は、_hibernate.jdbc.batch_size_をHibernateプロパティに追加する必要があります。
public Properties hibernateProperties() {
    Properties properties = new Properties();
    properties.put("hibernate.jdbc.batch_size", "5");

    // Other properties...
    return properties;
}
Spring Bootを使用している場合、アプリケーションプロパティとして定義できます。
spring.jpa.properties.hibernate.jdbc.batch_size=5

4. 単一テーブルのバッチ挿入

4.1. 明示的なフラッシュなしのバッチ挿入

最初に、1つのエンティティタイプのみを処理する場合にバッチ挿入を使用する方法を見てみましょう。
前のコードサンプルを使用しますが、今回はバッチ処理が有効になっています。
@Transactional
@Test
public void whenInsertingSingleTypeOfEntity_thenCreatesSingleBatch() {
    for (int i = 0; i < 10; i++) {
        School school = createSchool(i);
        entityManager.persist(school);
    }
}
ここでは、10個の_School_エンティティを永続化しました。 ログを見ると、Hibernateがバッチでinsertステートメントを送信していることを確認できます。
"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School1","1"],["School2","2"],["School3","3"],["School4","4"],["School5","5"]]
"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School6","6"],["School7","7"],["School8","8"],["School9","9"],["School10","10"]]
ここで言及する1つの重要なことは、メモリ消費です。 *エンティティを永続化すると、Hibernateはそれを永続化コンテキストに保存します*。 たとえば、1つのトランザクションで100,000個のエンティティを保持すると、メモリに100,000個のエンティティインスタンスが存在することになり、_OutOfMemoryException_が発生する可能性があります。

4.2. 明示的なフラッシュを使用したバッチ挿入

次に、バッチ処理中のメモリ使用量を最適化する方法を見ていきます。 永続コンテキストの役割を深く掘り下げましょう。
まず、永続コンテキストは、新しく作成されたエンティティと変更されたエンティティをメモリに保存します。 Hibernateは、トランザクションが同期されると、これらの変更をデータベースに送信します。 これは通常、トランザクションの終了時に発生します。 ただし、* _ EntityManager.flush()_を呼び出すと、トランザクションの同期もトリガーされます*。
第二に、永続コンテキストはエンティティキャッシュとして機能するため、一次キャッシュとも呼ばれます。 *永続コンテキスト内のエンティティをクリアするには、* _ * EntityManager.clear()* ._を呼び出します。
そのため、バッチ処理中のメモリ負荷を減らすために、バッチサイズに達したときにアプリケーションコードで_EntityManager.flush()_および_EntityManager.clear()_を呼び出すことができます。
@Transactional
@Test
public void whenFlushingAfterBatch_ThenClearsMemory() {
    for (int i = 0; i < 10; i++) {
        if (i > 0 && i % BATCH_SIZE == 0) {
            entityManager.flush();
            entityManager.clear();
        }
        School school = createSchool(i);
        entityManager.persist(school);
    }
}
ここでは、永続コンテキストのエンティティをフラッシュして、Hibernateがデータベースにクエリを送信するようにします。 さらに、永続コンテキストをクリアすることで、_School_エンティティをメモリから削除しています。 バッチ処理の動作は変わりません。

5. 複数のテーブルのバッチ挿入

ここで、1つのトランザクションで複数のエンティティタイプを処理するときに、バッチ挿入を構成する方法を見てみましょう。
複数のタイプのエンティティを永続化する場合、Hibernateはエンティティタイプごとに異なるバッチを作成します。 これは、*単一のバッチには1つのタイプのエンティティしか存在できない*ためです。
さらに、Hibernateはinsertステートメントを収集するため、現在のバッチとは異なるエンティティタイプに遭遇すると、新しいバッチを作成します。 これは、そのエンティティタイプのバッチが既に存在する場合でも同様です。
@Transactional
@Test
public void whenThereAreMultipleEntities_ThenCreatesNewBatch() {
    for (int i = 0; i < 10; i++) {
        if (i > 0 && i % BATCH_SIZE == 0) {
            entityManager.flush();
            entityManager.clear();
        }
        School school = createSchool(i);
        entityManager.persist(school);
        Student firstStudent = createStudent(school);
        Student secondStudent = createStudent(school);
        entityManager.persist(firstStudent);
        entityManager.persist(secondStudent);
    }
}
ここでは、_School_を挿入し、2つの__Student__sを割り当てて、このプロセスを10回繰り返しています。
ログでは、Hibernateがサイズ1の複数のバッチで_School_ insertステートメントを送信しているのに対し、サイズ5の2つのバッチのみを想定していることがわかります。 さらに、_Student_ insertステートメントも、サイズ5の4つのバッチではなく、サイズ2のいくつかのバッチで送信されます。
"batch":true, "querySize":1, "batchSize":1, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School1","1"]]
"batch":true, "querySize":1, "batchSize":2, "query":["insert into student (name, school_id, id)
  values (?, ?, ?)"], "params":[["Student-School1","1","2"],["Student-School1","1","3"]]
"batch":true, "querySize":1, "batchSize":1, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School2","4"]]
"batch":true, "querySize":1, "batchSize":2, "query":["insert into student (name, school_id, id)
  values (?, ?, ?)"], "params":[["Student-School2","4","5"],["Student-School2","4","6"]]
"batch":true, "querySize":1, "batchSize":1, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School3","7"]]
"batch":true, "querySize":1, "batchSize":2, "query":["insert into student (name, school_id, id)
  values (?, ?, ?)"], "params":[["Student-School3","7","8"],["Student-School3","7","9"]]
Other log lines...
同じエンティティタイプのすべての挿入ステートメントをバッチ処理するには、* _ hibernate.order_inserts_プロパティを設定する必要があります*。
_EntityManagerFactory_を使用して、Hibernateプロパティを手動で構成できます。
public Properties hibernateProperties() {
    Properties properties = new Properties();
    properties.put("hibernate.order_inserts", "true");

    // Other properties...
    return properties;
}
Spring Bootを使用している場合、application.propertiesでプロパティを構成できます。
spring.jpa.properties.hibernate.order_inserts=true
このプロパティを追加すると、_School_挿入用に1つのバッチと_Student_挿入用に2つのバッチができます。
"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"],
  "params":[["School6","16"],["School7","19"],["School8","22"],["School9","25"],["School10","28"]]
"batch":true, "querySize":1, "batchSize":5, "query":["insert into student (name, school_id, id)
  values (?, ?, ?)"], "params":[["Student-School6","16","17"],["Student-School6","16","18"],
  ["Student-School7","19","20"],["Student-School7","19","21"],["Student-School8","22","23"]]
"batch":true, "querySize":1, "batchSize":5, "query":["insert into student (name, school_id, id)
  values (?, ?, ?)"], "params":[["Student-School8","22","24"],["Student-School9","25","26"],
  ["Student-School9","25","27"],["Student-School10","28","29"],["Student-School10","28","30"]]

6. バッチ更新

それでは、バッチ更新に移りましょう。 バッチ挿入と同様に、複数の更新ステートメントをグループ化し、それらを一度にデータベースに送信できます。
これを有効にするには、* _ hibernate.order_updates_および_hibernate.jdbc.batch_versioned_data_プロパティを設定します*。
_EntityManagerFactory_を手動で作成する場合は、プログラムでプロパティを設定できます。
public Properties hibernateProperties() {
    Properties properties = new Properties();
    properties.put("hibernate.order_updates", "true");
    properties.put("hibernate.batch_versioned_data", "true");

    // Other properties...
    return properties;
}
そして、Spring Bootを使用している場合は、それらをapplication.propertiesに追加するだけです。
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.batch_versioned_data=true
これらのプロパティを設定したら、Hibernateはバッチで更新ステートメントをグループ化する必要があります。
@Transactional
@Test
public void whenUpdatingEntities_thenCreatesBatch() {
    TypedQuery<School> schoolQuery =
      entityManager.createQuery("SELECT s from School s", School.class);
    List<School> allSchools = schoolQuery.getResultList();
    for (School school : allSchools) {
        school.setName("Updated_" + school.getName());
    }
}
ここで学校エンティティを更新し、Hibernateはサイズ5の2つのバッチでSQLステートメントを送信します。
"batch":true, "querySize":1, "batchSize":5, "query":["update school set name=? where id=?"],
  "params":[["Updated_School1","1"],["Updated_School2","2"],["Updated_School3","3"],
  ["Updated_School4","4"],["Updated_School5","5"]]
"batch":true, "querySize":1, "batchSize":5, "query":["update school set name=? where id=?"],
  "params":[["Updated_School6","6"],["Updated_School7","7"],["Updated_School8","8"],
  ["Updated_School9","9"],["Updated_School10","10"]]

7. _ @ Id_生成戦略

挿入/更新にバッチ処理を使用する場合、主キー生成戦略に注意する必要があります。 *エンティティが_GenerationType.IDENTITY_識別子ジェネレーターを使用する場合、http://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#batch-session-batch [Hibernateはバッチ挿入/更新をサイレントに無効にします] *。
この例のエンティティは_GenerationType.SEQUENCE_識別子ジェネレーターを使用するため、Hibernateはバッチ操作を有効にします。
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;

8. 概要

この記事では、Hibernate / JPAを使用したバッチ挿入とバッチ更新について説明しました。
この記事のコードサンプルhttps://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-data-jpa-2[Github上]をご覧ください。