116 votes

Comment faire des insertions en masse (multi-row) avec JpaRepository?

Lors de l'appel de la méthode saveAll de mon JpaRepository avec une longue List depuis la couche de service, le suivi de journalisation de Hibernate montre des déclarations SQL uniques émises par entité.

Puis-je le forcer à effectuer une insertion groupée (c'est-à-dire multi-lignes) sans avoir besoin de manipuler manuellement l' EntityManager, les transactions, etc. ou même des chaînes de requêtes SQL brutes?

Avec l'insertion multi-lignes, je veux dire passer de:

démarrer la transaction
INSERT INTO table VALUES (1, 2)
fin de la transaction
démarrer la transaction
INSERT INTO table VALUES (3, 4)
fin de la transaction
démarrer la transaction
INSERT INTO table VALUES (5, 6)
fin de la transaction

à:

démarrer la transaction
INSERT INTO table VALUES (1, 2)
INSERT INTO table VALUES (3, 4)
INSERT INTO table VALUES (5, 6)
fin de la transaction

mais plutôt à:

démarrer la transaction
INSERT INTO table VALUES (1, 2), (3, 4), (5, 6)
fin de la transaction

En PROD j'utilise CockroachDB, et la différence de performance est significative.

Voici un exemple minimal qui reproduit le problème (H2 pour la simplicité).

....

0 votes

Veuillez vérifier ma réponse, j'espère qu'elle vous sera utile : stackoverflow.com/a/50694902/5380322

0 votes

@Cepr0 Merci, mais je fais déjà cela (j'accumule dans une liste et j'appelle saveAll. J'ai simplement ajouté un exemple de code minimal pour reproduire le problème.

0 votes

Avez-vous défini la propriété hibernate.jdbc.batch_size ?

168voto

Cepr0 Points 7789

Pour réaliser une insertion en masse avec Spring Boot et Spring Data JPA, vous avez seulement besoin de deux choses :

  1. définir l'option spring.jpa.properties.hibernate.jdbc.batch_size à la valeur appropriée dont vous avez besoin (par exemple : 20).

  2. utiliser la méthode saveAll() de votre repo avec la liste d'entités préparée pour l'insertion.

L'exemple de travail se trouve ici.

En ce qui concerne la transformation de l'instruction d'insertion en quelque chose comme ceci :

INSERT INTO table VALUES (1, 2), (3, 4), (5, 6)

la telle 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.

Des informations supplémentaires sur le lotissement, vous pouvez les trouver ici.

À JOUR

Projet de démonstration en Kotlin : sb-kotlin-batch-insert-demo

À JOUR

Hibernate désactive le lotissement des inserts au niveau JDBC de manière transparente si vous utilisez un générateur d'identifiant IDENTITY.

0 votes

Merci. J'essaie de faire fonctionner votre démo Kotlin, mais je n'ai pas encore réussi. Je fais git clone https://github.com/Cepr0/sb-kotlin-batch-insert-demo, cd sb-kotlin-batch-insert-demo et mvn package mais je me retrouve avec l'erreur suivante: gist.github.com/Dobiasd/7f1163110b52876f171d43e17af0853c

0 votes

@Cepr0, j'ai essayé ton programme avec la base de données MySql mais cela ne fonctionne pas comme prévu. Est-ce qu'il y a quelque chose à faire avec le driver. Voici une propriété que j'utilise, ``` spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect ```

0 votes

@ShaunakPatel Qu'est-ce qui ne fonctionne pas exactement et dans quel programme, java ou kotlin ?

20voto

Jean Marois Points 707

Le problème sous-jacent est le code suivant dans SimpleJpaRepository :

@Transactional
public  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 la classe SimpleJpaRepository appelle persist et non merge. Il existe quelques approches pour résoudre ce problème : utiliser un générateur @Id qui ne consulte pas le séquence, comme

@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
var id: Long

Ou forcer la persistance à traiter les enregistrements comme neufs en faisant implémenter à votre entité Persistable et en remplaçant l'appel à isNew()

@Entity
class Thing implements Pesistable {
    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 remplacer la méthode save(List) et utiliser l'entity manager pour appeler persist()

@Repository
public class ThingRepository extends SimpleJpaRepository {
    private EntityManager entityManager;
    public ThingRepository(EntityManager entityManager) {
        super(Thing.class, entityManager);
        this.entityManager=entityManager;
    }

    @Transactional
    public List save(List things) {
        things.forEach(thing -> entityManager.persist(thing));
        return things;
    }
}

Le code ci-dessus est basé sur les liens suivants :

~~

1 votes

Merci Jean de partager des liens utiles. Mais il y a encore un problème avec la persistance des valeurs @Generated @Id en utilisant la méthode Persistable. Le lot n'est exécuté que lorsque je définis manuellement le champ id selon ma propre logique. Si je compte sur @Generated pour ma propriété id de type Long, alors les instructions ne s'exécutent pas en lots. Tous les liens partagés par vous n'utilisent pas de stratégie de type @Generated avec la méthode Persistable. J'ai même vérifié le lien du code Github fourni dans le 2e lien, mais il attribue également manuellement la propriété id.

0 votes

Je pense que cette réponse n'a pas vraiment été comprise (et appréciée à sa juste valeur). J'ai moi-même constaté le même problème avec saveAll. Donc pour reformuler le problème : si vous AVEZ du regroupement de travail, votre entité N'utilise PAS d'ID généré, et vous utilisez SimpleJpaRepository avec saveAll, alors : 1. saveAll utilisera un save en boucle 2. save appellera entityInformation.isNew(entity) en obtenant une réponse négative pour chaque appel. 3. appellera merge pour chaque entité. 4. Si je comprends bien, ces appels merge effectuent une sélection préalable, et ceux-ci ne peuvent pas être regroupés, ce qui entraînera un problème de N+1, en raison d'une mise en œuvre incorrecte de saveAll.

2 votes

Regroupement avec spring et JPA medium.com/@clydecroix/…

9voto

rieckpil Points 1620

Vous pouvez configurer Hibernate pour effectuer du DML en vrac. Jetez un œil à Spring Data JPA - inserts/updates en vrac concurrents. Je pense que la section 2 de la réponse pourrait résoudre votre problème :

Activer le regroupement des instructions DML

L'activation du support du regroupement permettrait de réduire le nombre de trajets vers la base de données pour insérer/mettre à jour le même nombre d'enregistrements.

En citant des instructions de regroupement INSERT et UPDATE :

hibernate.jdbc.batch_size = 50

hibernate.order_inserts = true

hibernate.order_updates = true

hibernate.jdbc.batch_versioned_data = true

MISE À JOUR : Vous devez définir les propriétés Hibernate différemment dans votre fichier application.properties. Ils sont sous l'espace de noms : spring.jpa.properties.*. Un exemple pourrait ressembler à ce qui suit :

spring.jpa.properties.hibernate.jdbc.batch_size = 50
spring.jpa.properties.hibernate.order_inserts = true
....

0 votes

Merci pour la suggestion. Je l'ai essayée, mais cela n'a pas fonctionné. J'ai ajouté un exemple de code minimal à ma question pour reproduire le problème, même avec vos paramètres.

0 votes

Merci, j'ai ajusté ma configuration (et mis à jour ma question en conséquence), mais toujours pas de chance.

0 votes

Avez-vous essayé avec une base de données différente ou est-ce que votre H2 est une exigence? Je suggérerais d'essayer avec une base de données MySQL la prochaine fois. Tous les pilotes de base de données n'implémentent pas correctement la mise à jour/inclusion en bloc JDBC.

3voto

l0co Points 614

Toutes les méthodes mentionnées fonctionnent mais seront lentes, surtout si la source des données insérées se trouve dans une autre table. Tout d'abord, même avec batch_size>1, l'opération d'insertion sera exécutée en plusieurs requêtes SQL. Deuxièmement, si les données sources se trouvent dans une autre table, vous devez les récupérer avec d'autres requêtes (et dans le pire des cas charger toutes les données en mémoire) et les convertir en insertions massives statiques. Troisièmement, avec un appel persist() séparé pour chaque entité (même si le lot est activé), vous allez saturer le cache de premier niveau de l'EntityManager avec toutes ces instances d'entités.

Mais il y a une autre option pour Hibernate. Si vous utilisez Hibernate en tant que fournisseur JPA, vous pouvez revenir à l'HQL qui prend en charge nativement les insertions massives avec un sous-sélecteur d'une autre table. L'exemple :

Session session = entityManager.unwrap(Session::class.java)
session.createQuery("insert into Entity (field1, field2) select [...] from [...]")
  .executeUpdate();

Si cela fonctionne dépendra de votre stratégie de génération d'identifiant. Si l'ID de Entity est généré par la base de données (par exemple auto-incrémentation MySQL), cela réussira. Si l'ID de Entity est généré par votre code (surtout vrai pour les générateurs UUID), cela échouera avec une exception "méthode de génération d'ID non prise en charge".

Cependant, dans ce dernier scénario, ce problème peut être surmonté par une fonction SQL personnalisée. Par exemple, dans PostgreSQL, j'utilise l'extension uuid-ossp qui fournit la fonction uuid_generate_v4(), que je registre enfin dans mon dialogue personnalisé :

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));
    }
}

Ensuite, je registre cette classe en tant que dialogue hibernate :

hibernate.dialect=MyPostgresDialect

Enfin, je peux utiliser cette fonction dans la requête d'insertion en masse :

SessionImpl session = entityManager.unwrap(Session::class.java);
session.createQuery("insert into Entity (id, field1, field2) "+
  "select uuid_generate_v4(), [...] from [...]")
  .executeUpdate();

Le plus important est le SQL sous-jacent généré par Hibernate pour accomplir cette opération et il s'agit d'une seule requête :

insert into entity ( id, [...] ) select uuid_generate_v4(), [...] from [...]

2voto

Gui Alencar Points 198

J'ai eu le même problème mais je ne pouvais pas voir mes requêtes Hibernate en batch, j'ai réalisé que la requête ne se traduisait pas réellement en ce qui était réellement interrogé. Mais pour être sûr que c'est en vrac, vous pouvez activer la génération de statistiques spring.jpa.properties.hibernate.generate_statistics=true puis vous verrez :

entrer la description de l'image ici

lorsque vous ajoutez spring.jpa.properties.hibernate.jdbc.batch_size=100, vous commencerez à voir des différences, comme moins de déclarations jdbc et plus de lots jdbc :

entrer la description de l'image ici

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X