При вызове метода 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 (?, ?)
@ Cepr0 Спасибо, но я уже делаю это (накапливаю список и вызываю saveAll. Я просто добавил минимальный пример кода, чтобы воспроизвести проблему.
Вы установили свойство hibernate.jdbc.batch_size?
@ Cepr0 Да. (см. выше)
Это неверно, оно должно быть в таком виде: spring.jpa.properties.hibernate.jdbc.batch_size
@ Cepr0 Спасибо, rieckpil уже упомянул, и я соответствующим образом скорректировал свой код. Однако это все еще не дозирование.





Вы можете настроить 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
....
Спасибо за предложение. Я попробовал, но ничего не вышло. Я добавил к своему вопросу минимальный пример кода, чтобы воспроизвести проблему, даже с вашими настройками.
Спасибо, я скорректировал свою конфигурацию (и соответственно обновил свой вопрос), но все равно не повезло.
Вы пробовали это с другой базой данных или требуется ваш H2? @TobiasHermann Далее я бы посоветовал попробовать это с базой данных MySQL. Не все драйверы базы данных правильно реализуют пакетную вставку / обновление JDBC.
Я пробовал использовать CockroachDB 2.0.2. Он поддерживает многострочные вставки и работает примерно в 10 раз быстрее, когда я вручную создаю необходимый java.sql.PreparedStatement в моем приложении и отправляю его, используя необработанный java.sql.Connection из javax.sql.DataSource.
Чтобы получить массовую вставку с помощью Sring Boot и Spring Data JPA, вам нужно всего две вещи:
установите для параметра spring.jpa.properties.hibernate.jdbc.batch_size соответствующее значение (например: 20).
используйте метод 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
IDENTITYidentifier 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
@ Cepr0, я только что попробовал вашу программу с mySql db, но она не работает должным образом. Есть какое-то отношение к драйверу. Вот свойство, которое я использую: `` spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect '' `
@ShaunakPatel Что именно не работает и в какой программе, java или kotlin?
@ Cepr0 в Java. Я вижу только одно отличие по сравнению с вашей программой. 1) База данных (я использую MySQL). Значит, я запускаю ваш код против MySQL
Есть ли способ перехватить или прослушать список метода saveAll (List ..)? -
Что использовать, если у меня нет saveAll ()? Spring Boot 1.5.1.RELEASE.
Пакетирование с сильным и JPA medium.com/@clydecroix/…
Можно ли также использовать ON CONFLICT (я бы предпочел игнорировать их в своей ситуации). Мое ограничение установлено для уникальной комбинации полей
Основные проблемы - это следующий код в 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 вручную.
Я думаю, что этот ответ был непонятен (и достаточно оценен). Я сам обнаружил такую же проблему с saveAll. Итак, перефразируя проблему: если у вас ЕСТЬ рабочая пакетная обработка, ваша сущность НЕ использует сгенерированный идентификатор, и вы используете SimpleJpaRepository с saveAll, тогда: 1. saveAll будет использовать сохранение в цикле 2. save вызовет entityInformation.isNew (entity), получив ответ false для каждого звонка. 3. вызовет слияние для каждой сущности. 4. IIUC, эти вызовы слияния выбираются первыми, и они не могут быть объединены в пакет, поэтому это создаст проблему N + 1 из-за неправильной реализации saveAll.
Дозирование с пружиной и JPA medium.com/@clydecroix/…
Все упомянутые методы работают, но будут медленными, особенно если источник вставленных данных находится в другой таблице. Во-первых, даже с 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 [...]
Пожалуйста, проверьте мой ответ, надеюсь, он будет вам полезен: stackoverflow.com/a/50694902/5380322