Insertion / mise à jour par lots avec Hibernate / JPA

1. Vue d'ensemble

Dans ce didacticiel, nous verrons comment nous pouvons insérer ou mettre à jour des entités par lots à l'aide d'Hibernate / JPA.

Le traitement par lots nous permet d'envoyer un groupe d'instructions SQL à la base de données en un seul appel réseau. De cette façon, nous pouvons optimiser l'utilisation du réseau et de la mémoire de notre application.

2. Configuration

2.1. Exemple de modèle de données

Examinons notre exemple de modèle de données que nous utiliserons dans les exemples.

Tout d'abord, nous allons créer une entité School :

@Entity public class School { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private long id; private String name; @OneToMany(mappedBy = "school") private List students; // Getters and setters... }

Chaque école aura zéro élève ou plus :

@Entity public class Student { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private long id; private String name; @ManyToOne private School school; // Getters and setters... }

2.2. Suivi des requêtes SQL

Lors de l'exécution de nos exemples, nous devrons vérifier que les instructions insert / update sont bien envoyées par lots. Malheureusement, nous ne pouvons pas comprendre à partir des instructions du journal Hibernate si les instructions SQL sont groupées ou non. Pour cette raison, nous utiliserons un proxy de source de données pour tracer les instructions SQL Hibernate / JPA:

private static class ProxyDataSourceInterceptor implements MethodInterceptor { private final DataSource dataSource; public ProxyDataSourceInterceptor(final DataSource dataSource) { this.dataSource = ProxyDataSourceBuilder.create(dataSource) .name("Batch-Insert-Logger") .asJson().countQuery().logQueryToSysOut().build(); } // Other methods... }

3. Comportement par défaut

Hibernate n'active pas le traitement par lots par défaut . Cela signifie qu'il enverra une instruction SQL distincte pour chaque opération d'insertion / mise à jour:

@Transactional @Test public void whenNotConfigured_ThenSendsInsertsSeparately() { for (int i = 0; i < 10; i++) { School school = createSchool(i); entityManager.persist(school); } entityManager.flush(); }

Ici, nous avons conservé 10 entités scolaires . Si nous regardons les journaux de requêtes, nous pouvons voir qu'Hibernate envoie chaque instruction d'insertion séparément:

"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], "params":[["School1","1"]] "querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], "params":[["School2","2"]] "querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], "params":[["School3","3"]] "querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], "params":[["School4","4"]] "querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], "params":[["School5","5"]] "querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], "params":[["School6","6"]] "querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], "params":[["School7","7"]] "querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], "params":[["School8","8"]] "querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], "params":[["School9","9"]] "querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], "params":[["School10","10"]]

Par conséquent, nous devons configurer Hibernate pour activer le traitement par lots. Pour cela, nous devons définir la propriété hibernate.jdbc.batch_size sur un nombre supérieur à 0 .

Si nous créons EntityManager manuellement, nous devons ajouter hibernate.jdbc.batch_size aux propriétés Hibernate:

public Properties hibernateProperties() { Properties properties = new Properties(); properties.put("hibernate.jdbc.batch_size", "5"); // Other properties... return properties; }

Si nous utilisons Spring Boot, nous pouvons le définir comme une propriété d'application:

spring.jpa.properties.hibernate.jdbc.batch_size=5

4. Insertion par lots pour une seule table

4.1. Insertion par lots sans rinçage explicite

Voyons d'abord comment nous pouvons utiliser les insertions par lots lorsque nous avons affaire à un seul type d'entité.

Nous utiliserons l'exemple de code précédent, mais cette fois, le traitement par lots est activé:

@Transactional @Test public void whenInsertingSingleTypeOfEntity_thenCreatesSingleBatch() { for (int i = 0; i < 10; i++) { School school = createSchool(i); entityManager.persist(school); } }

Ici, nous avons persisté 10 entités scolaires . Lorsque nous examinons les journaux, nous pouvons vérifier qu'Hibernate envoie des instructions d'insertion par lots:

"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"], "params":[["School1","1"],["School2","2"],["School3","3"],["School4","4"],["School5","5"]] "batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"], "params":[["School6","6"],["School7","7"],["School8","8"],["School9","9"],["School10","10"]]

Une chose importante à mentionner ici est la consommation de mémoire. Lorsque nous persistons une entité, Hibernate la stocke dans le contexte de persistance . Par exemple, si nous conservons 100 000 entités dans une transaction, nous finirons par avoir 100 000 instances d'entité en mémoire, ce qui peut provoquer une OutOfMemoryException .

4.2. Insertion par lots avec vidage explicite

Maintenant, nous allons voir comment nous pouvons optimiser l'utilisation de la mémoire lors des opérations de traitement par lots. Explorons en profondeur le rôle du contexte de persistance.

Tout d'abord, le contexte de persistance stocke les entités nouvellement créées ainsi que celles modifiées en mémoire. Hibernate envoie ces modifications à la base de données lorsque la transaction est synchronisée. Cela se produit généralement à la fin d'une transaction. Cependant, l' appel à EntityManager.flush () déclenche également une synchronisation de transaction .

Deuxièmement, le contexte de persistance sert de cache d'entité, donc également appelé cache de premier niveau. Pour effacer les entités dans le contexte de persistance, nous pouvons appeler EntityManager.clear () .

Ainsi, pour réduire la charge mémoire lors du traitement par lots, nous pouvons appeler EntityManager.flush () et EntityManager.clear () sur notre code d'application, chaque fois que la taille du lot est atteinte:

@Transactional @Test public void whenFlushingAfterBatch_ThenClearsMemory() { for (int i = 0; i  0 && i % BATCH_SIZE == 0) { entityManager.flush(); entityManager.clear(); } School school = createSchool(i); entityManager.persist(school); } }

Ici, nous vidons les entités dans le contexte de persistance, ce qui oblige Hibernate à envoyer des requêtes à la base de données. De plus, en effaçant le contexte de persistance, nous supprimons les entités School de la mémoire. Le comportement de traitement par lots restera le même.

5. Insertion par lots pour plusieurs tables

Voyons maintenant comment nous pouvons configurer les insertions par lots lorsque nous traitons plusieurs types d'entités dans une transaction.

Lorsque nous voulons conserver les entités de plusieurs types, Hibernate crée un lot différent pour chaque type d'entité. En effet, il ne peut y avoir qu'un seul type d'entité dans un seul lot .

De plus, comme Hibernate collecte des instructions d'insertion, chaque fois qu'il rencontre un type d'entité différent de celui du lot actuel, il crée un nouveau lot. C'est le cas même s'il existe déjà un lot pour ce type d'entité:

@Transactional @Test public void whenThereAreMultipleEntities_ThenCreatesNewBatch() { for (int i = 0; i  0 && i % BATCH_SIZE == 0) { entityManager.flush(); entityManager.clear(); } School school = createSchool(i); entityManager.persist(school); Student firstStudent = createStudent(school); Student secondStudent = createStudent(school); entityManager.persist(firstStudent); entityManager.persist(secondStudent); } }

Ici, nous insérons une école et lui attribuons deux étudiants et répétons ce processus 10 fois.

Dans les logs, nous voyons qu'Hibernate envoie des instructions d'insertion School en plusieurs lots de taille 1 alors que nous n'attendions que 2 lots de taille 5. De plus, les instructions d'insertion Student sont également envoyées en plusieurs lots de taille 2 au lieu de 4 lots de taille 5. :

"batch":true, "querySize":1, "batchSize":1, "query":["insert into school (name, id) values (?, ?)"], "params":[["School1","1"]] "batch":true, "querySize":1, "batchSize":2, "query":["insert into student (name, school_id, id) values (?, ?, ?)"], "params":[["Student-School1","1","2"],["Student-School1","1","3"]] "batch":true, "querySize":1, "batchSize":1, "query":["insert into school (name, id) values (?, ?)"], "params":[["School2","4"]] "batch":true, "querySize":1, "batchSize":2, "query":["insert into student (name, school_id, id) values (?, ?, ?)"], "params":[["Student-School2","4","5"],["Student-School2","4","6"]] "batch":true, "querySize":1, "batchSize":1, "query":["insert into school (name, id) values (?, ?)"], "params":[["School3","7"]] "batch":true, "querySize":1, "batchSize":2, "query":["insert into student (name, school_id, id) values (?, ?, ?)"], "params":[["Student-School3","7","8"],["Student-School3","7","9"]] Other log lines...

Pour regrouper toutes les instructions d'insertion du même type d'entité, nous devons configurer la propriété hibernate.order_inserts .

Nous pouvons configurer la propriété Hibernate manuellement en utilisant EntityManagerFactory :

public Properties hibernateProperties() { Properties properties = new Properties(); properties.put("hibernate.order_inserts", "true"); // Other properties... return properties; }

Si nous utilisons Spring Boot, nous pouvons configurer la propriété dans application.properties:

spring.jpa.properties.hibernate.order_inserts=true

Après avoir ajouté cette propriété, nous aurons 1 lot pour les insertions School et 2 lots pour les insertions Student :

"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"], "params":[["School6","16"],["School7","19"],["School8","22"],["School9","25"],["School10","28"]] "batch":true, "querySize":1, "batchSize":5, "query":["insert into student (name, school_id, id) values (?, ?, ?)"], "params":[["Student-School6","16","17"],["Student-School6","16","18"], ["Student-School7","19","20"],["Student-School7","19","21"],["Student-School8","22","23"]] "batch":true, "querySize":1, "batchSize":5, "query":["insert into student (name, school_id, id) values (?, ?, ?)"], "params":[["Student-School8","22","24"],["Student-School9","25","26"], ["Student-School9","25","27"],["Student-School10","28","29"],["Student-School10","28","30"]]

6. Mise à jour par lots

Maintenant, passons aux mises à jour par lots. Comme pour les insertions par lots, nous pouvons regrouper plusieurs instructions de mise à jour et les envoyer à la base de données en une seule fois.

Pour activer cela, nous allons configurer les propriétés hibernate.order_updates et hibernate.jdbc.batch_versioned_data .

Si nous créons notre EntityManagerFactory manuellement, nous pouvons définir les propriétés par programme:

public Properties hibernateProperties() { Properties properties = new Properties(); properties.put("hibernate.order_updates", "true"); properties.put("hibernate.batch_versioned_data", "true"); // Other properties... return properties; }

Et si nous utilisons Spring Boot, nous les ajouterons simplement à application.properties:

spring.jpa.properties.hibernate.order_updates=true spring.jpa.properties.hibernate.batch_versioned_data=true

Après avoir configuré ces propriétés, Hibernate doit regrouper les instructions de mise à jour par lots:

@Transactional @Test public void whenUpdatingEntities_thenCreatesBatch() { TypedQuery schoolQuery = entityManager.createQuery("SELECT s from School s", School.class); List allSchools = schoolQuery.getResultList(); for (School school : allSchools) { school.setName("Updated_" + school.getName()); } }

Ici, nous avons mis à jour les entités scolaires et Hibernate envoie des instructions SQL en 2 lots de taille 5:

"batch":true, "querySize":1, "batchSize":5, "query":["update school set name=? where id=?"], "params":[["Updated_School1","1"],["Updated_School2","2"],["Updated_School3","3"], ["Updated_School4","4"],["Updated_School5","5"]] "batch":true, "querySize":1, "batchSize":5, "query":["update school set name=? where id=?"], "params":[["Updated_School6","6"],["Updated_School7","7"],["Updated_School8","8"], ["Updated_School9","9"],["Updated_School10","10"]]

7. Stratégie de génération @Id

Lorsque nous voulons utiliser le traitement par lots pour les insertions / mises à jour, nous devons être conscients de la stratégie de génération de clé primaire. Si nos entités utilisent le générateur d'identifiant GenerationType.IDENTITY , Hibernate désactivera silencieusement les insertions / mises à jour de lots .

Étant donné que les entités de nos exemples utilisent le générateur d'identifiant GenerationType.SEQUENCE , Hibernate permet les opérations par lots:

@Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private long id;

8. Résumé

Dans cet article, nous avons examiné les insertions de lots et les mises à jour à l'aide d'Hibernate / JPA.

Consultez les exemples de code pour cet article sur Github.