Массовая вставка данных через JpaRepository в Spring Boot и Kotlin
Почему saveAll() не работает как batch?
По умолчанию saveAll() из JpaRepository просто вызывает save() для каждой сущности в цикле. Hibernate генерирует отдельный INSERT для каждой записи, что очень медленно при большом количестве данных. Это не batch-вставка, а последовательные одиночные вставки.
Шаг 1: Включаем batch-режим Hibernate
Добавьте в application.yml следующие настройки:
spring:
jpa:
properties:
hibernate:
jdbc.batch_size: 50
order_inserts: true
order_updates: true
generate_statistics: true # для отладки
hibernate:
ddl-auto: none
datasource:
hikari:
maximum-pool-size: 10
- batch_size — сколько INSERT'ов объединять в один пакет (50-100 оптимально).
- order_inserts — обязательно true, иначе Hibernate не сгруппирует вставки.
- generate_statistics — помогает проверить, что batch работает.
Шаг 2: Используем правильный метод
Вместо цикла с save() используйте saveAll() с коллекцией:
@Service
class UserService(private val repo: UserRepository) {
@Transactional
fun insertUsers(users: List<User>) {
repo.saveAll(users) // теперь это будет batch!
}
}
Важно: метод должен быть @Transactional, иначе Hibernate будет сбрасывать batch после каждого INSERT.
Шаг 3: Проверяем, что batch работает
Включите в логах SQL и статистику:
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.stat: DEBUG
В логах вы увидите:
2024-01-01 12:00:00.123 DEBUG 12345 --- [ main] org.hibernate.SQL :
insert into users (id, name, email) values (?, ?, ?)
insert into users (id, name, email) values (?, ?, ?)
insert into users (id, name, email) values (?, ?, ?)
...
Это означает, что Hibernate объединил несколько INSERT в один batch-пакет.
Шаг 4: Настройка для CockroachDB
CockroachDB поддерживает batch-вставки, но нужно убедиться, что JDBC-драйвер включил режим rewriteBatchedInserts:
spring:
datasource:
url: jdbc:postgresql://localhost:26257/mydb?rewriteBatchedInserts=true
Без этого параметра CockroachDB будет выполнять каждый INSERT отдельно, даже если Hibernate шлёт batch.
Шаг 5: Альтернатива — ручная вставка через EntityManager
Если saveAll() всё ещё медленный, можно использовать EntityManager напрямую:
@Service
class UserService(
@PersistenceContext private val entityManager: EntityManager
) {
@Transactional
fun insertUsers(users: List<User>) {
var batchSize = 50
users.forEachIndexed { index, user ->
entityManager.persist(user)
if (index % batchSize == 0 && index > 0) {
entityManager.flush()
entityManager.clear()
}
}
}
}
Этот подход даёт полный контроль над размером batch и позволяет очищать persistence context, чтобы избежать переполнения памяти.
Итоговый чек-лист
- Добавьте
hibernate.jdbc.batch_sizeиorder_inserts=trueв конфиг. - Оберните метод вставки в
@Transactional. - Используйте
saveAll()с коллекцией, а не цикл. - Для CockroachDB добавьте
rewriteBatchedInserts=trueв URL JDBC. - Проверьте логи — должны появиться batch-INSERT'ы.
После этих настроек скорость вставки 1000 записей должна вырасти в 10-50 раз по сравнению с циклом save().
Использование JDBC batch напрямую (без Hibernate)
Если вам нужна максимальная производительность и вы не хотите зависеть от настроек Hibernate, можно использовать JdbcTemplate с batch-операциями:
@Repository
class UserJdbcRepository(private val jdbcTemplate: JdbcTemplate) {
fun batchInsert(users: List<User>) {
jdbcTemplate.batchUpdate(
"INSERT INTO users (id, name, email) VALUES (?, ?, ?)",
users,
50 // размер batch
) { ps, user ->
ps.setLong(1, user.id)
ps.setString(2, user.name)
ps.setString(3, user.email)
}
}
}
Этот подход:
- Работает быстрее Hibernate, так как нет overhead ORM.
- Не требует настройки
batch_sizeв application.yml. - Гарантированно работает с CockroachDB через JDBC.
- Позволяет контролировать размер пакета в коде.
Минусы: нужно писать SQL вручную, нет проверки сущностей Hibernate (например, каскадных операций).
Когда использовать: для bulk-вставок в таблицы без связей, когда скорость критична. Для обычных CRUD-операций оставьте JpaRepository.