Memakai @Formula Di Hibernate


Pada suatu hari, saya mengimplementasikan rancangan seperti yang terlihat pada UML Class Diagram berikut ini:

Rancangan UML

Rancangan UML

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:

Tabel yang dihasilkan

Tabel yang dihasilkan

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:

Tampilan program

Tampilan program

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.

Tentang Solid Snake
I'm nothing...

Apa komentar Anda?