Batch Insert / Update dengan Hibernate / JPA

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan melihat bagaimana kita dapat mengumpulkan entri atau mengemas kini entiti menggunakan Hibernate / JPA.

Batching membolehkan kita mengirim sekumpulan pernyataan SQL ke pangkalan data dalam satu panggilan rangkaian. Dengan cara ini, kita dapat mengoptimumkan penggunaan rangkaian dan memori aplikasi kita.

2. Persediaan

2.1. Model Data Contoh

Mari lihat contoh model data yang akan kami gunakan dalam contoh.

Pertama, kami akan mewujudkan entiti 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 mempunyai Pelajar sifar atau lebih :

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

2.2. Mengesan Pertanyaan SQL

Semasa menjalankan contoh kami, kami perlu mengesahkan bahawa pernyataan penyisipan / kemas kini memang dihantar secara berkumpulan. Malangnya, kami tidak dapat memahami dari penyataan log Hibernate sama ada penyataan SQL mempunyai kumpulan atau tidak. Oleh kerana itu, kami akan menggunakan proksi sumber data untuk mengesan 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. Kelakuan Lalai

Hibernate tidak membolehkan kumpulan secara lalai . Ini bermaksud bahawa ia akan menghantar pernyataan SQL yang berasingan untuk setiap operasi memasukkan / kemas kini:

@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 mempunyai 10 entiti Sekolah . Sekiranya kita melihat log pertanyaan, kita dapat melihat bahawa Hibernate menghantar setiap penyataan sisipan secara berasingan:

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

Oleh itu kita harus mengkonfigurasi Hibernate untuk membolehkan kumpulan. Untuk tujuan ini, kita harus menetapkan harta hibernate.jdbc.batch_size ke nombor yang lebih besar daripada 0 .

Sekiranya kita membuat EntityManager secara manual, kita harus menambahkan hibernate.jdbc.batch_size ke sifat Hibernate:

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

Sekiranya kita menggunakan Spring Boot, kita dapat menentukannya sebagai harta aplikasi:

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

4. Masukkan Kumpulan untuk Jadual Tunggal

4.1. Masukkan Batch Tanpa Lenturan Secara Eksplisit

Mari kita lihat dahulu bagaimana kita boleh menggunakan sisipan kumpulan ketika kita berurusan dengan satu jenis entiti sahaja.

Kami akan menggunakan contoh kod sebelumnya, tetapi kumpulan 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 mempunyai 10 entiti Sekolah . Apabila kita melihat log, kita dapat mengesahkan bahawa Hibernate menghantar penyataan sisipan dalam kumpulan:

"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 perkara penting yang perlu disebutkan di sini ialah penggunaan memori. Apabila kita mengekalkan entiti, Hibernate menyimpannya dalam konteks kegigihan . Sebagai contoh, jika kita mengekalkan 100,000 entiti dalam satu transaksi, kita akan mempunyai 100,000 entiti entiti dalam memori, mungkin menyebabkan OutOfMemoryException .

4.2. Batch Insert dengan Explicit Flush

Sekarang, kita akan melihat bagaimana kita dapat mengoptimumkan penggunaan memori semasa operasi kumpulan. Mari kita selami peranan konteks kegigihan.

Pertama sekali, konteks kegigihan menyimpan entiti yang baru dibuat dan juga yang diubah dalam memori. Hibernate menghantar perubahan ini ke pangkalan data apabila transaksi disegerakkan. Ini biasanya berlaku pada akhir transaksi. Walau bagaimanapun, memanggil EntityManager.flush () juga mencetuskan penyegerakan transaksi .

Kedua, konteks kegigihan berfungsi sebagai cache entiti, sehingga juga disebut sebagai cache tahap pertama. Untuk membersihkan entiti dalam konteks kegigihan, kita boleh memanggil EntityManager.clear () .

Jadi, untuk mengurangkan beban memori semasa kumpulan, kita dapat memanggil EntityManager.flush () dan EntityManager.clear () pada kod aplikasi kita, setiap kali ukuran kumpulan 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 menyingkirkan entiti dalam konteks kegigihan sehingga menjadikan Hibernate menghantar pertanyaan ke pangkalan data. Selanjutnya, dengan membersihkan konteks kegigihan, kami akan menghapus entiti Sekolah dari ingatan. Tingkah laku kumpulan akan tetap sama.

5. Batch Insert untuk Pelbagai Jadual

Sekarang mari kita lihat bagaimana kita dapat mengkonfigurasi sisipan kumpulan ketika berurusan dengan pelbagai jenis entiti dalam satu transaksi.

Apabila kita ingin mengekalkan entiti dari beberapa jenis, Hibernate membuat kumpulan yang berbeza untuk setiap jenis entiti. Ini kerana hanya boleh ada satu jenis entiti dalam satu kumpulan .

Selain itu, ketika Hibernate mengumpulkan penyataan sisipan, setiap kali menghadapi jenis entiti yang berbeza dari yang ada dalam kumpulan semasa, ia menghasilkan kumpulan baru. Ini berlaku walaupun sudah ada kumpulan untuk jenis entiti tersebut:

@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 memberikannya dua Pelajar dan mengulangi proses ini 10 kali.

Dalam log, kami melihat bahawa Hibernate menghantar pernyataan sisipan Sekolah dalam beberapa kumpulan ukuran 1 sementara kami menjangkakan hanya 2 kumpulan ukuran 5. Selain itu, penyataan sisipan Pelajar juga dihantar dalam beberapa kumpulan ukuran 2 dan bukannya 4 kumpulan 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 penyataan penyisipan dari jenis entiti yang sama, kita harus mengkonfigurasi harta hibernate.order_inserts .

Kita boleh mengkonfigurasi harta Hibernate secara manual menggunakan EntityManagerFactory :

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

Sekiranya kita menggunakan Spring Boot, kita dapat mengkonfigurasi harta tanah di application.properties:

spring.jpa.properties.hibernate.order_inserts=true

Setelah menambahkan harta ini, kami akan mempunyai 1 kumpulan untuk sisipan Sekolah dan 2 kumpulan untuk sisipan Pelajar :

"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. Kemas kini Kumpulan

Sekarang, mari beralih ke kemas kini kumpulan. Sama seperti sisipan kumpulan, kami dapat mengumpulkan beberapa penyataan kemas kini dan menghantarnya ke pangkalan data dalam satu masa.

Untuk membolehkan ini, kami akan mengkonfigurasi hibernate.order_updates dan hibernate.jdbc.batch_versioned_data hartanah .

Sekiranya kami membuat EntityManagerFactory kami secara manual, kami dapat menetapkan sifat secara program:

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 kita menggunakan Spring Boot, kita hanya akan menambahkannya ke application.properties:

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

Setelah mengkonfigurasi sifat ini, Hibernate harus mengumpulkan penyataan kemas kini dalam kumpulan:

@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 mengemas kini entiti sekolah dan Hibernate menghantar penyataan SQL dalam 2 kumpulan saiz 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 Penjanaan Id

Apabila kita ingin menggunakan kumpulan untuk memasukkan / kemas kini, kita harus mengetahui strategi penjanaan kunci utama. Sekiranya entiti kami menggunakan penjana pengenalan GenerationType.IDENTITY , Hibernate akan mematikan sisipan / kemas kini kumpulan secara senyap-senyap .

Oleh kerana entiti dalam contoh kami menggunakan generator pengenal GenerationType.SEQUENCE , Hibernate membolehkan operasi kumpulan:

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

8. Ringkasan

Dalam artikel ini, kami melihat sisipan dan kemas kini kumpulan menggunakan Hibernate / JPA.

Lihat contoh kod untuk artikel ini di Github.