Panduan Transaksi Di Plugin simple-jpa


Plugin simple-jpa 0.4 dapat di-install dengan memberikan perintah install-plugin simple-jpa 0.4

Pada spesifikasi JPA, setiap operasi JPA disarankan untuk selalu berada dalam sebuah transaction.  Bila dijalankan dalam sebuah container, masalah transaction sudah ditangani secara otomatis oleh JTA (Java Transaction API) sehingga developer tidak perlu repot.  Tapi pada aplikasi desktop, pengguna dapat memilih untuk memakai implementasi JTA untuk aplikasi desktop atau mengatur transaction secara manual.  Agar sederhana, plugin simple-jpa mengatur transaction secara manual.

Untuk menunjukkan penanganan transaction di simple-jpa, saya akan membuat sebuah proyek sederhana dengan tiga domain class yang isinya adalah:

@DomainModel @Entity @Canonical
class PenjualanEceran {

    @Size(min=5, max=5)
    String kodeTransaksi

    @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
    DateTime tanggal

    @NotEmpty @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true, mappedBy="penjualanEceran")
    List<ItemTransaksi> listItemTransaksi = []

    ItemTransaksi tambahItemTransaksi(Stok stok, Integer jumlah) {
        stok.kurangiStok(jumlah)
        ItemTransaksi itemTransaksi = new ItemTransaksi(stok, jumlah, this)
        listItemTransaksi << itemTransaksi
        itemTransaksi
    }

}

@DomainModel @Entity
@TupleConstructor @EqualsAndHashCode
@ToString(excludes = "penjualanEceran")
class ItemTransaksi {

    @ManyToOne(cascade = CascadeType.MERGE)
    Stok stok

    @NotNull @Min(1l)
    Integer jumlah

    @ManyToOne
    PenjualanEceran penjualanEceran

}

@DomainModel @Entity @Canonical
class Stok {

    @Size(min=5, max=5)
    String kode

    @Size(min=2, max=50)
    String nama

    @NotNull @Min(0l)
    Integer jumlahTersedia

    void kurangiStok(Integer jumlah) {
        if (jumlahTersedia < jumlah) {
            throw new Exception("Jumlah stok tidak mencukupi!")
        }
        jumlahTersedia -= jumlah
    }

}

Pada domain class di atas, bila PenjualanEceran dilakukan, maka jumlah Stok akan berkurang.  Bila jumlah Stok tidak mencukupi, maka PenjualanEceran akan menimbulkan kesalahan atau Exception!

TIPS: Sebaiknya kode program business logic diletakkan bersamaan dengan domain class, bukan di controller.  Dengan demikian, business logic yang berhubungan dengan domain class tersebut akan bisa diakses dengan mudah dimanapun domain object berada. Selain itu, kode program akan lebih mudah dimengerti karena business logic tidak berhubungan langsung dengan layer lain seperti tampilan (presentation) dan database (persistence).

Saya kemudian melakukan proses scaffolding dengan memberikan perintah:

generate-all * --startup-group=MainGroup

Seluruh controller yang dihasilkan memiliki annotation @SimpleJpaTransaction.  Hal ini menandakan bahwa seluruh method dan definisi closure dalam controller tersebut akan memiliki transaction yang diatur oleh simple-jpa, kecuali method tidak plain seperti mvcGroupInit() dan mvcGroupDestroy().

Bila pengguna tidak ingin memakai annotation @SimpleJpaTransaction, maka ia harus melakukan pengaturan transaction secara manual di dalam setiap method dan closure yang mengandung operasi JPA, dengan memanggil method berikut ini:

  • beginTransaction() akan memulai sebuah transaction baru.  Parameter pertama adalah sebuah boolean yang bila bernilai false akan selalu memulai transaksi baru (tidak ada propagasi).  Parameter kedua adalah sebuah boolean yang bila bernilai true akan selalu membuat EntityManager baru.
  • commitTransaction() akan mengakhiri transaction dan menyimpan seluruh perubahan yang telah dibuat.
  • rollbackTransaction() akan membatalkan transaction.

PENTING: Sangat tidak disarakankan untuk memanggil method di atas bila menggunakan annotation @SimpleJpaTransaction karena dapat mengacaukan pengaturan transaction secara otomatis.

Perilaku default untuk setiap closure dan method bila memakai annotation @SimpleJpaTransaction adalah:

  • Bila method atau closure yang dikerjakan berakhir secara normal, maka transaction akan di-commit dan perubahan pada database akan ditulis.
  • Bila method atau closure mengalami kesalahan dan terdapat Exception, maka transaction akan di-rollback dan perubahan tidak akan disimpan ke database.
  • Bila method atau closure memanggil method/closure lain yang juga berada dalam annotation @SimpleJpaTransaction, maka pemanggilan tersebut akan dianggap berada dalam 1 transaction yang sama.

PENTING: Operasi penyimpanan, update atau penghapusan (INSERT, UPDATE, DELETE) tidak akan benar-benar dilakukan pada database sebelum mencapai baris terakhir dari method atau closure.  Hal ini dapat menyebabkan kebingungan bila pengguna memberikan query SQL secara manual sebelum method/closure selesai dikerjakan, karena ia tidak akan menemukan perubahan di tabel.

Untuk melanjutkan kode program yang telah saya buat di atas, saya akan mengubah kode program di PenjualanEceranController.groovy di closure save dari:

def save = {
  PenjualanEceran penjualanEceran = new PenjualanEceran('kodeTransaksi': model.kodeTransaksi, 'tanggal': model.tanggal, 'listItemTransaksi': new ArrayList(model.listItemTransaksi))
  penjualanEceran.listItemTransaksi.each { ItemTransaksi itemTransaksi ->
    itemTransaksi.penjualanEceran = penjualanEceran
  }
  ...
}

menjadi seperti berikut ini:

def save = {
  PenjualanEceran penjualanEceran = new PenjualanEceran('kodeTransaksi': model.kodeTransaksi, 'tanggal': model.tanggal)
  model.listItemTransaksi.each {
    penjualanEceran.tambahItemTransaksi(it)
  }
  ...
}

Karena PenjualanEceranController memiliki annotation @SimpleJpaTransaction, maka closure save secara otomatis akan dikerjakan dalam sebuah transaction.  Untuk melihat perilaku transaction yang ada, saya akan melihat hasil log program.  log dapat dilihat di window Run dan juga dapat dialihkan ke file bila diperlukan.  Untuk mencetak informasi yang lebih detail, saya menambahkan baris berikut ini pada Config.groovy:

log4j = {

   ...
   debug  'griffon.app.controller',
          'simplejpa'
   ...
}

Setelah itu, saya menjalakan program dan menambahkan sebuah PenjualanEceran secara normal. Saya akan memperoleh hasil log yang terlihat seperti berikut ini:

INFO  simplejpa.SimpleJpaHandler - Begin transaction from thread 23...
INFO  simplejpa.SimpleJpaHandler - Creating a new entity manager...
INFO  simplejpa.SimpleJpaHandler - Reusing previous entity manager...
INFO  simplejpa.SimpleJpaHandler - List of cached EntityManager: 22=TransactionHolder[em=org.hibernate.ejb.EntityManagerImpl@16c4313, resumeLevel=0], 23=TransactionHolder[em=org.hibernate.ejb.EntityManagerImpl@16c4313, resumeLevel=0]
INFO  simplejpa.transaction.TransactionHolder - Start a new transaction...
DEBUG org.hibernate.engine.transaction.spi.AbstractTransactionImpl - begin
DEBUG org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction - initial autocommit status: false
DEBUG simplejpa.transaction.TransactionHolder - Now in tr [1].
...
DEBUG org.hibernate.SQL - insert into PenjualanEceran (createdDate, deleted, kodeTransaksi, modifiedDate, tanggal, id) values (?, ?, ?, ?, ?, ?)
DEBUG org.hibernate.SQL - insert into ItemTransaksi (createdDate, deleted, jumlah, modifiedDate, penjualanEceran_id, stok_id, id) values (?, ?, ?, ?, ?, ?, ?)
DEBUG org.hibernate.SQL - update Stok set createdDate=?, deleted=?, jumlahTersedia=?, kode=?, modifiedDate=?, nama=? where id=?
DEBUG org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction - committed JDBC Connection
DEBUG simplejpa.transaction.TransactionHolder - Now in tr  [no transaction].
DEBUG griffon.app.controller.project.PenjualanEceranController - end of finally_block

Terlihat bahwa setelah tombol ‘Save’ di-klik, sebuah transaction baru akan dibuat.   Setelah closure save selesai dikerjakan, Hibernate akan mengerjakan 3 operasi SQL dan transaction akan di-commit.   Semua proses ini berlangsung secara otomatis tanpa sepengetahuan developer karena simple-jpa memakai AST transformation yang menambahkan kode program try-catch-finally pada setiap method dan closure yang ada di controller.

PENTING: Closure yang berada dalam execInsideUIAsync dapat dianggap TIDAK berada dalam transaction (perilakunya tidak dapat ditebak), seperti yang diperlihatkan di ilustrasi berikut:

def test = {

   // berada dalam transaction

   execInsideUIAsync {
       // dapat dianggap tidak berada dalam transaction
       // karena kode program disini dikerjakan
       // secara asynchronous, sehingga
       // transaction mungkin sudah di-commit/di-rollback
       // 
       // ini adalah perilaku yang wajar karena
       // kode program disini harus ringkas dan cepat
       // dan hanya memanipulasi view
   }

   // berada dalam transaction

}

Mengatur Transaction

Annotation @SimpleJpaTransaction tidak hanya dapat dipakai di-class, tetapi juga dapat dipakai langsung pada method atau deklarasi closure, seperti yang terlihat pada ilustrasi berikut ini:

class PenjualanEceranController {

    def listAll = {
    	// tidak berada dalam transaction
    }

    def search = {
    	// tidak berada dalam transaction
    }

    @SimpleJpaTransaction
    def save = {
    	// BERADA dalam transaction
    }

    @SimpleJpaTransaction
    def delete = {
    	// BERADA dalam transaction
    }
}

Transaksi juga dapat diatur lebih lanjut dengan memberikan salah satu nilai berikut ini:

  • Policy.PROPAGATE – Ini adalah nilai default bila tidak ada nilai yang diberikan. Penggunaan nilai ini menyebabkan seluruh method lain yang dipanggil oleh method ini menjadi bagian dari satu transaction yang sama.
  • Policy.NO_PROPAGATE – Penggunaan nilai ini menyebabkan masing-masing method akan dikerjakan dalam transaction sendiri-sendiri, walaupun method tersebut memanggil yang lainnya.
  • Policy.SKIP – Penggunaan nilai ini menyebabkan method tersebut tidak akan dijalankan dalam sebuah transaction. Hal ini berguna untuk meningkatkan kinerja pada method yang tidak melakukan operasi database sehingga tidak membutuhkan transaction.

PENTING: Bila sebuah class memiliki annotation @SimpleJpaTransaction di deklarasi class dan juga di method atau definisi closure, maka annotation @SimpleJpaTransaction yang dipakai adalah yang terletak di method atau closure, bukan yang berada di class.

Untuk menunjukkan perilaku Policy.PROPAGATE, saya akan menggunakan kode program berikut ini:

class PenjualanEceranController {

    @SimpleJpaTransaction(SimpleJpaTransaction.Policy.PROPAGATE)
    void methodA() {
        PenjualanEceran penjualanEceran1 = new PenjualanEceran(kodeTransaksi: 'TR001', tanggal: DateTime.now())
        penjualanEceran1.tambahItemTransaksi(findStokByKode("STK01")[0], 1)
        persist(penjualanEceran1)
        methodB()
    }

    @SimpleJpaTransaction(SimpleJpaTransaction.Policy.PROPAGATE)
    void methodB() {
        PenjualanEceran penjualanEceran2 = new PenjualanEceran(kodeTransaksi: 'TR002', tanggal: DateTime.now())
        penjualanEceran2.tambahItemTransaksi(findStokByKode("STK02")[0], 1)
        persist(penjualanEceran2)
        methodC()
    }

    @SimpleJpaTransaction(SimpleJpaTransaction.Policy.PROPAGATE)
    void methodC() {
        PenjualanEceran penjualanEceran3 = new PenjualanEceran(kodeTransaksi: 'TR003', tanggal: DateTime.now())
        penjualanEceran3.tambahItemTransaksi(findStokByKode("STK03")[0], 1)
        persist(penjualanEceran3)
        throw new Exception("TERJADI KESALAHAN DISINI!")
    }
}

Pada kode program di atas, methodA() memanggil methodB() yang selanjutnya memanggil methodC().  Kemudian pada methodC(),saya mensimulasikan terjadinya kesalahan.

Efek dari penggunaan Policy.PROPAGATE adalah transaksi hanya akan di-commit bila sudah mencapai akhir dari methodA().  Bila pada methodB() atau methodC() terjadi kesalahan, maka transaksi akan di-rollback.  Dengan kata lain, bila salah satu dari TR001, TR002, dan TR003 gagal disimpan, maka tidak akan ada satupun PenjualanEceran yang disimpan.  Gambar berikut ini memperlihatkan ilustrasi penggunaan Policy.PROPAGATE:

Ilustrasi Transaksi Dengan Policy.PROPAGATE

Ilustrasi Transaksi Dengan Policy.PROPAGATE

TIPS:  Sebenarnya annotation @SimpleJpaTransaction(SimpleJpaTransaction.Policy.PROPAGATE) tidak perlu diberikan karena merupakan nilai default.

Bila memakai Policy.NO_PROPAGATE, maka masing-masing method akan dikerjakan dalam transaction yang terpisah. Sebagai contoh, saya mengubah kode program di atas menjadi seperti berikut ini:

class PenjualanEceranController {
    @SimpleJpaTransaction(SimpleJpaTransaction.Policy.NO_PROPAGATE)
    void methodA() {
        PenjualanEceran penjualanEceran1 = new PenjualanEceran(kodeTransaksi: 'TR001', tanggal: DateTime.now())
        penjualanEceran1.tambahItemTransaksi(findStokByKode("STK01")[0], 1)
        persist(penjualanEceran1)
        methodB()
    }

    @SimpleJpaTransaction(SimpleJpaTransaction.Policy.NO_PROPAGATE)
    void methodB() {
        PenjualanEceran penjualanEceran2 = new PenjualanEceran(kodeTransaksi: 'TR002', tanggal: DateTime.now())
        penjualanEceran2.tambahItemTransaksi(findStokByKode("STK02")[0], 1)
        persist(penjualanEceran2)
        methodC()
    }

    @SimpleJpaTransaction(SimpleJpaTransaction.Policy.NO_PROPAGATE)
    void methodC() {
        PenjualanEceran penjualanEceran3 = new PenjualanEceran(kodeTransaksi: 'TR003', tanggal: DateTime.now())
        penjualanEceran3.tambahItemTransaksi(findStokByKode("STK03")[0], 1)
        persist(penjualanEceran3)
        throw new Exception("TERJADI KESALAHAN DISINI!")
    }
}

Penggunaan  Policy.NO_PROPAGATE menyebabkan PenjualanEceran dengan kode TR001 yang dibuat oleh methodA() dan kode TR002 yang dibuat oleh methodB() tersimpan.   Hanya PenjualanEceran dengan kode TR003 yang dibuat oleh methodC() yang tidak tersimpan di database.   Hal ini biasanya tidak diharapkan karena dapat menyebabkan data menjadi tidak konsisten.  Gambar berikut ini memperlihatkan ilustrasi penggunaan Policy.NO_PROPAGATE:

Ilustrasi Transaksi Dengan Policy.NO_PROPAGATE

Ilustrasi Transaksi Dengan Policy.NO_PROPAGATE

Policy.SKIP dapat dipakai pada method atau definisi closure yang tidak membutuhkan transaction. Sebagai contoh, pada kode program berikut ini:

@SimpleJpaTransaction
class LatihanController {

    def save = {
   	...
    }

    def delete = {
    	...
    }

    @SimpleJpaTransaction(SimpleJpaTransaction.Policy.SKIP)
    def hitung = {
    	model.hasil = model.angka1 + model.angka2
    }

}

PENTING: Bahkan bila sebuah method atau closure tidak berada dalam transaction, setiap pemanggilan method simple-jpa akan menciptakan transaction yang hanya berlaku selama pemanggilan.

Mengatur EntityManager

Setiap operasi JPA harus berdasarkan sebuah EntityManager.   Selain menjadi pusat operasi, EntityManager juga dapat dipandang sebagai sebuah cache di memori yang menampung hasil query sehingga untuk mengakses object yang sudah pernah di-query sebelumnya tidak perlu ke database lagi.

Penggunaan EntityManager menimbulkan beberapa permasalahan yang harus dihadapi, misalnya:

  1. EntityManager tidak bersifat thread-safe!  Dengan demikian, EntityManager tidak bisa diakses oleh thread yang berbeda secara bersamaan.
  2. EntityManager adalah cache object serta perubahannya.  Dengan demikian, EntityManager perlu ditutup bila telah selesai digunakan.   EntityManager yang tidak ditutup akan menampung object yang tidak dibutuhkan (telah selesai dipakai) hingga aplikasi ditutup.  Ini akan menyebabkan memori cepat habis terpakai.

Untuk mengatasi permasalahan pertama, simple-jpa akan secara otomatis membuat sebuah EntityManager per thread per controller.  Setiap controller memiliki EntityManager-nya masing-masing.   Setiap thread untuk controller memiliki EntityManager-nya masing-masing.

PENTING: Secara default, setiap kali EntityManager dibuat, akan terbentuk sebuah JDBC connection ke database. Saat EntityManager ditutup, JDBC connection tersebut juga akan ditutup. Hal ini akan menyebabkan sering kali terdapat koneksi yang dibuka dan ditutup ke database.  Untuk meningkatkan kinerja, gunakan connection pool.

simple-jpa memungkinkan pengaturan kapan sebuah EntityManager dibuat dan ditutup, dengan menambahkan baris seperti berikut ini di Config.groovy:

griffon.simplejpa.entityManager.lifespan = "Manual"

Nilai yang dapat diberikan adalah:

  • “Manual” – Ini adalah nilai default.
  • “Transaction”

PENTING: Pengguna TIDAK DISARANKAN untuk mengubah nilai griffon.simplejpa.entitymanager.lifespan kecuali memang dibutuhkan!

Penggunaan nilai “Manual” akan menyebabkan EntityManager yang telah dibuat tidak akan pernah ditutup secara otomatis. Pengguna wajib memberikan perintah destroyEntityManager() untuk menutup seluruh EntityManager. Salah satu keuntungan metode ini adalah lazy loading tetap bekerja dengan baik.  Lazy loading adalah perilaku JPA dimana pada saat membaca entity yang mengandung Collection seperti ArrayList atau Set,  JPA tidak akan melakukan query untuk mengambil nilainya.  Query baru akan dilakukan pada saat Collection tersebut diakses (misalnya pada saat getKelas() dipanggil).  Bila EntityManager yang berisi entity tersebut sudah ditutup, entitas akan menjadi detached sehingga akses ke Collection akan menimbulkan kesalahan karena JPA.

Salah satu strategi yang dapat dipakai adalah session-per-mvcgroup, dimana pengguna menutup EntityManager:

  • Memanggil method mvcGroupDestroy() pada mvcGroupDestroy() untuk menutup seluruh EntityManager yang berhubungan dengan controller ini (termasuk yang berada di thread berbeda) setelah mvc group tidak dipakai lagi.
  • Memberikan annotation @SimpleJpaTransaction(newSession=true) pada clsoure listAll() dan search. Hal ini akan menyebabkan EntityManager untuk thread ini ditutup dan EntityManager baru dibuat sebelum definisi closure tersebut dikerjakan.

Sebagai contoh, secara default, hasil scaffolding akan menutup EntityManager seperti berikut ini:

void mvcGroupDestroy() {
    destroyEntityManager()
}

@SimpleJpaTransaction(newSession = true)
def listAll = {
    ...
}

@SimpleJpaTransaction(newSession = true)
def search = {
    ...
}

Strategi session-per-mvcgroup akan bekerja dengan baik bila setiap mvc group sedikit atau tidak berinteraksi antara satu dengan yang lainnya.

Strategi lain, bila aplikasi sangat sederhana sekali, maka developer boleh mencoba untuk tidak menutup EntityManager hingga aplikasi berakhir atau ditutup oleh pengguna.

PENTING: Jangan pernah memanggil method atau closure yang memiliki annotation @SimpleJpaTransaction(newSession=true) dari method atau closure lain yang juga berada dalam transaction karena dapat merusak transaction untuk method atau closure lain tersebut. Sebagai contoh, closure listAll atau search di atas boleh dipanggil oleh view tetapi tidak boleh dipanggil oleh method/closure lain di controller.

EntityManager yang dibiarkan terbuka terlalu lama selain boros memori, juga dapat menimbulkan data yang tidak akurat. Sebagai contoh, seandainya terdapat 2 EntityManager berbeda yang membaca sebuah entity, kemudian salah satu EntityManager melakukan menghapus entity tersebut, maka EntityManager lainnya TIDAK akan melihat perubahan tersebut dan menganggap entity masih ada, seperti yang terlihat pada gambar berikut ini:

Contoh Kesalahan Bila EntityManager Dibuka Terlalu Lama

Contoh Kesalahan Bila EntityManager Dibuka Terlalu Lama

Bila nilai griffon.simplejpa.entitymanager.lifespan adalah “Transaction”, maka EntityManager akan dibuat pada saat transaksi dimulai dan akan ditutup setelah transaksi selesai.  Ini adalah cara yang direkomendasikan oleh Hibernate JPA.  Dengan cara ini, pengguna akan terhindar dari masalah pemborosan memori dan data yang tidak konsisten (stale data).  Akan tetapi, cara ini menyebabkan lazy loading tidak bekerja. Solusinya, pengguna harus memberikan konfigurasi eager fetching atau menggunakan query yang lengkap setiap kali membaca sebuah entity dari database.

PENTING: Konfigurasi griffon.simplejpa.entitymanager.lifespan dengan nilai “Transaction” dapat menyebabkan kode program hasil scaffolding tidak bekerja, sehingga tidak disarankan untuk dipakai.

Perihal Solid Snake
I'm nothing...

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: