Memakai @Formula Di Hibernate
29 September 2013 Tinggalkan komentar
Pada suatu hari, saya mengimplementasikan rancangan seperti yang terlihat pada UML Class Diagram berikut ini:
Sebuah Barang
memiliki hubungan one-to-many dengan EntriStok
. Class EntriStok
mewakili setiap perubahan jumlah stok untuk sebuah Barang
dimana nilai jumlah
positif menyebabkan jumlah barang bertambah dan sebaliknya, nilai jumlah
negatif menyebabkan jumlah barang berkurang. Pada kebanyakan kasus, perubahan jumlah stok untuk sebuah barang selalu disebabkan oleh transaksi seperti pembelian atau penjualan; perubahan ini diwakili oleh class EntriStokFaktur
. Terkadang jumlah barang dapat berkurang atau bertambah oleh hal-hal lain seperti bonus, kerusakan yang tidak dapat diganti, dan kesalahan yang tidak diketahui; perubahan ini diwakili oleh class EntriStokPenyesuaian
.
Saya kemudian membuat implementasi dari diagram di atas dengan menggunakan simple-jpa. Berikut ini adalah isi dari file EntriStok.groovy:
@DomainModel @Entity @Canonical @Inheritance class EntriStok { @NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate") LocalDate tanggal @NotNull Integer jumlah @NotNull @ManyToOne Barang barang }
Berikut ini adalah isi dari file EntriStokFaktur.groovy:
@DomainModel @Entity @Canonical class EntriStokFaktur extends EntriStok { @NotBlank @Size(min=2, max=100) String nomorFaktur }
Berikut ini adalah isi dari file EntriStokPenyesuaian.groovy:
@DomainModel @Entity @Canonical class EntriStokPenyesuaian extends EntriStok { @NotBlank @Size(min=2, max=100) String keterangan }
Berikut ini adalah isi dari file Barang.groovy:
@DomainModel @Entity @Canonical class Barang { @NotBlank @Size(min=2, max=10) String kode @NotBlank @Size(min=2, max=100) String nama @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true, mappedBy="barang") List<EntriStok> entriStokList = [] Integer getJumlahTersedia() { entriStokList.sum { EntriStok entriStok -> entriStok.jumlah } ?: 0 } }
Pada kode program di atas, getJumlahTersedia()
akan mengembalikan jumlah barang berdasarkan seluruh EntriStok
untuk barang tersebut. Saya memilih untuk selalu menghitung ulang jumlah barang setiap kali nilai tersebut dibutuhkan/dibaca. Alternatif lainnya yang lebih menekankan pada sisi ‘write’ adalah menghitung ulang jumlah barang setiap kali terdapat EntriStok
baru untuk Barang
tersebut, setiap kali EntriStok
untuk barang tersebut di-update, dan setiap kali EntriStok
untuk barang tersebut dihapus. Terdengar kompleks bukan? Itu sebabnya saya memilih berhadapan dengan sisi ‘read’-nya saja.
Pada saat program dijalankan, Hibernate akan menghasilkan tabel secara otomatis dengan struktur yang terlihat seperti pada gambar berikut ini:
Saya kemudian mengisi kedua tabel di atas dengan data yang jumlahnya cukup banyak, seperti yang terlihat pada hasil query berikut:
mysql> SELECT COUNT(*) FROM barang; +----------+ | COUNT(*) | +----------+ | 5449 | +----------+ 1 row in set (0.00 sec) mysql> SELECT COUNT(*) FROM entristok; +----------+ | COUNT(*) | +----------+ | 152206 | +----------+ 1 row in set (0.05 sec)
Kemudian, saya memakai fasilitas scaffolding dari simple-jpa untuk menghasilkan MVC untuk menampilkan daftar barang beserta entri stok untuk barang tersebut. Saya kemudian mengubah kode program di BarangView.groovy menjadi seperti berikut ini:
... panel(constraints: CENTER) { ... scrollPane(constraints: CENTER) { table(rowSelectionAllowed: true, id: 'table') { eventTableModel(list: model.barangList, columnNames: ['Kode', 'Nama', 'Jumlah Tersedia'], columnValues: ['${value.kode}', '${value.nama}', '${value.jumlahTersedia}']) table.selectionModel = model.barangSelection } } ... }
Saya menambahkan sebuah kolom pada tabel untuk menampilkan jumlah barang yang tersedia. Hasilnya akan terlihat seperti pada gambar berikut ini:
Secara default, simple-jpa akan sebisa mungkin ‘berbagi’ EntityManager
bila diakses dari thread yang berbeda. Pada aplikasi di atas, daftar EntriStok
dan total dari sebuah Barang
akan dibaca pada saat akan ditampilkan di tabel. Dengan demikian, tidak ada waktu loading yang lama pada saat menampilkan seluruh Barang
yang ada. Tapi akibatnya: aplikasi bisa menjadi tidak responsif bila pengguna men-scroll tabel karena query untuk membaca EntriStok
akan dikerjakan pada saat tersebut.
Griffon bekerja dengan baik dalam memanfaatkan threading, tapi masalahnya adalah EntityManager
TIDAK thread-safe!! Bila saya tidak ingin mengambil resiko terjadinya kesalahan aneh akibat EntityManager
diakses dari thread yang tidak seharusnya, maka saya dapat menambahkan baris berikut ini pada Config.groovy:
griffon.simplejpa.entityManager.lifespan = "transaction"
Konfigurasi ini akan menyebabkan setiap transaksi yang ditangani oleh simple-jpa selalu memakai EntityManager
baru. Konsekuensinya? Saya harus mengucapkan selamat tinggal pada lazy loading di transaksi berbeda! Seluruh data yang perlu ditampilkan di tabel harus di-load secara lengkap terlebih dahulu. Pertanyaannya adalah apakah ini adalah sebuah proses yang berat? Saya mengubah kode program di BarangController.groovy
agar menampilkan berapa lama waktu yang diperlukan untuk me-load seluruh objek seperti pada berikut ini:
@SimpleJpaTransaction(newSession = true) def listAll = { long startTime = System.currentTimeMillis() execInsideUIAsync { model.barangList.clear() } List barangResult = findAllBarang() barangResult.each { Barang barang -> barang.jumlahTersedia } execInsideUISync { model.barangList.addAll(barangResult) model.kodeSearch = null model.searchMessage = app.getMessage("simplejpa.search.all.message") long endTime = System.currentTimeMillis() JOptionPane.showMessageDialog(view.mainPanel, "Total waktu yang dibutuhkan = ${(endTime-startTime)/1000} detik") } }
Percobaan pertama menunjukkan bahwa dibutuhkan waktu 91,883 detik untuk membaca seluruh Barang
beserta dengan EntriStok
-nya. Ini berarti saya harus menunggu hingga 1 menit lebih hanya untuk menunggu tabel terisi dengan data yang dibutuhkan! Terlalu lama…!!!
Sepertinya Hibernate kewalahan harus me-load 5.449 object Barang
dan 152.206 object EntriStok
dari database. Tunggu…! Bukankah saya perlu menampilkan total (SUM) dari jumlah untuk seluruh EntriStok
per Barang
? Saya tidak membutuhkan masing-masing objek EntriStok
yang di-load untuk saat ini. Oleh sebab itu saya akan menggunakan annotation @Formula
. Ini adalah fitur khusus untuk Hibernate dan bukan bagian dari spesifikasi Java Persistence API (JPA) sehingga belum tentu ada di provider JPA selain Hibernate.
Annotation @Formula
akan menyebabkan sebuah atribut menjadi bersifat read-only dimana nilai dari atribut tersebut diperoleh dari ekspresi SQL. Sebagai contoh, saya akan mengubah Barang.groovy
menjadi seperti berikut ini:
@DomainModel @Entity @Canonical class Barang { @NotBlank @Size(min=2, max=10) String kode @NotBlank @Size(min=2, max=100) String nama @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true, mappedBy="barang") List<EntriStok> entriStokList = [] @Formula("(SELECT SUM(e.jumlah) FROM entristok e WHERE e.barang_id = id)") Integer jumlahTersedia }
Saat saya kembali menjalankan program, saya menemukan bahwa waktu yang dibutuhkan untuk menampilkan seluruh Barang
yang ada adalah 3,374 detik. Ini adalah selisih yang cukup jauh dibanding dengan sebelumnya!! Salah satu konsekuensinya adalah karena saya tidak me-load EntriStok
sama sekali, maka pada saat user akan menampilkan EntriStok
untuk sebuah Barang
, saya harus me-load EntriStok
secara manual (atau men-merge object Barang
tersebut ke EntityManager
baru sehingga lazy loading bisa bekerja).
Kesimpulannya: @Formula
membuat kode program menjadi tidak portabel di JPA provider selain Hibernate, tapi ia sangat berguna untuk meningkatkan kinerja terutama untuk kasus saya. Solusi lain yang dapat saya tempuh adalah memakai materialized view. Sayang sekali MySQL Server belum mendukung materialized view. Selain itu saya dapat meningkatkan kecepatan operasi SUM dengan menggunakan data partitioning. MySQL Server memang sudah mendukung data partitioning seperti melakuan partisi tabel entristok
per tahun berdasarkan tanggal. Akan tetapi, untuk saat ini, partitioning di MySQL Server belum mendukung parallelization yang seharusnya dapat mempercepat fungsi agregasi seperti SUM
(karena total untuk setiap tahun boleh dihitung secara bersamaan, baru kemudian ditambahkan). Fitur ini mungkin akan ditambahkan di kemudian hari.
Anda harus log masuk untuk menerbitkan komentar.