Batching store secara transparan
Kami menggunakan kerangka kerja dan versi berikut:
jOOQ 3.11.1Spring Boot 2.3.1.RELEASESpring 5.2.7.RELEASE
Saya memiliki masalah ketika beberapa logika bisnis kami dibagi menjadi unit logis yang terlihat sebagai berikut:
- Permintaan yang berisi transaksi pengguna diterima
- Permintaan ini berisi berbagai informasi, seperti jenis transaksi, produk mana yang menjadi bagian dari transaksi ini, jenis pembayaran yang dilakukan, dll.
- Atribut ini kemudian disimpan secara individual dalam database.
Dalam kode, ini terlihat kira-kira sebagai berikut:
TransactionRecord transaction = transactionRepository.create();
transaction.create(creationCommand);`
Di Transaction#create(yang berjalan secara transaksional), hal seperti berikut ini terjadi:
storeTransaction();
storePayments();
storeProducts();
// ... other relevant information
Transaksi tertentu dapat memiliki berbagai jenis produk dan atribut, yang semuanya disimpan. Banyak dari atribut ini menghasilkan UPDATEpernyataan, sementara beberapa mungkin menghasilkan INSERTpernyataan - sulit untuk diketahui sepenuhnya sebelumnya.
Misalnya, storeProductsmetode tersebut terlihat kira-kira sebagai berikut:
products.forEach(product -> {
ProductRecord record = productRepository.findProductByX(...);
if (record == null) {
record = productRepository.create();
record.setX(...);
record.store();
} else {
// do something else
}
});
Jika produknya baru, berarti sudah INSERTed. Jika tidak, perhitungan lain mungkin dilakukan. Bergantung pada ukuran transaksinya, transaksi pengguna tunggal ini jelas dapat menghasilkan hingga O(n)panggilan / bolak -balik database, dan bahkan lebih, bergantung pada atribut lain yang ada. Dalam transaksi yang memiliki atribut dalam jumlah besar, hal ini dapat mengakibatkan ratusan panggilan database untuk satu permintaan (!). Saya ingin menurunkan ini sedekat mungkin O(1)sehingga memiliki beban yang lebih dapat diprediksi pada database kami.
Secara alami, penyisipan / pembaruan batch dan massal muncul di sini. Apa yang ingin saya lakukan adalah mengumpulkan semua pernyataan ini ke dalam satu batch menggunakan jOOQ, dan mengeksekusi setelah pemanggilan metode yang berhasil sebelum melakukan. Saya telah menemukan beberapa kiriman ( SO Post , jOOQ API , jOOQ GitHub Feature Request ) di mana topik ini disebutkan secara implisit, dan satu kiriman grup pengguna yang tampaknya secara eksplisit terkait dengan masalah saya.
Karena saya menggunakan Springbersama jOOQ, saya yakin solusi ideal saya (sebaiknya deklaratif) akan terlihat seperti berikut:
@Batched(100) // batch size as parameter, potentially
@Transactional
public void createTransaction(CreationCommand creationCommand) {
// all inserts/updates above are added to a batch and executed on successful invocation
}
Agar ini berfungsi, saya membayangkan saya perlu mengelola sumber daya scoped ( ThreadLocal/ Transactional/ Sessionscope) yang dapat melacak batch saat ini seperti itu:
- Sebelum memasukkan metode, batch kosong dibuat jika metode tersebut
@Batched, - Sebuah custom
DSLContext(mungkin diperpanjangDefaultDSLContext) yang tersedia melalui DI memiliki sebuahThreadLocalflag yang melacak apakah pernyataan saat ini harus di-batch atau tidak, dan jika demikian - Hentikan panggilan dan tambahkan ke batch saat ini alih-alih mengeksekusinya dengan segera.
Namun, langkah 3 mengharuskan harus menulis ulang sebagian besar kode kami dari (IMO) yang relatif dapat dibaca:
records.forEach(record -> {
record.setX(...);
// ...
record.store();
}
untuk:
userObjects.forEach(userObject -> {
dslContext.insertInto(...).values(userObject.getX(), ...).execute();
}
yang akan menggagalkan tujuan memiliki abstraksi ini sejak awal, karena bentuk kedua juga dapat ditulis ulang menggunakan DSLContext#batchStoreatau DSLContext#batchInsert. Namun IMO, batching dan penyisipan massal tidak boleh tergantung pada pengembang individu dan harus dapat ditangani secara transparan di tingkat yang lebih tinggi (misalnya oleh kerangka kerja).
Saya menemukan keterbacaan jOOQAPI menjadi manfaat luar biasa dalam menggunakannya, namun tampaknya itu tidak cocok (sejauh yang saya tahu) untuk intersepsi / ekstensi dengan sangat baik untuk kasus-kasus seperti ini. Apakah mungkin, dengan jOOQ 3.11.1API (atau bahkan saat ini), untuk mendapatkan perilaku yang mirip dengan API sebelumnya dengan penanganan batch / massal yang transparan? Apa artinya ini?
EDIT:
Salah satu solusi yang mungkin tetapi sangat meretas yang terlintas dalam pikiran untuk mengaktifkan batching penyimpanan transparan adalah seperti berikut:
- Buat
RecordListenerdan tambahkan sebagai defaultConfigurationkapan pun pengelompokan diaktifkan. - Di
RecordListener#storeStart, tambahkan kueri ke batch Transaksi saat ini (misalnya di aThreadLocal<List>) - The
AbstractRecordmemilikichangedbendera yang diperiksa (org.jooq.impl.UpdatableRecordImpl#store0,org.jooq.impl.TableRecordImpl#addChangedValues) sebelum menyimpan. Menyetel ulang ini (dan menyimpannya untuk digunakan nanti) membuat operasi penyimpanan tidak bisa dioperasikan. - Terakhir, setelah pemanggilan metode berhasil tetapi sebelum melakukan:
- Setel ulang
changesbendera dari masing-masing rekaman ke nilai yang benar - Panggil
org.jooq.UpdatableRecord#store, kali ini tanpaRecordListeneratau saat melewatkanstoreStartmetode (mungkin menggunakanThreadLocaltanda lain untuk memeriksa apakah pengelompokan telah dilakukan).
Sejauh yang saya tahu, pendekatan ini seharusnya berhasil, secara teori. Jelas, ini sangat hacky dan rawan rusak karena internal perpustakaan dapat berubah kapan saja jika kodenya bergantung pada Refleksi untuk bekerja.
Adakah yang tahu cara yang lebih baik, hanya dengan menggunakan jOOQAPI publik ?
Jawaban
jOOQ 3.14 solusi
Anda telah menemukan permintaan fitur yang relevan # 3419 , yang akan menyelesaikannya pada level JDBC mulai dari jOOQ 3.14. Anda dapat menggunakan BatchedConnectionsecara langsung, membungkus koneksi Anda sendiri untuk mengimplementasikan di bawah ini, atau menggunakan API ini:
ctx.batched(c -> {
// Make sure all records are attached to c, not ctx, e.g. by fetching from c.dsl()
records.forEach(record -> {
record.setX(...);
// ...
record.store();
}
});
jOOQ 3.13 dan sebelum solusi
Untuk saat ini, hingga # 3419 diimplementasikan (ini akan terjadi, di jOOQ 3.14), Anda dapat mengimplementasikannya sendiri sebagai solusi. Anda harus mem-proxy JDBC Connectiondan PreparedStatementdan ...
... intersep semua:
- Panggilan ke
Connection.prepareStatement(String), mengembalikan pernyataan proxy yang di-cache jika string SQL sama dengan pernyataan yang disiapkan terakhir, atau sekumpulan mengeksekusi pernyataan yang terakhir disiapkan dan membuat yang baru. - Panggilan ke
PreparedStatement.executeUpdate()danexecute(), dan gantikan dengan panggilan kePreparedStatement.addBatch()
... delegasikan semua:
- Panggilan ke API lain, seperti misalnya
Connection.createStatement(), yang harus membersihkan batch yang di-buffer di atas, lalu memanggil API delegasi sebagai gantinya.
Saya tidak akan merekomendasikan meretas cara Anda di sekitar jOOQ RecordListenerdan SPI lainnya, saya pikir itu adalah tingkat abstraksi yang salah untuk menyangga interaksi basis data. Selain itu, Anda juga ingin menggabungkan jenis pernyataan lainnya.
Perhatikan bahwa secara default, jOOQ UpdatableRecordmencoba untuk mengambil nilai identitas yang dihasilkan (lihat Settings.returnIdentityOnUpdatableRecord), yang mencegah pengelompokan. store()Panggilan semacam itu harus segera dijalankan, karena Anda mungkin mengharapkan nilai identitas tersedia.