Batch Insert / Update mit Hibernate / JPA

1. Übersicht

In diesem Tutorial sehen wir uns an, wie wir Entitäten mithilfe von Hibernate / JPA stapelweise einfügen oder aktualisieren können.

Durch Batching können wir eine Gruppe von SQL-Anweisungen in einem einzigen Netzwerkaufruf an die Datenbank senden. Auf diese Weise können wir die Netzwerk- und Speichernutzung unserer Anwendung optimieren.

2. Setup

2.1. Beispieldatenmodell

Schauen wir uns unser Beispieldatenmodell an, das wir in den Beispielen verwenden werden.

Erstens werden wir eine erstellen Schule Einheit:

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

Jede Schule hat null oder mehr Schüler :

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

2.2. SQL-Abfragen verfolgen

Wenn Sie unsere Beispiele ausführen, müssen Sie überprüfen, ob Insert / Update-Anweisungen tatsächlich stapelweise gesendet werden. Leider können wir anhand der Protokollanweisungen im Ruhezustand nicht verstehen, ob SQL-Anweisungen gestapelt sind oder nicht. Aus diesem Grund verwenden wir einen Datenquellen-Proxy, um Hibernate / JPA-SQL-Anweisungen zu verfolgen:

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

Im Ruhezustand ist das Stapeln standardmäßig nicht aktiviert . Dies bedeutet, dass für jede Einfüge- / Aktualisierungsoperation eine separate SQL-Anweisung gesendet wird:

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

Hier haben wir 10 Schuleinheiten beibehalten. Wenn wir uns die Abfrageprotokolle ansehen, können wir sehen, dass Hibernate jede Einfügeanweisung separat sendet:

"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"]]

Daher sollten wir den Ruhezustand so konfigurieren, dass die Stapelverarbeitung aktiviert wird. Zu diesem Zweck sollten wir die Eigenschaft hibernate.jdbc.batch_size auf eine Zahl größer als 0 setzen .

Wenn wir EntityManager manuell erstellen , sollten wir hibernate.jdbc.batch_size zu den Hibernate-Eigenschaften hinzufügen :

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

Wenn wir Spring Boot verwenden, können wir es als Anwendungseigenschaft definieren:

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

4. Batch Insert für einzelne Tabelle

4.1. Batch-Insert ohne explizite Spülung

Schauen wir uns zunächst an, wie wir Batch-Einfügungen verwenden können, wenn es sich nur um einen Entitätstyp handelt.

Wir werden das vorherige Codebeispiel verwenden, aber dieses Mal ist die Stapelverarbeitung aktiviert:

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

Hier haben wir 10 Schuleinheiten beibehalten. Wenn wir uns die Protokolle ansehen, können wir überprüfen, ob Hibernate Einfügeanweisungen in Stapeln sendet:

"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"]]

Eine wichtige Sache, die hier erwähnt werden muss, ist der Speicherverbrauch. Wenn wir eine Entität beibehalten, speichert Hibernate sie im Persistenzkontext . Wenn wir beispielsweise 100.000 Entitäten in einer Transaktion beibehalten , befinden sich 100.000 Entitätsinstanzen im Speicher, was möglicherweise zu einer OutOfMemoryException führt .

4.2. Batch Insert mit Explicit Flush

Nun werden wir uns ansehen, wie wir die Speichernutzung während Batching-Vorgängen optimieren können. Lassen Sie uns tief in die Rolle des Persistenzkontexts eintauchen.

Zunächst speichert der Persistenzkontext neu erstellte und auch die geänderten Entitäten im Speicher. Der Ruhezustand sendet diese Änderungen an die Datenbank, wenn die Transaktion synchronisiert wird. Dies geschieht in der Regel am Ende einer Transaktion. Der Aufruf von EntityManager.flush () löst jedoch auch eine Transaktionssynchronisation aus .

Zweitens dient der Persistenzkontext als Entitätscache und wird daher auch als Cache der ersten Ebene bezeichnet. Um Entitäten im Persistenzkontext zu löschen, können wir EntityManager.clear () aufrufen .

Um die Speicherlast während des Stapelns zu verringern, können wir EntityManager.flush () und EntityManager.clear () in unserem Anwendungscode aufrufen , wenn die Stapelgröße erreicht ist:

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

Hier werden die Entitäten im Persistenzkontext geleert, sodass der Ruhezustand Abfragen an die Datenbank sendet. Darüber hinaus wird durch die Persistenz Kontext löschen, sind zu entfernen wir die Schule Einheiten aus dem Speicher. Das Stapelverhalten bleibt gleich.

5. Batch-Insert für mehrere Tabellen

Lassen Sie uns nun sehen, wie wir Batch-Einfügungen konfigurieren können, wenn mehrere Entitätstypen in einer Transaktion behandelt werden.

Wenn wir die Entitäten mehrerer Typen beibehalten möchten, erstellt Hibernate für jeden Entitätstyp einen anderen Stapel. Dies liegt daran, dass es in einem einzelnen Stapel nur einen Entitätstyp geben kann .

Wenn Hibernate Einfügeanweisungen sammelt, wird außerdem immer dann, wenn ein Entitätstyp auftritt, der sich von dem im aktuellen Stapel unterscheidet, ein neuer Stapel erstellt. Dies ist der Fall, obwohl für diesen Entitätstyp bereits ein Stapel vorhanden ist:

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

Hier fügen wir eine Schule ein, weisen ihr zwei Schüler zu und wiederholen diesen Vorgang zehnmal.

In den Protokollen sehen wir , dass Hibernate sendet Schule Insert - Anweisungen in mehreren Chargen der Größe 1 , während wir nur zwei Chargen von Größe 5. Darüber hinaus erwartet wurden, Studenten Insert - Anweisungen sind auch in mehreren Chargen der Größe 2 statt 4 Chargen von Größe 5 gesendet ::

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

Um alle Einfügeanweisungen desselben Entitätstyps zu stapeln, sollten wir die Eigenschaft hibernate.order_inserts konfigurieren .

Wir können die Hibernate-Eigenschaft manuell mit EntityManagerFactory konfigurieren :

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

Wenn wir Spring Boot verwenden, können wir die Eigenschaft in application.properties konfigurieren:

spring.jpa.properties.hibernate.order_inserts=true

Nach dieser Eigenschaft hinzugefügt werden wir 1 Charge haben Schuleinsätze und 2 Chargen für Schüler Einsätze:

"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. Stapelaktualisierung

Fahren wir nun mit den Stapelaktualisierungen fort. Ähnlich wie bei Batch-Einfügungen können wir mehrere Aktualisierungsanweisungen gruppieren und auf einmal an die Datenbank senden.

Um dies zu aktivieren, konfigurieren wir die Eigenschaften hibernate.order_updates und hibernate.jdbc.batch_versioned_data .

Wenn wir unsere EntityManagerFactory manuell erstellen , können wir die Eigenschaften programmgesteuert festlegen:

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

Und wenn wir Spring Boot verwenden, fügen wir sie einfach zu application.properties hinzu:

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

Nach dem Konfigurieren dieser Eigenschaften sollte Hibernate Aktualisierungsanweisungen in Stapeln gruppieren:

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

Hier haben wir Schulentitäten aktualisiert und Hibernate sendet SQL-Anweisungen in 2 Stapeln der Größe 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. @ ID- Generierungsstrategie

Wenn wir Batching für Einfügungen / Aktualisierungen verwenden möchten, sollten wir die Strategie zur Primärschlüsselgenerierung kennen. Wenn unsere Einheiten verwenden GenerationType.IDENTITY Kennung Generator, Hibernate wird still disable Batch - Einsätze / Updates .

Da Entitäten in unseren Beispielen den Bezeichnergenerator GenerationType.SEQUENCE verwenden , ermöglicht der Ruhezustand Stapelvorgänge:

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

8. Zusammenfassung

In diesem Artikel haben wir uns Batch-Einfügungen und Aktualisierungen mit Hibernate / JPA angesehen.

Schauen Sie sich die Codebeispiele für diesen Artikel auf Github an.