Sisipkan / Perbarui Batch dengan Hibernate / JPA

1. Ikhtisar

Dalam tutorial ini, kita akan melihat bagaimana kita dapat memasukkan atau memperbarui entitas secara batch menggunakan Hibernate / JPA.

Batching memungkinkan kita mengirim sekelompok pernyataan SQL ke database dalam satu panggilan jaringan. Dengan cara ini, kami dapat mengoptimalkan penggunaan jaringan dan memori aplikasi kami.

2. Penyiapan

2.1. Contoh Model Data

Mari kita lihat model data sampel kita yang akan kita gunakan dalam contoh.

Pertama, kami akan membuat entitas Sekolah :

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

Setiap Sekolah akan memiliki nol atau lebih Siswa :

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

2.2. Melacak Kueri SQL

Saat menjalankan contoh kita, kita perlu memverifikasi bahwa pernyataan insert / update memang dikirim dalam batch. Sayangnya, kami tidak dapat memahami dari pernyataan log Hibernasi apakah pernyataan SQL dikelompokkan atau tidak. Karena itu, kami akan menggunakan proxy sumber data untuk melacak pernyataan Hibernate / JPA SQL:

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. Perilaku Default

Hibernate tidak mengaktifkan batching secara default . Ini berarti itu akan mengirim pernyataan SQL terpisah untuk setiap operasi penyisipan / pembaruan:

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

Di sini, kami telah mempertahankan 10 entitas Sekolah . Jika kita melihat log kueri, kita dapat melihat bahwa Hibernate mengirimkan setiap pernyataan penyisipan secara terpisah:

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

Karenanya kita harus mengkonfigurasi Hibernate untuk mengaktifkan batching. Untuk tujuan ini, kita harus menyetel properti hibernate.jdbc.batch_size ke angka yang lebih besar dari 0 .

Jika kita membuat EntityManager secara manual, kita harus menambahkan hibernate.jdbc.batch_size ke properti Hibernate:

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

Jika kita menggunakan Spring Boot, kita dapat mendefinisikannya sebagai properti aplikasi:

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

4. Sisipan Batch untuk Tabel Tunggal

4.1. Sisipan Batch Tanpa Pembilasan Eksplisit

Pertama mari kita lihat bagaimana kita dapat menggunakan penyisipan batch ketika kita hanya berurusan dengan satu jenis entitas.

Kami akan menggunakan contoh kode sebelumnya, tetapi pengelompokan kali ini diaktifkan:

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

Di sini kami telah mempertahankan 10 entitas Sekolah . Saat kita melihat log, kita dapat memverifikasi bahwa Hibernate mengirimkan pernyataan insert dalam batch:

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

Satu hal penting yang perlu disebutkan di sini adalah konsumsi memori. Saat kami mempertahankan entitas, Hibernate menyimpannya dalam konteks ketekunan . Misalnya, jika kita mempertahankan 100.000 entitas dalam satu transaksi, kita akan memiliki 100.000 instance entitas dalam memori, kemungkinan menyebabkan OutOfMemoryException .

4.2. Sisipan Batch dengan Flush Eksplisit

Sekarang, kita akan melihat bagaimana kita dapat mengoptimalkan penggunaan memori selama operasi batching. Mari kita gali lebih dalam peran konteks ketekunan.

Pertama-tama, konteks persistensi menyimpan entitas yang baru dibuat dan juga yang dimodifikasi dalam memori. Hibernate mengirimkan perubahan ini ke database saat transaksi disinkronkan. Ini biasanya terjadi di akhir transaksi. Namun, memanggil EntityManager.flush () juga memicu sinkronisasi transaksi .

Kedua, konteks persistensi berfungsi sebagai cache entitas, sehingga juga disebut sebagai cache tingkat pertama. Untuk menghapus entitas dalam konteks persistensi, kita bisa memanggil EntityManager.clear () .

Jadi, untuk mengurangi beban memori selama batching, kita bisa memanggil EntityManager.flush () dan EntityManager.clear () pada kode aplikasi kita, setiap kali ukuran batch tercapai:

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

Di sini kami membersihkan entitas dalam konteks ketekunan sehingga membuat Hibernate mengirim kueri ke database. Selain itu, dengan menghapus konteks persistensi, kami menghapus entitas Sekolah dari memori. Perilaku batching akan tetap sama.

5. Menyisipkan Batch untuk Beberapa Tabel

Sekarang mari kita lihat bagaimana kita dapat mengonfigurasi penyisipan batch ketika berhadapan dengan beberapa tipe entitas dalam satu transaksi.

Saat kami ingin mempertahankan entitas dari beberapa jenis, Hibernate membuat kumpulan berbeda untuk setiap jenis entitas. Ini karena hanya ada satu jenis entitas dalam satu batch .

Selain itu, saat Hibernate mengumpulkan pernyataan penyisipan, setiap kali menemukan jenis entitas yang berbeda dari yang ada di kumpulan saat ini, ini membuat kumpulan baru. Ini masalahnya meskipun sudah ada batch untuk tipe entitas itu:

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

Di sini, kami memasukkan Sekolah dan menugaskannya dua Siswa dan mengulangi proses ini 10 kali.

Di log, kita melihat bahwa Hibernate mengirimkan pernyataan sisipan Sekolah dalam beberapa kelompok ukuran 1 sementara kita mengharapkan hanya 2 kelompok ukuran 5. Selain itu, pernyataan sisipan siswa juga dikirim dalam beberapa kelompok ukuran 2 bukannya 4 kelompok ukuran 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...

Untuk mengumpulkan semua pernyataan insert dari tipe entitas yang sama, kita harus mengkonfigurasi properti hibernate.order_inserts .

Kita dapat mengkonfigurasi properti Hibernate secara manual menggunakan EntityManagerFactory :

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

Jika kita menggunakan Spring Boot, kita dapat mengonfigurasi properti di application.properties:

spring.jpa.properties.hibernate.order_inserts=true

Setelah menambahkan properti ini, kita akan memiliki 1 kelompok untuk sisipan Sekolah dan 2 kelompok untuk sisipan Siswa :

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

Sekarang, mari beralih ke pembaruan batch. Mirip dengan penyisipan batch, kita dapat mengelompokkan beberapa pernyataan pembaruan dan mengirimkannya ke database sekaligus.

Untuk mengaktifkan ini, kita akan mengkonfigurasi hibernate.order_updates dan hibernate.jdbc.batch_versioned_data properti .

Jika kita membuat EntityManagerFactory kita secara manual, kita dapat mengatur properti secara terprogram:

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

Dan jika kami menggunakan Spring Boot, kami hanya akan menambahkannya ke application.properties:

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

Setelah mengkonfigurasi properti ini, Hibernate harus mengelompokkan pernyataan pembaruan dalam batch:

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

Di sini kami telah memperbarui entitas sekolah dan Hibernate mengirimkan pernyataan SQL dalam 2 batch ukuran 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. Strategi Pembuatan @Id

Saat kita ingin menggunakan batching untuk penyisipan / pembaruan, kita harus mengetahui strategi pembuatan kunci utama. Jika entitas kami menggunakan generator pengenal GenerationType.IDENTITY , Hibernate akan menonaktifkan penyisipan / pembaruan batch secara diam-diam .

Karena entitas dalam contoh kami menggunakan generator pengenal GenerationType.SEQUENCE , Hibernate memungkinkan operasi batch:

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

8. Ringkasan

Pada artikel ini, kami melihat penyisipan dan pembaruan batch menggunakan Hibernate / JPA.

Lihat contoh kode untuk artikel ini di Github.