web-dev-qa-db-fra.com

Comment faire des insertions en bloc (multi-lignes) avec JpaRepository?

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 (?, ?)
23
Tobias Hermann

Pour obtenir une insertion en bloc avec Sring Boot et Spring Data JPA, vous n'avez besoin que de deux choses:

  1. définissez l'option spring.jpa.properties.hibernate.jdbc.batch_size sur la valeur appropriée (par exemple: 20).

  2. 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

Hibernate désactive l’insertion de lots au niveau JDBC de manière transparente si vous utilisez un générateur d’identifiant IDENTITY.

37
Cepr0

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:

7
Jean Marois

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
....
4
rieckpil