Хватит использовать Spring Data saveAll как молоток
saveAll(...) удобен. Очень удобен.
Именно поэтому он так часто оказывается в местах, где ему не место.
Проблема начинается, когда через него пытаются загрузить > 1_000+ строк за раз.saveAll(...) универсальный, но не всегда оптимальный для массовой вставки: в итоге вылезают лишние SQL-операции,
лишние round-trip и просто заметная задержка на ровном месте.
Я как раз уперся в это на практике и не захотел оставлять массовые вставки на “авось оптимизируется само”.
В чем проблема с saveAll(...) на больших объемах
saveAll(...)в первую очередь про удобство API, а не про максимальную пропускную способность;saveAll(...)не только вставляет: если у сущности уже естьid, он идет в update-сценарий;- в базовом сценарии это обработка коллекции в обычном цикле, то есть при
Nобъектах вы получаете околоNопераций
записи в БД; - на больших пачках он часто проигрывает выделенной batch-стратегии;
- чем больше данных, тем дороже становится эта разница.
Если у вас обычный CRUD с небольшими наборами, это вообще не проблема.
Но когда у вас импорты, синхронизации или технические джобы на тысячи записей, “просто saveAll” быстро становится узким
местом.
Как я это решал
Я использовал подход Spring Data Repository Fragment:
BulkRepository<T>с методомinsertBatch(...)BulkRepositoryImpl<T>сJdbcTemplate.batchUpdate(...)- регистрация фрагмента через
META-INF/spring.factories - подключение в
UserRepository
Короткая идея в коде:
public interface UserRepository
extends ListCrudRepository<User, Long>, BulkRepository<User> {
}
То есть обычный репозиторий остается обычным, но получает отдельный bulk-метод под массовые операции.
Внутри реализации:
- SQL собирается из metadata Spring Data (таблица/колонки);
- план вставки кэшируется на тип сущности;
- параметры передаются через prepared statement (
?); - сущности с уже заполненным
idпропускаются сWARN.
Важный нюанс про JPA
Если вы на Spring Data JPA, batching для saveAll(...) часто можно включить Hibernate-настройками (например,hibernate.jdbc.batch_size, hibernate.order_inserts).
Но там есть ограничения: IDENTITY-генерация id часто ломает эффективный batching, плюс поведение сильно зависит от
persistence context, flush/clear и жизненного цикла сущностей.
В остальных Spring Data модулях обычно нет одного универсального “включателя”, который магически сделает массовые
операции быстрыми. Поэтому отдельные bulk-методы остаются рабочим и прозрачным подходом.
Куда это развивать дальше
Самое полезное в таком подходе: это не одноразовый трюк под insertBatch(...).
- можно добавить
updateAll(...)с batch update; - можно сделать
deleteBatch(...)для массовых удалений; - можно вынести общие bulk-паттерны в переиспользуемый слой для нескольких сервисов.
Иными словами, вы строите собственный набор репозиторных операций под ваши реальные нагрузки, а не под усредненный
случай.
Небольшие бенчмарки
Мой текущий локальный прогон для 5 000 записей:
| Сценарий | Пользователей | Генерация (ms) | Вставка (ms) | Итого (ms) |
|---|---|---|---|---|
saveAll | 5000 | 5 | 232 | 238 |
insertBatch | 5000 | 5 | 58 | 67 |
Прогон на удалённой БД. Сервер находится в моём городе:
| Сценарий | Пользователей | Генерация (ms) | Вставка (ms) | Итого (ms) |
|---|---|---|---|---|
saveAll | 10000 | 17 | 919 | 936 |
insertBatch | 10000 | 17 | 171 | 188 |
Репозиторий с полной реализацией
Если хотите посмотреть все целиком (Flyway, PostgreSQL, endpoint’ы, fragment implementation):
github Ulllie/spring-data-ext