Как сделать массовые (многорядные) вставки с JpaRepository?

При звонке saveAll метод моего JpaRepository с длинным List<Entity> На уровне сервисов в журнале трассировки Hibernate отображаются отдельные операторы SQL, выдаваемые для каждой сущности.

Могу ли я заставить его сделать массовую вставку (т.е. многорядную) без необходимости вручную возиться с EntityManger, транзакции и т. д. или даже необработанные строки операторов SQL?

Под многорядной вставкой я имею в виду не просто переход от:

start transaction
INSERT INTO table VALUES (1, 2)
end transaction
start transaction
INSERT INTO table VALUES (3, 4)
end transaction
start transaction
INSERT INTO table VALUES (5, 6)
end transaction

чтобы:

start transaction
INSERT INTO table VALUES (1, 2)
INSERT INTO table VALUES (3, 4)
INSERT INTO table VALUES (5, 6)
end transaction

но вместо этого:

start transaction
INSERT INTO table VALUES (1, 2), (3, 4), (5, 6)
end transaction

В PROD я использую CockroachDB, и разница в производительности значительна.

Ниже приведен минимальный пример, который воспроизводит проблему (H2 для простоты).


./src/main/kotlin/ThingService.kt:

package things

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.data.jpa.repository.JpaRepository
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.GeneratedValue

interface ThingRepository : JpaRepository<Thing, Long> {
}

@RestController
class ThingController(private val repository: ThingRepository) {
    @GetMapping("/test_trigger")
    fun trigger() {
        val things: MutableList<Thing> = mutableListOf()
        for (i in 3000..3013) {
            things.add(Thing(i))
        }
        repository.saveAll(things)
    }
}

@Entity
data class Thing (
    var value: Int,
    @Id
    @GeneratedValue
    var id: Long = -1
)

@SpringBootApplication
class Application {
}

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

./src/main/resources/application.properties:

jdbc.driverClassName = org.h2.Driver
jdbc.url = jdbc:h2:mem:db
jdbc.username = sa
jdbc.password = sa

hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.hbm2ddl.auto=create

spring.jpa.generate-ddl = true
spring.jpa.show-sql = true

spring.jpa.properties.hibernate.jdbc.batch_size = 10
spring.jpa.properties.hibernate.order_inserts = true
spring.jpa.properties.hibernate.order_updates = true
spring.jpa.properties.hibernate.jdbc.batch_versioned_data = true

./build.gradle.kts:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    val kotlinVersion = "1.2.30"
    id("org.springframework.boot") version "2.0.2.RELEASE"
    id("org.jetbrains.kotlin.jvm") version kotlinVersion
    id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
    id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
    id("io.spring.dependency-management") version "1.0.5.RELEASE"
}

version = "1.0.0-SNAPSHOT"

tasks.withType<KotlinCompile> {
    kotlinOptions {
        jvmTarget = "1.8"
        freeCompilerArgs = listOf("-Xjsr305=strict")
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    compile("org.jetbrains.kotlin:kotlin-reflect")
    compile("org.hibernate:hibernate-core")
    compile("com.h2database:h2")
}

Бежать:

./gradlew bootRun

ВСТАВИТЬ БД INSERT:

curl http://localhost:8080/test_trigger

Выход журнала:

Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)

6 ответов

Решение

Чтобы получить массовую вставку с помощью Sring Boot и Spring Data JPA, вам понадобятся только две вещи:

  1. установить опцию spring.jpa.properties.hibernate.jdbc.batch_size к соответствующему значению, которое вам нужно (например: 20).

  2. использование saveAll() метод вашего репо со списком сущностей, подготовленных для вставки.

Рабочий пример здесь.

Что касается преобразования оператора вставки во что-то вроде этого:

INSERT INTO table VALUES (1, 2), (3, 4), (5, 6)

такое доступно в PostgreSQL: вы можете установить опцию reWriteBatchedInserts Значение true в строке подключения jdbc:

jdbc:postgresql://localhost:5432/db?reWriteBatchedInserts=true

тогда драйвер JDBC сделает это преобразование.

Дополнительную информацию о дозировании вы можете найти здесь.

ОБНОВЛЕНО

Демонстрационный проект в Котлине: https://github.com/Cepr0/sb-kotlin-batch-insert-demo

Основными проблемами является следующий код в SimpleJpaRepository:

@Transactional
public <S extends T> S save(S entity) {
    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

В дополнение к настройкам свойства размера пакета необходимо убедиться, что вызовы класса SimpleJpaRepository сохраняются, а не объединяются. Есть несколько подходов для решения этой проблемы: используйте @Id генератор, который не запрашивает последовательность, как

@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
var id: Long

Или заставляя упорство обрабатывать записи как новые, заставив вашу сущность реализовать Persistable и переопределив isNew() вызов

@Entity
class Thing implements Pesistable<Long> {
    var value: Int,
    @Id
    @GeneratedValue
    var id: Long = -1
    @Transient
    private boolean isNew = true;
    @PostPersist
    @PostLoad
    void markNotNew() {
        this.isNew = false;
    }
    @Override
    boolean isNew() {
        return isNew;
    }
}

Или переопределить save(List) и использовать менеджер сущностей для вызова persist()

@Repository
public class ThingRepository extends SimpleJpaRepository<Thing, Long> {
    private EntityManager entityManager;
    public ThingRepository(EntityManager entityManager) {
        super(Thing.class, entityManager);
        this.entityManager=entityManager;
    }

    @Transactional
    public List<Thing> save(List<Thing> things) {
        things.forEach(thing -> entityManager.persist(thing));
        return things;
    }
}

Приведенный выше код основан на следующих ссылках:

Вы можете настроить Hibernate для выполнения объемного DML. Взгляните на Spring Data JPA - одновременные массовые вставки / обновления. Я думаю, что раздел 2 ответа может решить вашу проблему:

Включите пакетную обработку операторов DML. Включение пакетной поддержки приведет к уменьшению количества обращений к базе данных для вставки / обновления одинакового количества записей.

Цитирование из пакетных операторов INSERT и UPDATE:

hibernate.jdbc.batch_size = 50

hibernate.order_inserts = true

hibernate.order_updates = true

hibernate.jdbc.batch_versioned_data = true

ОБНОВЛЕНИЕ: Вы должны установить свойства гибернации по-другому в вашем application.properties файл. Они находятся под пространством имен: spring.jpa.properties.*, Пример может выглядеть следующим образом:

spring.jpa.properties.hibernate.jdbc.batch_size = 50
spring.jpa.properties.hibernate.order_inserts = true
....

Hibernate использует стратегию отложенной записи транзакций для автоматического выполнения пакетной вставки, обновления или удаления.

Но только установка свойства Spring.jpa.properties.hibernate.jdbc.batch_size=100 сама по себе не сработает. нам также нужно установить генератор идентификаторов как@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_generator")

Если мы используем GenerationType.AUTO или GenerationType.IDENTITY для объекта в этом случае пакетной вставки, обновление не будет работать. Потому что в этом случае спящий режим не знает о значении идентификатора, которое нужно вставить, поскольку оно генерируется на уровне БД, поэтому он отключает пакетную вставку, и происходит индивидуальная вставка.

Итак, для использования пакетной вставки обновите нашу сущность, в которой генератор идентификаторов должен быть указан как Sequence.

Все упомянутые методы работают, но будут медленными, особенно если источник вставленных данных находится в другой таблице. Во-первых, даже сbatch_size>1операция вставки будет выполнена в нескольких SQL-запросах. Во-вторых, если исходные данные находятся в другой таблице, вам необходимо получить данные с помощью других запросов (и в худшем случае загрузить все данные в память) и преобразовать их в статические массовые вставки. В-третьих, с отдельнымиpersist() вызовите для каждой сущности (даже если включена пакетная обработка), вы заполнили кеш первого уровня диспетчера сущностей всеми этими экземплярами сущностей.

Но есть еще один вариант для Hibernate. Если вы используете Hibernate в качестве поставщика JPA, вы можете вернуться к HQL, который изначально поддерживает массовые вставки с подвыбором из другой таблицы. Пример:

Session session = entityManager.unwrap(Session::class.java)
session.createQuery("insert into Entity (field1, field2) select [...] from [...]")
  .executeUpdate();

Будет ли это работать, зависит от вашей стратегии создания идентификатора. ЕслиEntity.idгенерируется базой данных (например, автоинкремент MySQL), он будет успешно выполнен. ЕслиEntity.id генерируется вашим кодом (особенно для генераторов UUID), он завершится ошибкой с исключением "неподдерживаемого метода генерации идентификатора".

Однако в последнем сценарии эту проблему можно решить с помощью настраиваемой функции SQL. Например, в PostgreSQL я использую расширение uuid-ossp, которое предоставляетuuid_generate_v4() функция, которую я наконец регистрирую в своем настраиваемом диалоговом окне:

import org.hibernate.dialect.PostgreSQL10Dialect;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.type.PostgresUUIDType;

public class MyPostgresDialect extends PostgreSQL10Dialect {

    public MyPostgresDialect() {
        registerFunction( "uuid_generate_v4", 
            new StandardSQLFunction("uuid_generate_v4", PostgresUUIDType.INSTANCE));
    }
}

А затем я регистрирую этот класс как диалог спящего режима:

hibernate.dialect=MyPostgresDialect

Наконец, я могу использовать эту функцию в запросе массовой вставки:

SessionImpl session = entityManager.unwrap(Session::class.java);
session.createQuery("insert into Entity (id, field1, field2) "+
  "select uuid_generate_v4(), [...] from [...]")
  .executeUpdate();

Наиболее важным является базовый SQL, сгенерированный Hibernate для выполнения этой операции, и это всего лишь один запрос:

insert into entity ( id, [...] ) select uuid_generate_v4(), [...] from [...]

Я столкнулся с той же проблемой, но я не мог видеть свои спящие запросы в пакетном режиме, я понял, что запрос не преобразуется в то, что действительно запрашивалось. Но чтобы быть уверенным, что это навалом, вы можете включить генерацию статистики.spring.jpa.properties.hibernate.generate_statistics=trueтогда вы увидите:

когда вы добавляетеspring.jpa.properties.hibernate.jdbc.batch_size=100вы начнете видеть некоторые различия, такие как меньше операторов jdbc и больше пакетов jdbc:

Другие вопросы по тегам