Avatar ·

Массовая вставка данных через JpaRepository в Spring Boot и Kotlin

Всем привет! Я пишу приложение на Kotlin + Spring Boot + Hibernate, и мне нужно вставлять много записей в таблицу CockroachDB. Сейчас я делаю это в цикле через saveAll(): ```kotlin @Repository interface UserRepository : JpaRepository @Service class UserService(private val repo: UserRepository) { fun insertUsers(users: List) { users.forEach { repo.save(it) } // или repo.saveAll(users) } } ``` Проблема: если пользователей 1000+, это работает очень медленно. В логах видно, что Hibernate создает отдельный INSERT для каждой записи. Я читал, что можно делать batch inserts, но не понимаю, как правильно настроить. Вопросы: 1. Как сделать массовую вставку через JpaRepository, чтобы Hibernate объединял INSERT в один пакет? 2. Нужно ли менять application.yml или добавлять какие-то аннотации? 3. Есть ли разница между saveAll() и циклом с save()? 4. Как это работает с CockroachDB (она вроде бы поддерживает batch)? Версии: Spring Boot 3.2, Hibernate 6.4, Kotlin 1.9, CockroachDB 23.2. Буду благодарен за примеры кода и конфигурации. <<>>

Почему 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, чтобы избежать переполнения памяти.

Итоговый чек-лист

  1. Добавьте hibernate.jdbc.batch_size и order_inserts=true в конфиг.
  2. Оберните метод вставки в @Transactional.
  3. Используйте saveAll() с коллекцией, а не цикл.
  4. Для CockroachDB добавьте rewriteBatchedInserts=true в URL JDBC.
  5. Проверьте логи — должны появиться 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.