Spring DATA JPAで一括インサート

使用環境

GitHub - n-yata/batch-insert-sample

pom.xml

spring-boot-starter-data-jpaを入れる

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
</dependencies>

application.properties

reWriteBatchedInserts=trueをURLのパラメータに入れる(PostgreSQLの場合)。spring.jpa.properties.hibernate.order_insertsspring.jpa.properties.hibernate.jdbc.batch_sizeを設定。batch_sizeは20~100が推奨値みたい。

# DB接続設定
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres?reWriteBatchedInserts=true
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true

#thymeleafの設定
spring.thymeleaf.cache=false

# バッチインサートの設定
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.jdbc.batch_size=100

使用テーブルのCREATE, ALTER

テーブルを作成。シーケンステーブルの増分値を100に変更

DROP TABLE IF EXISTS todoitems CASCADE;

CREATE TABLE IF NOT EXISTS todoitems(
  id SERIAL NOT NULL,
  title VARCHAR(255),
  done BOOLEAN,
  tododate TIMESTAMP NOT NULL,
  user_id VARCHAR(255) NOT NULL,
  PRIMARY KEY (id)
);

ALTER SEQUENCE todoitems_id_seq INCREMENT BY 100;

Modelクラス

@SequenceGeneratorにallocationSize = 100を指定する。

package com.example.model;

import java.io.Serializable;
import java.time.LocalDateTime;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

@Entity
@Table(name="todoitems")
public class Todoitem implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @SequenceGenerator(name = "todoitems_id_seq", sequenceName = "todoitems_id_seq", allocationSize = 100)
    @GeneratedValue(strategy=GenerationType.IDENTITY, generator = "todoitems_id_seq")
    private Integer id;

    private Boolean done;

    @Column(length=255)
    private String title;

    @Column(nullable=false)
    private LocalDateTime tododate;

    @Column(name="user_id", nullable=false, length=255)
    private String userId;

// Getter, Setterは省略

    @Override
    public String toString() {
        return "Todoitem [id=" + id + ", done=" + done + ", title=" + title + ", tododate=" + tododate + ", userId="
                + userId + "]";
    }

}

Repositoryクラス

package com.example.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.model.Todoitem;

public interface TodoitemRepository extends JpaRepository<Todoitem, Integer>{

}

Controllerクラス

repository.saveAll(List<Entity>)で一括登録する

package com.example.controller;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import com.example.model.Todoitem;
import com.example.repository.TodoitemRepository;

@Controller
public class HomeController {
    @Autowired
    TodoitemRepository repository;

    @GetMapping(value = "/")
    public String get() {
        return "index";
    }

    /**
     * サンプルを5件インサート
     */
    @PostMapping(value = "/insert")
    @Transactional(readOnly = false)
    public String insert() {
        List<Todoitem> items = new ArrayList<>();
        Todoitem item;
        for(int i  = 0; i < 5; i++) {
            item = new Todoitem();
            item.setTitle("テスト" + i);
            item.setDone(false);
            item.setTododate(LocalDateTime.now());
            item.setUserId("test_user");
            items.add(item);
        }
        repository.saveAll(items);
        return "index";
    }

    /**
     * アイテム一覧を表示
     * @param model
     * @return
     */
    @GetMapping(value = "/find")
    public String find(Model model) {
        List<Todoitem> items = repository.findAll();
        List<String> lines = new ArrayList<>();

        for(Todoitem item : items) {
            lines.add(item.toString());
        }

        model.addAttribute("items", lines);
        return "index";
    }
}

View(index.html)

thymeleafを使用。5件インサートと全件画面に表示ボタンをつけとく。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="https://www.thymeleaf.org"
    xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
    <form method="get" action="/find">
        <input type="submit" value="findAll">
    </form>
    <form method="post" action="/insert">
        <input type="submit" value="insert to 5 items">
    </form>
    <table>
        <tr th:each="line:${items}">
            <td th:text=${line}></td>
        </tr>
    </table>
</body>
</html>

PostgreSQLのログを出力するようにする

とりあえず一括インサートのサンプルは上記で完成だけど、 eclipse上のログだと1件ずつインサートしているように見える。 アプリからDBに飛んでから変換してる?(reWriteBatchedInserts=trueで)ようなのでPostgreSQL側のログを確認して一括インサートになってるか見てみる。

postgresql.confを編集
デフォルトの状態でインストールしてればたぶんここらへんにある。
C:\Program Files\PostgreSQL\13\data\postgresql.conf

コメントアウトとパラメータをallに変更してSQLが出力されるようにする

# log_statement = 'none'          # none, ddl, mod, all
↓
log_statement = 'all'          # none, ddl, mod, all

PostgreSQLを再起動して編集内容を反映

$ pg_ctl -D "C:\Program Files\PostgreSQL\13\data" restart  

または
f:id:n-yata:20210430095054p:plain

f:id:n-yata:20210430095158p:plain

アプリ起動してインサートすると、logファイルにSQLが書き込まれる。 ログファイルの場所はデフォルトだと下のようなパス。
C:\Program Files\PostgreSQL\13\data\log

一括インサートできていることが確認できる。

2021-04-29 21:15:28.494 JST [49252] LOG:  実行 <unnamed>: insert into todoitems (done, title, tododate, user_id, id) values ($1, $2, $3, $4, $5),($6, $7, $8, $9, $10),($11, $12, $13, $14, $15),($16, $17, $18, $19, $20)

参考文献

Java - spring-data-jpaでbulk insertするにはentity manager を使うしかないのでしょうか。|teratail