Как делать массовые (многострочные) вставки с помощью 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

ВСТАВКИ БД триггера:

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 (?, ?)

Пожалуйста, проверьте мой ответ, надеюсь, он будет вам полезен: stackoverflow.com/a/50694902/5380322

Cepr0 09.06.2018 10:26

@ Cepr0 Спасибо, но я уже делаю это (накапливаю список и вызываю saveAll. Я просто добавил минимальный пример кода, чтобы воспроизвести проблему.

Tobias Hermann 09.06.2018 19:15

Вы установили свойство hibernate.jdbc.batch_size?

Cepr0 09.06.2018 20:49

@ Cepr0 Да. (см. выше)

Tobias Hermann 09.06.2018 21:23

Это неверно, оно должно быть в таком виде: spring.jpa.properties.hibernate.jdbc.batch_size

Cepr0 09.06.2018 22:01

@ Cepr0 Спасибо, rieckpil уже упомянул, и я соответствующим образом скорректировал свой код. Однако это все еще не дозирование.

Tobias Hermann 09.06.2018 22:05
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
58
6
108 185
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

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

Enable the batching of DML statements Enabling the batching support would result in less number of round trips to the database to insert/update the same number of records.

Quoting from batch INSERT and UPDATE statements:

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
....

Спасибо за предложение. Я попробовал, но ничего не вышло. Я добавил к своему вопросу минимальный пример кода, чтобы воспроизвести проблему, даже с вашими настройками.

Tobias Hermann 09.06.2018 19:13

Спасибо, я скорректировал свою конфигурацию (и соответственно обновил свой вопрос), но все равно не повезло.

Tobias Hermann 09.06.2018 22:00

Вы пробовали это с другой базой данных или требуется ваш H2? @TobiasHermann Далее я бы посоветовал попробовать это с базой данных MySQL. Не все драйверы базы данных правильно реализуют пакетную вставку / обновление JDBC.

rieckpil 12.06.2018 06:39

Я пробовал использовать CockroachDB 2.0.2. Он поддерживает многострочные вставки и работает примерно в 10 раз быстрее, когда я вручную создаю необходимый java.sql.PreparedStatement в моем приложении и отправляю его, используя необработанный java.sql.Connection из javax.sql.DataSource.

Tobias Hermann 12.06.2018 16:55
Ответ принят как подходящий

Чтобы получить массовую вставку с помощью 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 выполнит это преобразование.

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

ОБНОВЛЕНО

Демо-проект на Котлине: sb-kotlin-batch-insert-demo

ОБНОВЛЕНО

Hibernate disables insert batching at the JDBC level transparently if you use an IDENTITY identifier generator.

Спасибо. Я пытаюсь запустить вашу демоверсию Kotlin, но пока не удалось. Я делаю git clone https://github.com/Cepr0/sb-kotlin-batch-insert-demo, cd sb-kotlin-batch-insert-demo и mvn package, но затем получаю следующую ошибку: gist.github.com/Dobiasd/7f1163110b52876f171d43e17af0853c

Tobias Hermann 18.06.2018 14:02

@ Cepr0, я только что попробовал вашу программу с mySql db, но она не работает должным образом. Есть какое-то отношение к драйверу. Вот свойство, которое я использую: `` spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect '' `

Shaunak Patel 02.12.2018 15:11

@ShaunakPatel Что именно не работает и в какой программе, java или kotlin?

Cepr0 02.12.2018 15:37

@ Cepr0 в Java. Я вижу только одно отличие по сравнению с вашей программой. 1) База данных (я использую MySQL). Значит, я запускаю ваш код против MySQL

Shaunak Patel 02.12.2018 15:59

Есть ли способ перехватить или прослушать список метода saveAll (List ..)? -

Jonathan JOhx 17.12.2018 20:11

Что использовать, если у меня нет saveAll ()? Spring Boot 1.5.1.RELEASE.

Woland 17.01.2019 10:05

Пакетирование с сильным и JPA medium.com/@clydecroix/…

Clyde D'Cruz 24.03.2020 19:07

Можно ли также использовать ON CONFLICT (я бы предпочел игнорировать их в своей ситуации). Мое ограничение установлено для уникальной комбинации полей

Vincent 20.07.2020 10:32

Основные проблемы - это следующий код в 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;
    }
}

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

Спасибо Джину за то, что поделился полезными ссылками. Но по-прежнему существует проблема с сохранением значений @Generated@Id с использованием метода Persistable. Пакет выполняется только тогда, когда я вручную устанавливаю поле id по моей собственной логике. Если я полагаюсь на @Generated для своего свойства Longid, то операторы не выполняются партиями. Все ссылки, которыми вы поделились, не используют стратегию типа @Generated с методом Persistable. Я даже проверил ссылку на код Github, которая указана во второй ссылке, но она также присваивает свойство id вручную.

iamharish15 29.07.2018 07:49

Я думаю, что этот ответ был непонятен (и достаточно оценен). Я сам обнаружил такую ​​же проблему с saveAll. Итак, перефразируя проблему: если у вас ЕСТЬ рабочая пакетная обработка, ваша сущность НЕ использует сгенерированный идентификатор, и вы используете SimpleJpaRepository с saveAll, тогда: 1. saveAll будет использовать сохранение в цикле 2. save вызовет entityInformation.isNew (entity), получив ответ false для каждого звонка. 3. вызовет слияние для каждой сущности. 4. IIUC, эти вызовы слияния выбираются первыми, и они не могут быть объединены в пакет, поэтому это создаст проблему N + 1 из-за неправильной реализации saveAll.

Martin Mucha 13.12.2019 10:48

Дозирование с пружиной и JPA medium.com/@clydecroix/…

Clyde D'Cruz 24.03.2020 19:08

Все упомянутые методы работают, но будут медленными, особенно если источник вставленных данных находится в другой таблице. Во-первых, даже с 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 [...]

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