Lors de l'appel de la méthode saveAll
de ma JpaRepository
avec un long List<Entity>
à partir de la couche service, la consignation de trace d'Hibernate montre que des instructions SQL uniques sont émises par entité.
Puis-je le forcer à effectuer une insertion en bloc (c'est-à-dire plusieurs lignes) sans avoir besoin de manipuler manuellement EntityManger
, les transactions, etc. ni même les chaînes d'instructions SQL brutes?
Par insertion multi-lignes, je ne parle pas seulement de la transition de:
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
mais au lieu de:
start transaction
INSERT INTO table VALUES (1, 2), (3, 4), (5, 6)
end transaction
Dans PROD, j'utilise CockroachDB, et la différence de performance est significative.
Vous trouverez ci-dessous un exemple minimal reproduisant le problème (H2 pour simplifier).
./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")
}
Courir:
./gradlew bootRun
Déclencheurs DB INSERT:
curl http://localhost:8080/test_trigger
Journal de sortie:
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 (?, ?)
Pour obtenir une insertion en bloc avec Sring Boot et Spring Data JPA, vous n'avez besoin que de deux choses:
définissez l'option spring.jpa.properties.hibernate.jdbc.batch_size
sur la valeur appropriée (par exemple: 20).
utilisez la méthode saveAll()
de votre rapport avec la liste des entités préparées pour l'insertion.
Exemple de travail est ici .
En ce qui concerne la transformation de l'instruction insert en quelque chose comme ceci:
INSERT INTO table VALUES (1, 2), (3, 4), (5, 6)
le type est disponible dans PostgreSQL: vous pouvez définir l'option reWriteBatchedInserts
sur true dans la chaîne de connexion jdbc:
jdbc:postgresql://localhost:5432/db?reWriteBatchedInserts=true
alors le pilote jdbc fera cette transformation .
Vous pouvez trouver des informations supplémentaires sur la mise en lots ici .
MIS À JOUR
Projet de démonstration à Kotlin: sb-kotlin-batch-insert-demo
MIS À JOUR
Les problèmes sous-jacents sont le code suivant dans SimpleJpaRepository:
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
En plus des paramètres de propriété de taille de lot, vous devez vous assurer que les appels de classe SimpleJpaRepository persistent et ne sont pas fusionnés. Il existe plusieurs approches pour résoudre ce problème: utilisez un générateur @Id
qui ne demande pas la séquence, comme
@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
var id: Long
Ou forcer la persistance à traiter les enregistrements comme nouveaux en demandant à votre entité d'implémenter Persistable et en remplaçant l'appel 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;
}
}
Ou remplacez la save(List)
et utilisez le gestionnaire d'entités pour appeler 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;
}
}
Le code ci-dessus est basé sur les liens suivants:
Vous pouvez configurer Hibernate pour faire de la DML en bloc. Examinez Spring Data JPA - Inserts/mises à jour en masse simultanés . Je pense que la section 2 de la réponse pourrait résoudre votre problème:
Activer le traitement par lots des instructions DML L'activation de la prise en charge du traitement par lots réduira le nombre d'allers-retours à la base de données pour insérer/mettre à jour le même nombre d'enregistrements.
Citant des instructions batch INSERT et UPDATE:
hibernate.jdbc.batch_size = 50
hibernate.order_inserts = true
hibernate.order_updates = true
hibernate.jdbc.batch_versenced_data = true
UPDATE: vous devez définir les propriétés de veille prolongée de manière différente dans votre fichier application.properties
. Ils sont sous l'espace de noms: spring.jpa.properties.*
. Un exemple pourrait ressembler à ceci:
spring.jpa.properties.hibernate.jdbc.batch_size = 50
spring.jpa.properties.hibernate.order_inserts = true
....