Mengatasi Permasalahan N+1 Pada Query Di Hibernate

Apa itu permasalahan N+1? Sebagai contoh, anggap saja saya memiliki sebuah JPA entity seperti berikut ini:

@NamedEntityGraphs([
    @NamedEntityGraph(name='FakturJualOlehSales.Piutang', attributeNodes=[
        @NamedAttributeNode('listItemFaktur'),
        @NamedAttributeNode('piutang')
    ])
])
class FakturJualOlehSales extends FakturJual {

    @NotNull(groups=[Default,InputPenjualanOlehSales]) @ManyToOne
    Konsumen konsumen

    @NotNull(groups=[Default]) @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
    LocalDate jatuhTempo

    @OneToOne(cascade=CascadeType.ALL, orphanRemoval=true, fetch=FetchType.LAZY)
    KewajibanPembayaran piutang

    @OneToOne(cascade=CascadeType.ALL, orphanRemoval=true, fetch=FetchType.LAZY)
    BonusPenjualan bonusPenjualan

    @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true, fetch=FetchType.EAGER)
    @JoinTable(name='FakturJual_retur')
    @OrderColumn
    List<PenerimaanBarang> retur = []

    ...

}

Class FakturJualOlehSales memiliki banyak relasi dengan class lainnya. Ini adalah sesuatu yang wajar terutama bila perancangan dilakukan dengan menggunakan metode DDD yang mengedepankan aggregation. Sebagai turunan dari FakturJual, class FakturJualOlehSales memiliki relasi one-to-many dengan ItemFaktur. Class FakturJualOlehSales juga memiliki relasi one-to-one dengan KewajibanPembayaran (untuk mewakili piutang yang ditimbulkan oleh faktur ini) dan BonusPenjualan (untuk mewakili bonus yang diberikan pada faktur ini). Class KewajibanPembayaran selanjutnya memiliki relasi one-to-many dengan Pembayaran. Begitu juga, class BonusPenjualan selanjutnya memiliki relasi one-to-many dengan ItemBarang. Selain itu, class FakturJualOlehSales juga memiliki relasi one-to-many dengan PenerimaanBarang untuk mewakili retur.

Sebuah class dengan relasi yang kompleks, bukan? Tapi saya tidak selalu butuh semua nilai relasi yang ada setiap kali berhadapan dengan FakturJualOlehSales. Sebagai contoh, pada screen untuk entry data faktur, saya tidak perlu menampilkan nilai piutang. Tetapi pada screen untuk memasukkan data pembayaran piutang, saya perlu nilai piutang tetapi tidak perlu informasi seperti bonusPenjualan.

Oleh sebab itu, saya memberikan nilai atribut fetch=FetchType.LAZY pada beberapa atribut agar Hibernate tidak men-query atribut tersebut. Teknik ini disebut lazy loading. Nilai dari atribut piutang atau bonusPenjualan hanya akan di-query pada saat ia diakses. Ini hanya berlaku selama entity masih berada dalam pengelolaan EntityManager. Bila sudah diluar cakupan EntityManager, saya akan memperoleh pesan kesalahan LazyLoadingException yang sangat terkenal.

Cara lain untuk membaca nilai yang lazy adalah dengan menggunakan query JP QL yang melakukan join fetch secara manual. Khusus untuk JPA 2.1, pilihan yang lebih nyaman adalah dengan menggunakan fasilitas named entity graph. Dengan fasilitas ini, saya tidak perlu menghabiskan banyak waktu memikirkan query! Sebagai contoh, saya mendeklarasikan sebuah named entity graph dengan nama FakturJualOlehSales.Piutang yang akan menyertakan nilai atribut piutang pada saat FakturJualOlehSales dibaca dari database.

Berikut adalah contoh kode program yang memakai named entity graph melalui simple-jpa:

FakturJualRepository repo = simplejpa.SimpleJpaUtil.instance
    .repositoryManager.findRepository('FakturJual')

int start = System.currentTimeMillis();
repo.findAllFakturJualOlehSalesFetchPiutang();
int stop = System.currentTimeMillis();

println "Delta = ${stop-start}"

Walaupun kode program di atas terlihat sederhana, tapi kinerjanya tidak memuaskan! Hibernate ternyata mengerjakan sangat banyak query, seperti:

select distinct ... from FakturJual fakturjual0_ left outer join KewajibanPembayaran kewajibanp1_ on fakturjual0_.piutang_id=kewajibanp1_.id left outer join FakturJual_listItemFaktur listitemfa2_ on fakturjual0_.id=listitemfa2_.FakturJual_id where ...

select ... from Produk produk0_ inner join Satuan satuan1_ on produk0_.satuan_id=satuan1_.id left outer join Supplier supplier2_ on produk0_.supplier_id=supplier2_.id where produk0_.id=?

select ... from Produk produk0_ inner join Satuan satuan1_ on produk0_.satuan_id=satuan1_.id left outer join Supplier supplier2_ on produk0_.supplier_id=supplier2_.id where produk0_.id=?

select ... from Produk produk0_ inner join Satuan satuan1_ on produk0_.satuan_id=satuan1_.id left outer join Supplier supplier2_ on produk0_.supplier_id=supplier2_.id where produk0_.id=?

...

select ... from Konsumen konsumen0_ inner join Region region1_ on konsumen0_.region_id=region1_.id left outer join Region region2_ on region1_.bagianDari_id=region2_.id inner join Sales sales3_ on konsumen0_.sales_id=sales3_.id inner join Gudang gudang4_ on sales3_.gudang_id=gudang4_.id where konsumen0_.id=?

select ... from Konsumen konsumen0_ inner join Region region1_ on konsumen0_.region_id=region1_.id left outer join Region region2_ on region1_.bagianDari_id=region2_.id inner join Sales sales3_ on konsumen0_.sales_id=sales3_.id inner join Gudang gudang4_ on sales3_.gudang_id=gudang4_.id where konsumen0_.id=?

select ... from Konsumen konsumen0_ inner join Region region1_ on konsumen0_.region_id=region1_.id left outer join Region region2_ on region1_.bagianDari_id=region2_.id inner join Sales sales3_ on konsumen0_.sales_id=sales3_.id inner join Gudang gudang4_ on sales3_.gudang_id=gudang4_.id where konsumen0_.id=?

...

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

...

select ... from kewajibanpembayaran_items listpembay0_ left outer join BilyetGiro bilyetgiro1_ on listpembay0_.bilyetGiro_id=bilyetgiro1_.id where listpembay0_.KewajibanPembayaran_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from kewajibanpembayaran_items listpembay0_ left outer join BilyetGiro bilyetgiro1_ on listpembay0_.bilyetGiro_id=bilyetgiro1_.id where listpembay0_.KewajibanPembayaran_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from kewajibanpembayaran_items listpembay0_ left outer join BilyetGiro bilyetgiro1_ on listpembay0_.bilyetGiro_id=bilyetgiro1_.id where listpembay0_.KewajibanPembayaran_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

Permasalahan ini sering kali disebut sebagai permasalahan N+1. Nilai 1 adalah query pertama untuk SELECT * FROM x. Setelah itu, untuk N jumlah record yang diperoleh dari query pertama, lakukan query lain untuk membaca nilai di tabel lain seperti SELECT * FROM y WHERE y.x_id = x.id. Dengan demikian, semakin banyak jumlah record yang hendak dibaca, semakin banyak juga query tambahan yang perlu dilakukan. Permasalahan N+1 biasanya adalah pemborosan kinerja yang tak seharusnya terjadi karena ia dapat digantikan dengan join dan/atau subquery.

Sebagai patokan, saya akan menyimpan hasil eksekusi program di atas dan membuat versi grafik-nya yang terlihat seperti pada gambar berikut ini:

Grafik yang menunjukkan kinerja awal

Grafik yang menunjukkan kinerja awal

Pada grafik di atas, pada eksekusi pertama kali, saya akan memperoleh penalti kinerja yang cukup tinggi. Ini adalah karakteristik dari simple-jpa. Selain itu, hal ini juga ditambah lagi dengan server database yang belum memiliki cache hasil query.

Saya akan mulai dengan berusaha menghilangkan query N+1 ke tabel konsumen. FakturJualOlehSales memiliki hubungan @ManyToOne dengan konsumen. Saya menemukan bahwa dengan menyertakan definisi atribut konsumen pada named entity graph, maka query N+1 untuk relasi ke konsumen tidak akan muncul lagi. Perubahan yang saya lakukan menyebabkan definisi named entity graph saya menjadi seperti berikut ini:

@NamedEntityGraph(name='FakturJualOlehSales.Piutang', attributeNodes=[
    @NamedAttributeNode('listItemFaktur'),
    @NamedAttributeNode(value='konsumen', subgraph='konsumen'),
    @NamedAttributeNode('piutang')
], subgraphs = [
    @NamedSubgraph(name='konsumen', attributeNodes=[
        @NamedAttributeNode('region'),
        @NamedAttributeNode('sales')
    ])
])

Sekarang, nilai untuk konsumen tidak akan di-query satu per satu lagi, melainkan diperoleh melalui join pada saat mengambil nilai FakturJualOlehSales seperti yang terlihat pada SQL yang dihasilkan oleh Hiberate:

select distinct ... from FakturJual fakturjual0_ ... left outer join Konsumen konsumen2_ on fakturjual0_.konsumen_id=konsumen2_.id left outer join Region region3_ on konsumen2_.region_id=region3_.id left outer join Sales sales4_ on konsumen2_.sales_id=sales4_.id ...

Sayang sekali saya tidak dapat melakukan hal yang sama untuk Produk karena listItemFaktur adalah sebuah @ElementCollection yang tidak dianggap sebuah entity sehingga tidak dapat diatur melalui named entity graph.

Sampai disini, apakah versi yang memakai left outer join akan lebih cepat dari versi N+1? Saya akan kembali melakukan sedikit percobaan dan menemukan hasil seperti yang terlihat pada gambar berikut ini:

Grafik yang menunjukkan kinerja setelah perubahan.

Grafik yang menunjukkan kinerja setelah perubahan.

Pada grafik di atas, terlihat bahwa perubahan yang saya lakukan memberikan sedikit peningkatan kinerja (sekitar 8%). Hal ini karena pada rancangan saya, tidak banyak yang bisa dioptimalkan dengan cara seperti ini.

Sebagai langkah berikutnya, saya akan menghindari query N+1 untuk retur dengan menjadikannya sebagai 1 query tunggal yang terpisah. Saya dapat menggunakan @Fetch(FetchMode.SUBSELECT) untuk keperluan seperti ini. Sebagai informasi, @Fetch adalah annotation khusus dari Hibernate dan bukan merupakan bagian dari JPA! Sebagai contoh, saya mengubah kode program menjadi seperti berikut ini:

class FakturJualOlehSales extends FakturJual {

   ...

   @OneToMany(...) @JoinTable()
   @Fetch(FetchMode.SUBSELECT)
   List<PenerimaanBarang> retur = []

   ...

}

Konfigurasi di atas akan menyebabkan seluruh query N+1 yang tadinya seperti:

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

...

digantikan oleh sebuah query tunggal yang isinya seperti berikut ini:

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id in (select fakturjual0_.id from FakturJual fakturjual0_ ...)

Saya segera menambahkan @Fetch(FetchMode.SUBSELECT) pada beberapa atribut lainnya yang memiliki permasalahan N+1. Setelah itu, saya mencoba menjalankan program dan kini memperoleh kinerja seperti yang terlihat pada grafis berikut ini:

Grafik yang menunjukkan kinerja setelah perubahan.

Grafik yang menunjukkan kinerja setelah perubahan.

Kali ini saya memperoleh peningkatan kinerja yang cukup drastis karena saya menemukan banyak atribut yang bisa dioptimalkan melalui @Fetch(FetchMode.SUBSELECT). Sebagai hasil akhir, setelah berupaya menghilangkan sebagian besar query N+1, saya memperoleh peningkatan kinerja sebesar 56%. Tidak ada perubahan yang perlu saya lakukan pada kode program yang membaca entitas; ia tetap merupakan sebuah baris yang polos seperti findAllFakturJualFetchPiutang().

Memakai c3p0 Di Hibernate JPA

Sebelum sebuah aplikasi dapat memberikan query SQL, ia harus melakukan koneksi ke server database terlebih dahulu. Setelah query SQL selesai diberikan, aplikasi biasanya menutup koneksi database. Bila hal ini (membuat dan menutup koneksi) dilakukan terus menerus setiap kali melakukan query SQL, maka akan berdampak pada kinerja aplikasi. Hal ini akan semakin terasa bila lokasi client dan server di komputer yang berbeda yang diakses melalui jaringan.

Solusi untuk masalah seperti ini adalah dengan memakai JDBC connection pool. Cara paling naif adalah hanya membuat sebuah koneksi database dan memakai ulang koneksi ini sampai aplikasi ditutup. Akan tetapi solusi ini bisa menimbulkan masalah baru, misalnya bila terjadi gangguan jaringan pada koneksi database, maka koneksi menjadi tidak valid. Koneksi yang tidak valid tersebut tidak dapat lagi dipakai untuk mengerjakan SQL. Hal ini membuat aplikasi harus ditutup agar koneksi baru kembali dibuat.

Oleh sebab itu, daripada membuat sendiri, saya bisa memakai JDBC connection pool open-source yang sudah teruji seperti c3p0 dan Apache DBCP. Connection pool yang baik akan berusaha sebisa mungkin memakai koneksi yang sudah ada dan membuat koneksi hanya baru bila dibutuhkan. Pada tulisan ini, saya akan menggunakan c3p0 pada aplikasi yang memakai Hibernate JPA. Saya dapat menemukan artifak JAR untuk c3p0 di http://mvnrepository.com/artifact/com.mchange/c3p0. Karena saya memakai Griffon, saya bisa menambahkan baris berikut ini pada BuildConfig.groovy:

griffon.project.dependency.resolution = {
    ...
    repositories {
        griffonHome()
        mavenLocal()
        mavenCentral()
    }
    dependencies {
        runtime 'com.mchange:c3p0:0.9.2.1'
        runtime 'org.hibernate:hibernate-c3p0:4.3.6.Final'
        ...
    }
}

Dependency ke com.mchange:c3p0 akan menambah JAR milik c3p0. Sementara itu, org.hibernate:hibernate-c3p0 dibutuhkan untuk memakai c3p0 pada Hibernate JPA.

Langkah terakhir untuk mengaktifkan c3p0 adalah menambah konfigurasi seperti berikut ini pada persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence ...>
  <persistence-unit ...>
    ...
    <properties>
      ...
      <property name="hibernate.connection.provider_class" value="org.hibernate.c3p0.internal.C3P0ConnectionProvider" />
      ...
    </properties>
  </persistence-unit>
</persistence>

Setelah konfigurasi di atas diberikan, Hibernate akan memakai c3p0 connection pool dengan nilai konfigurasi default. Tidak ada kode program yang perlu diberikan untuk memakai c3p0, tapi ada banyak konfigurasi yang bisa diatur. Informasi lebih lanjut mengenai konfigurasi c3p0 dapat dibaca di http://www.mchange.com/projects/c3p0/. Saya dapat memberikan nilai konfigurasi yang ada langsung pada persistence.xml atau membuat sebuah file baru dengan nama c3p0.properties. Sebagai latihan, saya akan membuat file c3p0.properties di folder resources atau root dari folder source yang isinya seperti berikut ini:

c3p0.minPoolSize = 3
c3p0.maxPoolSize = 5
c3p0.initialPoolSize = 3
c3p0.acquireRetryAttempts = 10
c3p0.testConnectionOnCheckout = true

Pada konfigurasi di atas, pada saat aplikasi dijalankan, akan ada 3 koneksi database yang dibuat (nilai dari c3p0.initialPoolSize). Ketiga koneksi ini akan dipakai ulang sebisa mungkin. Koneksi yang dibuat ini tidak harus aktif dan dipakai. Umumnya database akan mempertahankan koneksi yang tidak ditutup oleh aplikasi (baik disengaja atau tidak) atau yang berada dalam keadaan tidur. Sebagai contoh, pada MySQL Server, saya bisa memberikan perintah SQL seperti:

SHOW VARIABLES LIKE 'wait_timeout';

untuk melihat seberapa lama server MySQL akan menunggu aktifitas jaringan sebelum menutup sebuah koneksi dari client di komputer berbeda. Secara default, nilai ini adalah 28800 detik atau 8 jam.

Bila terjadi gangguan jaringan atau masalah lain yang menyebabkan aplikasi tidak dapat terhubung ke server database, maka tanpa connection pool, aplikasi akan mendapatkan kesalahan (exception). Akan tetapi, bila memakai c3p0, aplikasi akan berusaha mencoba mendapatkan koneksi baru ke server database selama berkali-kali. Saya dapat mengatur berapa kali upaya mencoba mendapatkan koneksi dengan mengisi nilai c3p0.acquireRetryAttempts. Sebagai contoh, pada konfigurasi saya, hanya setelah 10 kali upaya mendapatkan koneksi gagal baru aplikasi memperoleh kesalahan JDBC (exception).

Sebuah koneksi dalam pool bisa saja menjadi rusak atau stale. Sebagai contoh, bila server database di-restart, maka seluruh koneksi sebenarnya telah ditutup. Akan tetapi, pada sisi client, koneksi masih tertampung di pool. Seluruh koneksi yang ada di pool kini sudah tidak valid lagi dan c3p0 harusnya membuat koneksi baru. Saya bisa mengatur agar c3p0 mendeteksi koneksi yang tidak valid pada saat koneksi tersebut akan dipakai. Caranya adalah dengan memberi nilai true pada c3p0.testConnectionOnCheckout. Alternatif lainnya adalah membuat c3p0 secara periodik memeriksa koneksi yang tidak valid di pool dengan memberi nilai true pada c3p0.testConnectionOnCheckin dan nilai periode pemeriksaan dalam detik pada c3p0.idleConnectionTestPeriod.

JDBC 4 mendukung isValid() pada Connection untuk menentukan apakah koneksi tersebut masih valid atau tidak. Bila driver JDBC yang dipakai mendukung JDBC 4 (misalnya, MySQL Connector/J di versi 5.1 ke atas), maka c3p0 akan memakai isValid(). Cara ini adalah yang paling cepat. Pada driver JDBC lama, sebuah SQL dapat diberikan sebagai nilai untuk c3p0.preferredTestQuery. SQL ini akan dipakai untuk menguji apakah koneksi masih aktif atau tidak. Bila query SQL tersebut gagal, maka koneksi dianggap tidak aktif.

Fasilitas menarik lainnya dari c3p0 adalah ia mendukung JMX sehingga saya bisa menggunakan JConsole untuk melihat dan melakukan perubahan konfigurasi pada sebuah aplikasi yang sedang dijalankan tanpa harus menutup dan men-compile ulang kode program untuk aplikasi tersebut. Program JConsole dapat dijumpai di lokasi %JDK_HOME%/bin/jconsole.exe. Setelah dijalankan, saya memilih virtual machine untuk aplikasi. Setelah itu, saya dapat melihat statistik dan melakukan perubahan konfigurasi c3p0 seperti yang terlihat pada gambar berikut ini:

Melakukan Administrasi c3p0 Melalui JMX

Melakukan Administrasi c3p0 Melalui JMX

Beberapa operasi membutuhkan username dan password yang isinya harus sesuai dengan yang diberikan pada saat membuat koneksi JDBC.

Implementing Aggregate Root In JPA: @OneToMany or @ElementCollection?

In domain driven design, an aggregate root contains one or more entities that represent a bounded context. Those entities should be only manipulated from their aggregate root. In UML class diagram, this is represented as composition (a filled diamond in relationship). Note that UML class diagram also has a concept of aggregation (a hollow diamond in a relationship). Despite similarity in the name, the contained part in UML aggregation can exists without its container. Thus, it is not something like DDD’s aggregate root in which the contained part should not exist without their container.

For example, the following is an UML class diagram with composition:

Composition in UML Class Diagram

Composition in UML Class Diagram

Invoice is the root aggregate that manages LineItem. Every LineItem is a value object. No one should be able to add or delete LineItem directly without obtaining an Invoice first. Because instances of LineItem are value objects, they don’t have a global identity. In the other side, instances of Invoice class are entities so they can be searched by a global identity (for example: invoice number). Each LineItem is associated with a Product entity. This is valid in domain driven design though some people will recommend using value object instead. The value object will store the Product identity (for example: product number). See this article for more information: http://dddcommunity.org/wp-content/uploads/files/pdf_articles/Vernon_2011_2.pdf.

The question is how to implement the classes in our diagram using JPA? Well, there are several possibilities with surprising caveats. The recommended way for @OneToMany relationship is bidirectional with owner on the many side. But this violates our aggregate root rules. No one should select existing LineItem or add new LineItem directly! They must manipulate LineItem from Invoice only. This can be solved by using unidirectional @OneToMany with @JoinColumn. But it is still not a containment. To implement the real containment, use @ElementCollection and mark all value objects as @Embeddable. Note that @ElementCollection is introduced in JPA 2.

For example, this is an implementation using @OneToMany and @JoinColumn in Groovy + Hibernate +simple-jpa:

import ...

@DomainClass @Entity @Canonical
class Invoice {

    @NotEmpty
    String number

    @OneToMany @JoinColumn
    List<LineItem> lineItems = []

    public void add(LineItem lineItem) {
        lineItems << lineItem
    }

}


@DomainClass @Entity @Canonical
class LineItem {

    @NotNull @ManyToOne
    Product product

    @NotNull
    Integer quantity

}


@DomainClass @Entity @Canonical
class Product {

    @NotEmpty
    String name

}

The code above will produce the following database tables:

Tables for @OneToMany with @JoinColumn

Tables for @OneToMany with @JoinColumn

Table for LineItem has an identity. This primary key is required for one to many relationships. In our case, it is pretty useless because LineItem should only be identified with their Invoice. The identity of LineItem has no meaning in global context.

This code will create several objects based on our domain classes:

def productA = new Product('Product A')
persist(productA)
def productB = new Product('Product B')
persist(productB)

def invoice = new Invoice('Invoice-01')
invoice.add(new LineItem(productA, 10))
invoice.add(new LineItem(productB, 20))
persist(invoice)

But if you execute the code above, you will get org.hibernate.TransientObjectException!! Every single LineItem must be persisted before persisting Invoice. This is a bit annoying. It doesn’t show that our Invoice is the boss – the aggregate root. To solve this problem, you will need to add @OneToMany(cascade=CascadeType.ALL) to Invoice.lineItems:

...
@OneToMany(cascade=CascadeType.ALL) @JoinColumn
List<LineItem> lineItems = []
...

Now if you run the code, Hibernate will actually perform the following SQL queries:

insert into Product (createdDate, deleted, modifiedDate, name, id) values (?, ?, ?, ?, ?)
insert into Product (createdDate, deleted, modifiedDate, name, id) values (?, ?, ?, ?, ?)
insert into Invoice (createdDate, deleted, modifiedDate, number, id) values (?, ?, ?, ?, ?)
insert into LineItem (createdDate, deleted, modifiedDate, product_id, quantity, id) values (?, ?, ?, ?, ?, ?)
insert into LineItem (createdDate, deleted, modifiedDate, product_id, quantity, id) values (?, ?, ?, ?, ?, ?)
update LineItem set lineItems_id=? where id=?
update LineItem set lineItems_id=? where id=?

Note that Hibernate will issue both insert and update query for every LineItem. So, if I insert 10 LineItem objects, Hibernate will issue 20 queries: 10 insert queries and another 10 update queries. Isn’t this a bit overwhelming?

What happened if I update existing LineItem or insert new LineItem such as shown in the following code:

def invoice = findInvoiceByNumberFetchComplete('Invoice-01')
invoice.lineItems[0].product = findProductByName('Product B')
invoice.lineItems[0].quantity = 999
invoice.lineItems.remove(1)
merge(invoice)

If I run the code, Hibernate will execute the following queries:

...
update LineItem set createdDate=?, deleted=?, modifiedDate=?, product_id=?, quantity=? where id=?
update LineItem set lineItems_id=null where lineItems_id=? and id=?
...

Hibernate only issue two update queries! Even when we remove the second LineItem in our Invoice in the source code, Hibernate doesn’t actually remove it from database. Hibernate only set LineItem.lineItems_id to null so in the next select query, we will not see that second item. To force Hibernate to delete the second item, add orphanRemoval=true to @OneToMany as shown in the following code:

...
@OneToMany(cascade=CascadeType.ALL, orphanRemoval=true) @JoinColumn
List<LineItem> lineItems = []
...

The updated mapping will generate the following queries:

update LineItem set createdDate=?, deleted=?, modifiedDate=?, product_id=?, quantity=? where id=?
update LineItem set lineItems_id=null where lineItems_id=? and id=?
delete from LineItem where id=?

While it is possible to implement aggregate roots and their managed objects using @OneToMany, developers can still manipulate objects directly without their aggregate roots. Our LineItem is required to have an identity in the mapping but we know that value objects shouldn’t have a global identity. Now imagine if you have several genius kids in your team who don’t like to follow your domain driven design rules! They code in whatever direction they want because they think they can!! When the system grows larger, some of the genius kids resigned and new kids join your team. They even do a big refactoring. At the end, you may have a big ball of mud. See http://laputan.org/mud/ for more information about this anti pattern.

To create a more restricted implementation, you can use @ElementCollection. One of the possible implementation using @ElementCollection will be:

@DomainClass @Entity @Canonical
class Invoice {

    @NotEmpty
    String number

    @ElementCollection
    List<LineItem> lineItems = []

    public void add(LineItem lineItem) {
        lineItems << lineItem
    }

}

@Embeddable @Canonical
class LineItem {

    @NotNull @ManyToOne
    Product product

    @NotNull
    Integer quantity

}

@DomainClass @Entity @Canonical
class Product {

    @NotEmpty
    String name

}

Note that LineItem is annotated with @Embeddable not @Entity. In JPA, @Embeddable is used for value object. @Embeddable class doesn’t have an identity just like what a value object should be. But this imposes a limitation: an embeddable class can have collections, but if it is embedded in another embeddab class, it can’t have collections. This is an important limitation if you’re going to implement all managed classes as @Embeddable!

The new domain classes will produce the following tables:

Tables for @ElementCollection and @Embeddable

Tables for @ElementCollection and @Embeddable

To insert new objects, I use the same code as in @OneToMany mapping. While it is the same code, Hibernate now generates different queries:

insert into Product (createdDate, deleted, modifiedDate, name, id) values (?, ?, ?, ?, ?)
insert into Product (createdDate, deleted, modifiedDate, name, id) values (?, ?, ?, ?, ?)
insert into Invoice (createdDate, deleted, modifiedDate, number, id) values (?, ?, ?, ?, ?)
insert into Invoice_lineItems (Invoice_id, product_id, quantity) values (?, ?, ?)
insert into Invoice_lineItems (Invoice_id, product_id, quantity) values (?, ?, ?)

Note that there are no more annoying updates like in @OneToMany mapping. How about update and delete? The same code now will generate the following queries:

delete from Invoice_lineItems where Invoice_id=?
insert into Invoice_lineItems (Invoice_id, product_id, quantity) values (?, ?, ?)

Wait, this is a big difference!! Hibernate will always delete all LineItem records from our Invoice before re-inserting old, updated and new records. Hibernate must do this because our LineItem doesn’t have an identity. Hibernate doesn’t know which records to update or delete if there is no identity on the records.
This behavior is acceptable in small collection. But this is not efficient for a large collection because Hibernate will re-create all records even when only one LineItem is changed. To avoid such case, you can use @OrderColumn in the List, such as:

...
@ElementCollection @OrderColumn
List<LineItem> lineItems = []
...

This new mapping will add a new field to Lineitem table that stores index number (remember that List is a number indexed collection).

Tables for @ElementCollection with @OrderColumn

Tables for @ElementCollection with @OrderColumn

With this mapping, the update and delete code will execute the following queries:

delete from Invoice_lineItems where Invoice_id=? and lineItems_ORDER=?
update Invoice_lineItems set product_id=?, quantity=? where Invoice_id=? and lineItems_ORDER=?

Now, Hibernate will not delete all line item but only delete the second line item (because it was deleted in the following code: invoice.lineItems.remove(1)).

Memakai Event Griffon Di simple-jpa

Salah satu penambahan yang saya lakukan di simple-jpa 0.5 adalah dukungan event Griffon.   Plugin simple-jpa kini akan menghasilkan event Griffon yang berupa:

  • simpleJpaCreateEntityManager – Event ini akan dihasilkan setiap kali simple-jpa membuat Entitymanager baru.   Event ini akan melewatkan sebuah object TransactionHolder.
  • simpleJpaDestroyEntityManagers – Event ini akan dihasilkan saat simple-jpa menutup seluruh EntityManager yang ada.
  • simpleJpaBeforeCloseEntityManager – Event ini akan dihasilkan pada saat simple-jpa menutup sebuah EntityManager.   Event ini akan melewatkan sebuah object TransactionHolder.
  • simpleJpaNewTransaction – Event ini akan dihasilkan setiap kali simple-jpa membuat transaksi baru.   Event ini akan melewatkan sebuah object TransactionHolder.
  • simpleJpaCommitTransaction – Event ini akan dihasilkan setiap kali sebuah transaction di-commit.   Event ini akan melewatkan sebuah object TransactionHolder.
  • simpleJpaRollbackTransaction – Event ini akan dihasilkan setiap kali sebuah transaction di-rollback.   Event ini akan melewatkan sebuah object TransactionHolder.
  • simpleJpaBeforeAutoCreateTransaction – Event ini akan dihasilkan bila simple-jpa secara otomatis membuat transaction baru bila sebuah operasi simple-jpa dikerjakan diluar transaksi.   Plugin simple-jpa akan selalu mengerjakan operasi JPA dalam sebuah transaksi; bila tidak ada transaksi yang aktif, maka ia akan membuat transaksi baru khusus untuk mengerjakan operasi tersebut.

Bagaimana cara memakai event tersebut? Pada file griffon-app/conf/Events.groovy, tambahkan kode program seperti berikut ini:

onSimpleJpaCreateEntityManager =  { TransactionHolder th ->
  // kode program yang akan dikerjakan saat sebuah EntityManager baru dibuat
  ...
}

onSimpleJpaNewTransaction = { TransactionHolder th ->
  // kode program yang akan dikerjakan saat transaksi baru dibuat
  ...
}

Salah satu contoh kegunaan dari event yang dihasilkan simple-jpa adalah untuk memberikan penanda bahwa aplikasi sedang sibuk.   Saya menemukan bahwa terkadang sebuah transaksi JPA bisa berlangsung lebih dari beberapa detik terutama bila transaksi tersebut melibatkan banyak object (misalnya dalam membuat laporan).   Agar pengguna tidak bingung dengan respon yang lama, akan lebih baik bila aplikasi menampilkan indikator bahwa sebuah transaksi sedang berlangsung.  Untuk menampilkan indikator sibuk, saya akan menggunakan plugin jxlayer yang akan memanfaatkan JLayer di Java 7.   Saya dapat men-install plugin tersebut dengan memberikan perintah:

griffon install-plugin jxlayer

JLayer bekerja dengan memanfaatkan decorator design pattern.   Saya tidak perlu menambahkan indikator sibuk tersebut pada view yang sebelumnya sudah dibuat.   Saya hanya perlu membuat sebuah dekorator yang nantinya akan diterapkan pada satu atau lebih view.   Sebagai contoh, saya akan membuat class Java (jangan lupa bahwa Groovy tetap bisa memakai class Java) bernama BusyLayerUI yang isinya seperti berikut ini:

import griffon.core.UIThreadManager;
import javax.swing.*;
import javax.swing.plaf.LayerUI;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.geom.Arc2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;

class BusyLayerUI extends LayerUI<JPanel> implements ActionListener {

    public static final BusyLayerUI instance = new BusyLayerUI();
    public static BusyLayerUI getInstance() {
        return instance;
    }

    private final int RADIUS = 150;
    private final Color WARNA_LATAR = new Color(219, 247, 186);
    private final Color WARNA_PROGRESS = new Color(118, 186, 39);
    private final Color WARNA_BAYANGAN = new Color(150, 240, 82, 157);

    private boolean visible = false;
    private BufferedImage pixelTexture;
    private Rectangle2D ukuranTexture;
    private Arc2D.Double fullCircle, progress;
    private Timer timer;

    private BusyLayerUI() {
        pixelTexture = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB);
        ukuranTexture = new Rectangle2D.Double(0, 0, 2, 2);
        Graphics2D g2 = pixelTexture.createGraphics();
        g2.setColor(Color.BLACK);
        g2.fillRect(0, 0, 1, 2);
        g2.fill(ukuranTexture);
        g2.setColor(Color.GRAY);
        g2.fillRect(1, 0, 1, 2);
        g2.dispose();
        pixelTexture.flush();

        progress = new Arc2D.Double(50, 50, 400, 400, 0, 0, Arc2D.OPEN);
        fullCircle = new Arc2D.Double(50, 50, 400, 400, 0, -360, Arc2D.OPEN);

    }

    void show() {
        if (visible) return;
        UIThreadManager.getInstance().executeSync(new Runnable() {
            public void run() {
                progress.setAngleExtent(0);
                timer = new Timer(1000/24, BusyLayerUI.this);
                timer.start();
                visible = true;
                firePropertyChange("visible", false, true);
            }
        });
    }

    void hide() {
        if (!visible) return;
        UIThreadManager.getInstance().executeSync(new Runnable() {
            public void run() {
                visible = false;
                firePropertyChange("visible", true, false);
            }
        });
    }

    @Override
    public void paint(Graphics g, JComponent c) {
        super.paint(g, c);
        if (!visible) return;

        int w = c.getWidth();
        int h = c.getHeight();

        Graphics2D g2 = (Graphics2D) g.create();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        Composite currentComposite = g2.getComposite();

        // Buat layar terlihat seperti tidak aktif (lebih gelap dan kabur)
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
        g2.setPaint(new TexturePaint(pixelTexture, ukuranTexture));
        g2.fillRect(0, 0, w, h);

        double centerX = (double) (w/2);
        double centerY = (double) (h/2);

        // Buat lingkaran terang
        g2.setStroke(new BasicStroke(20, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g2.setColor(WARNA_LATAR);
        fullCircle.setFrameFromCenter(centerX, centerY, centerX+RADIUS, centerY+RADIUS);
        g2.draw(fullCircle);

        // Buat progress yang menandakan program sedang sibuk
        g2.setColor(WARNA_PROGRESS);
        progress.setFrameFromCenter(centerX, centerY, centerX+RADIUS, centerY+RADIUS);
        g2.draw(progress);
        g2.setColor(WARNA_BAYANGAN);
        g2.setStroke(new BasicStroke(30, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g2.draw(progress);

        // Selesai
        g2.setComposite(currentComposite);
        g2.dispose();
    }

    @Override
    public void applyPropertyChange(PropertyChangeEvent evt, JLayer<? extends JPanel> l) {
        if ("tick".equals(evt.getPropertyName()) || "visible".equals(evt.getPropertyName())) {
            l.repaint();
        }
    }

    @Override
    public void installUI(JComponent c) {
        super.installUI(c);
        ((JLayer)c).setLayerEventMask(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.KEY_EVENT_MASK);
    }

    @Override
    public void uninstallUI(JComponent c) {
        ((JLayer)c).setLayerEventMask(0);
        super.uninstallUI(c);
    }

    @Override
    public void eventDispatched(AWTEvent e, JLayer<? extends JPanel> l) {
        if (visible && e instanceof InputEvent) {
            ((InputEvent)e).consume();
        }
    }

    public void actionPerformed(ActionEvent e) {
        if (visible) {
            double extend = progress.getAngleExtent();
            if (extend <= -360) {
                extend = 0;
            } else {
                extend -= 3;
            }
            progress.setAngleExtent(extend);
            firePropertyChange("tick", 0, 1);
        } else {
            timer.stop();
        }
    }
}

Bila method show() dipanggil, maka sebuah animasi lingkaran akan ditampilkan dan seluruh komponen di layar tersebut menjadi tidak dapat di-klik atau di-isi.   Pengguna hanya boleh pasrah menunggu :)

Sekarang saya perlu menentukan view apa saja yang perlu memiliki ‘informasi sibuk’ ini.   Sebagai contoh, saya dapat mendekorasi view utama dengan mengubah file MainGroupView.groovy menjadi seperti berikut ini:

application(...){

  ...
  borderLayout()
  jxlayer(UI: BusyLayerUI.getInstance(), constraints: BorderLayout.CENTER) {

    panel() {
      borderLayout()

       toolBar(id: 'toolBar', constraints: BorderLayout.PAGE_START, floatable: false) {
         ...
       }

       panel(id: "mainPanel") {
         cardLayout(id: "cardLayout")
       }

       statusBar(constraints: BorderLayout.PAGE_END, border: BorderFactory.createBevelBorder(BevelBorder.LOWERED)) {
         ...
       }
    }
  }
}

Sekarang pertanyaannya adalah bagaimana mengetahui sebuah transaksi sedang berlangsung sehingga method show() akan dipanggil?    Apakah saya harus mengubah kode program di controller dimana saya selalu memanggil show() sebelum memulai transaksi dan memanggil hide() setelah transaksi selesai dikerjakan?   Bila demikian, banyak sekali perubahannya karena hampir seluruh method harus diubah!   Tapi karena simple-jpa 0.5 sudah mempublikasi event yang berkaitan dengan transaksi, maka saya bisa dengan mudah mengetahui kapan sebuah transaksi dimulai dan diakhiri (berlaku secara global).   Dengan demikian, saya hanya perlu menambahkan kode program berikut ini pada griffon-app/conf/Events.groovy:

import simplejpa.transaction.TransactionHolder
import util.BusyLayerUI

onUncaughtExceptionThrown = { Exception e ->
    if (e instanceof org.codehaus.groovy.runtime.InvokerInvocationException) e = e.cause.cause
    javax.swing.JOptionPane.showMessageDialog(null, e.message, "Error", javax.swing.JOptionPane.ERROR_MESSAGE)
    BusyLayerUI.getInstance().hide()
}

onSimpleJpaNewTransaction = { TransactionHolder th ->
    BusyLayerUI.getInstance().show()
}

onSimpleJpaCommitTransaction = { TransactionHolder th ->
    BusyLayerUI.getInstance().hide()
}

onSimpleJpaRollbackTransaction = { TransactionHolder th ->
    BusyLayerUI.getInstance().hide()
}

Sekarang, setiap kali sebuah transaksi sedang berlangsung, layar utama akan menampilkan indikator sibuk seperti yang terlihat pada gambar berikut ini:

Indikator sibuk setiap kali transaksi berlangsung

Indikator sibuk setiap kali transaksi berlangsung

Sebagai informasi tambahan, JPA sendiri sudah memiliki mekanisme event yang disebut sebagai entity listener.   Hasil scaffolding dari simple-jpa sudah mendaftarkan sebuah entity listener bernama simplejpa.AuditingEntityListener seperti yang terlihat pada gambar berikut ini:

Isi file orm.xml

Isi file orm.xml

Pengguna dapat menambahkan entity listener baru pada file orm.xml tersebut.   Method pada entity listener dapat memiliki annotation yang mewakili lifecycle event JPA, yaitu @PrePersist, @PostPersist, @PreUpdate, @PostUpdate, @PreRemove, @PostRemove dan @PostLoad.   Berbeda dengan event dari simple-jpa yang berkaitan dengan transaksi, event disini lebih berkaitan dengan siklus hidup masing-masing object yang dikelola oleh JPA.

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.

Membuat Aplikasi Database Tanpa Coding Dengan NetBeans

P.S.: Pada artikel ini, saya memakai database Oracle TimesTen, akan tetapi langkah yang sama juga dapat diterapkan pada database lainnya asalkan memilih driver JDBC yang tepat untuk database tersebut.

Saya akan mencoba membuat sebuah program Java sederhana yang mengakses dan melakukan query pada database TimesTen.   Saya akan memakai Java 7 di NetBeans 7.3.   Karena NetBeans menyediakan fitur GUI editor yang lumayan canggih, pada artikel ini saya akan menempuh cara ang drag-n-drop (di balik layar NetBeans tetap memakai JPA!).   Saya tidak perlu mengetik kode program, bahkan satu baris kode program pun tidak perlu.   Seorang teman saya sangat tergila-gila pada fasilitas GUI yang drag-n-drop di NetBeans, membuatnya enggan beralih ke IDE lain.   Saya secara pribadi tidak akan pernah 100% bergantung pada visual editor, karena biasanya akselerasi hanya terasa di awalnya tapi sering kali ribet di kemudian hari ;)   Tapi mungkin ini hanya masalah selera; produktifitas seorang developer akan tinggi bila ‘selera’-nya terpenuhi.

Saya akan mulai dengan membuat sebuah proyek baru.   Untuk itu, saya memilih menu File, New Project….   Pada dialog yang muncul, saya memilih Java, Java Application kemudian men-klik tombol Next.   Saya mengisi nama proyek dengan LatihanTimesTen, kemudian menghilangkan tanda centang pada Create Main Class.   Setelah itu saya menyelesaikan pembuatan proyek baru dengan men-klik tombol Finish.

Sama seperti database lainnya yang berbasis SQL, untuk mengakses TimesTen di Java harus melalui JDBC API.   Btw, JDBC adalah sebuah ukan merupakan sebuah singkatan.   Walaupun demikian, nama JDBC sepertinya agak mirip dengan ODBC (Open Database Connectivity) buatan Microsoft, bahkan sekilas terlihat ada persamaan diantara mereka.   Untuk memakai ODBC, dibutuhkan driver yang disediakan oleh pembuat database.   Begitu juga, untuk memakai JDBC dibutuhkan driver JDBC dalam bentuk file JAR yang disediakan oleh pembuat database.   Pada instalasi saya, lokasi driver ini terletak di C:\TimesTen\tt1122_32\lib.   Untuk Java 7, saya akan memakai file bernama ttjdbc7.jar.   Pada dasarnya file ttjdbc7.jar dan ttjdbc6.jar adalah file yang sama (duplikat)!

Saya akan memakai fasilitas GUI dari NetBeans untuk menjelajahi database.   Tapi sebelumnya saya memastikan terlebih dahulu bahwa TimesTen Data Manager sudah dijalankan.   Untuk itu, saya memberikan perintah ttDaemonAdmin -start. Bagi   yang tidak ingin memakai perintah Command Prompt bisa langsung memerika di Control Panel, Administrative Tools, Services.   Setelah itu, saya juga melakukan koneksi pertama kali ke database melalui ttIsql agar database di-load ke memori sehingga dapat dipakai oleh user lainnya.

Lalu, saya memilih menu Window, Services di NetBeans.   Pada Databases, saya men-klik kanan dan memilih New Connection… seperti yang terlihat pada gambar berikut ini:

Mendaftarkan koneksi baru

Mendaftarkan koneksi baru

Setelah itu, pada Driver, saya memilih New Driver….   Akan muncul dialog New JDBC Driver. Saya mengisinya seperti dengan yang terlihat pada gambar berikut ini:

Mendaftarkan driver JDBC baru

Mendaftarkan driver JDBC baru

Pastikan pada Driver Class, yang dipilih adalah com.timesten.jdbc.TimesTenDriver dan bukan com.timesten.jdbc.TimesTenClientDriver.   Hal ini karena saya melakukan direct connection, bukan client server.

Setelah itu, saya men-klik tombol OK.   Kemudian, saya men-klik tombol Next untuk melanjutkan ke tahap berikutnya.

Saya mengisi dialog Customize Connection seperti pada gambar berikut ini:

Menambahkan informasi koneksi

Menambahkan informasi koneksi

Setelah itu, saya men-klik tombol Next.   Saya membiarkan schema SOLID terpilih (sesuai dengan nama user yang saya berikan) dan langsung men-klik tombol Next.   Saya kemudian mengisi Input connection name dengan TimesTen Database LATIHAN (boleh di-isi dengan nama apa saja), kemudian men-klik tombol Finish.

NetBeans tidak dapat menampilkan daftar tabel untuk database TimesTen, tetapi ia tetap dapat membantu saya dalam mengerjakan perintah SQL.   Saya men-klik kanan nama koneksi TimesTen Database LATIHAN, kemudian memilih menu Execute Command….   Akan muncul sebuah editor baru dimana saya bisa mengerjakan perintah SQL untuk koneksi tersebut, seperti yang terlihat pada gambar berikut ini:

Mengerjakan SQL dari NetBeans

Mengerjakan SQL dari NetBeans

Ok, sekarang saya mendefinisikan sebuah koneksi database.   Saya dapat mulai membuat kode program tanpa perlu ‘mengetik‘ (baca: sihir).   Pada window Projects, saya men-klik kanan Source Packages, kemudian memilih New, JFrame Form… seperti yang diperlihatkan oleh gambar berikut ini:

Membuat JFrame baru

Membuat JFrame baru

Bila menu ini tidak terlihat, saya dapat mencarinya dengan memilih Other….

Pada dialog yang muncul, saya mengisi Class Name dengan ProdukView dan mengisi package dengan com.wordpress.thesolidsnake.view.   Setelah itu, saya men-klik tombol Finish.

Pada visual editor yang muncul, saya men-drag komponen Table ke layar utama seperti yang terlihat pada gambar berikut ini:

Menambahkan komponen tabel

Menambahkan komponen tabel

Kemudian, saya men-klik kanan pada komponen yang baru saja saya tambahkan dan memilih menu paling awal yaitu Table Contents….   Pada kotak dialog Customizer Dialog yang muncul, saya memilih Bound, kemudian men-klik tombol Import Data to Form… seperti yang diperlihatkan oleh gambar berikut ini:

Melakukan binding data dari database

Melakukan binding data dari database

Pada dialog Import Data To Form, saya memilih koneksi database dan tabel seperti yang terlihat pada gambar berikut ini (saya mengandaikan bahwa tabel PRODUK sudah dibuat sebelumnya):

Mengambil informasi dari database

Mengambil informasi dari database

Setelah itu, saya men-klik tombol OK.   NetBeans akan memunculkan pesan menunggu untuk proses importing….   Setelah selesai, Binding Source secara otomatis akan terisi dengan produkList.

Saya akan berpindah ke tab Columns untuk mendefinisikan kolom yang akan ditampilkan.   Saya menambahkan dua buah kolom untuk tabel tersebut, sesuai dengan kolom yang ada di database, seperti yang terlihat pada gambar berikut ini:

Menambahkan kolom untuk tabel

Menambahkan kolom untuk tabel

Setelah itu, saya men-klik tombol Close untuk menutup dialog.

Sekarang, saya akan menjalankan aplikasi dengan men-klik tombol Run Project (atau menekan F6).   Pada dialog Run Project yang muncul, saya memastikan bahwa com.wordpress.thesolidsnake.view.ProdukView terpilih sebagai main class, lalu men-klik tombol OK.   Tabel akan muncul terisi dengan data dari database, seperti yang diperlihatkan oleh gambar berikut ini:

Tampilan program saat dijalankan

Tampilan program saat dijalankan

Sesuai dengan janji saya, tidak ada satu barispun kode program yang saya ketik disini.   Tapi dibalik layar, NetBeans akan memakai JPA untuk mengakses database!   Yup, JPA seperti pada plugin simple-jpa yang saya buat untuk Griffon, bukan plain SQL di JDBC.   Dengan demikian, NetBeans juga menghasilkan kode program yang membuat dan memakai EntityManager.   Bukan hanya itu, NetBeans juga menghasilkan sebuah domain class (atau tepatnya JPA Entity) dengan nama Produk yang sudah lengkap dengan named query, seperti yang diperlihatkan pada gambar berikut ini:

JPA Entity bernama Produk yang dihasilkan NetBeans

JPA Entity bernama Produk yang dihasilkan NetBeans

Kode program yang dihasilkan oleh NetBeans memakai org.jdesktop.swingbinding.JTableBinding untuk melakukan binding. Sebagai perbandingan, pada simple-jpa, binding dilakukan melalui SwingBuilder (bawaan Groovy) dan GlazedLists.   Untuk menentukan ekspresi setiap kolom, NetBeans memakai Expression Language (seperti pada JSP dan JSF).   Sebagai perbandingan, pada simple-jpa, saya memakai Groovy template engine yang menerima seluruh ekspresi Groovy.

Melihat Kinerja Aplikasi Desktop Berbasis Griffon dan simple-jpa

Sepuluh tahun silam saat bekerja sebagai developer di salah satu software house, seorang technical leader melarang saya untuk memakai Java Reflection dalam menghasilkan data transfer objects (DTO).   Alasannya: reflection lebih ‘berat’ dibanding copy paste!   Karena larangan tersebut, saya dan rekan-rekan menghabiskan waktu hampir seminggu lebih untuk modul tersebut.   Delivery proyek menjadi terlambat.   Iya, pada zaman tersebut, memakai reflection memang lebih ‘berat’ , tapi pertanyaan penting yang sering terlupakan adalah: seberapa ‘berat‘ atau seberapa lambat??   Apakah penalti kinerja ini cukup berharga untuk ditukarkan dengan berkurangnya ketepatan waktu dalam delivery proyek?  Apakah penalti kinerja ini memiliki poin lebih penting sehingga tidak apa-apa bila aplikasi menjadi rumit dan sulit di-maintain? Mempertimbangkan kinerja di atas segala-galanya adalah salah satu kecendurangan negatif dalam dunia software engineering yang disebut sebagai premature optimization.

Griffon adalah sebuah framework untuk aplikasi desktop yang berdasarkan pada bahasa Groovy.   Groovy sendiri adalah sebuah bahasa pemograman dinamis yang lebih lambat dibandingkan bahasa statis seperti Java.   Tapi pertanyaan adalah: seberapa lambat sebuah aplikasi desktop yang dikembangkan dengan Groovy? Apakah masih bisa diterima dalam batas kewajaran?

Untuk menjawab pertanyaan tersebut, saya akan melakukan pengujian dengan menggunakan sebuah aplikasi desktop yang dibuat dengan framework Griffon dan plugin simple-jpa.   Aplikasi ini akan membaca data invoice (faktur) dari sebuah file teks kemudian memproses setiap baris data tersebut: menyimpan invoice, menambah entri pada inventory, serta menambah entri pada pembayaran bila diperlukan.   Data lainnya yang dibutuhkan oleh invoice sudah tersimpan di tabel dengan jumlah yang mendekati pemakaian sehari-hari. Sebagai contoh, di tabel produk terdapat 7.360 record.  Sebuah jumlah yang tidak terlalu kecil dan tidak terlalu besar.

Jumlah baris yang harus diproses oleh aplikasi ini adalah 91.190 baris yang mewakili data invoice dan item-nya.   Aplikasi ini akan memakai Griffon 1.2.0, simple-jpa 0.4.2, Hibernate JPA 4.2.0.Final dan MySQL Server 5.6.11.   Selama pengujian, database hanya akan dipakai oleh program tersebut saja.   Pengujian dipakai untuk mensimulasikan aktifitas single user.  Btw, hasil percobaan ini hanya sebagai gambaran umum yang terbatas pada aplikasi yang saya uji tersebut.   Untuk hasil yang lebih akurat tentunya dibutuhkan aplikasi dan environment yang lebih terkendali dan fair.

Percobaan 1

Saya akan mulai dengan menjalankan aplikasi dalam modus hanya membaca dari database.   Pengguna biasanya akan lebih sering melihat data inventory atau mencari data pembayaran ketimbang membuat data invoice baru.   Hasil percobaan menunjukkan bahwa 91.190 baris data berhasil diproses dalam waktu 49 menit.   Ini sudah meliputi membaca tabel produk, membaca data suplier/konsumen, mengerjakan business logic, tapi hasil prosesnya tidak disimpan (tidak ada query INSERT).

Aplikasi percobaan berbasis Griffon dan simple-jpa ini dapat memproses hingga 31 transaksi dalam waktu 1 detik.   Saya akan menyebutnya sebagai 31 TPS.   Perhitungannya adalah 91.190 baris dibagi dengan 49 x 60 = 2940 detik.   Transaksi disini bukanlah transaksi dalam arti operasi database, melainkan satu kesatuan operasi membuat invoice baru serta implikasinya (pada pembayaran dan inventory).  Termasuk juga didalamnya operasi mencari suplier, konsumen atau produk berdasarkan kode.   Dengan demikian, sebuah transaksi akan terdiri atas lebih dari satu operasi SQL.

Percobaan 2

Walaupun saya yakin pengguna tidak akan men-klik setiap menu sebanyak 31 kali per detik, tapi angka tersebut cukup kecil.   Saya pun menyelidiki kenapa aplikasi Griffon ini begitu lambat.  Pada akhirnya saya menemukan jawabannya:

  1. Kode program memproses setiap baris data dalam looping.  Keseluruh looping ini akan dianggap sebagai satu transaction (disini saya merujuk pada database transaction).
  2. EntityManager selaku first level cache akan menampung data hasil query SELECT.   Beberapa query men-trigger terjadinya flushing.
  3. Semakin banyak data yang ada di first level cache, maka semakin lama proses validasi yang timbul akibat flushing yang di-trigger secara otomatis.

Untuk mengatasinya, saya mengubah struktur program dari:

...
beginTransaction()
baris.each {
   ... // baca data
   Product product = findProductByCode(productCode)
   Invoice invoice = new Invoice()
   invoice.tambahItem(...)
   if (bayar) {
      invoice.bayar(...)
   }
}
commitTransaction()
...

menjadi:

...
def total = 0
beginTransaction()
baris.each {
   ... // baca data
   Product product = findProductByCode(productCode)
   Invoice invoice = new Invoice()
   invoice.tambahItem(...)
   if (bayar) {
      invoice.bayar(...)
   }
   total++
   if (total%20000==0) clear() // memanggil EntityManager.clear()
}
commitTransaction()
...

Setelah perubahan, saya menemukan bahwa aplikasi tersebut dapat menyelesaikan tugasnya dalam waktu 6 menit.   Hal ini berarti meningkat dari 31 TPS menjadi 253 TPS.   Ini adalah peningkatan yang cukup drastis.

Kesimpulannya:  kode program business logic dengan Groovy terlihat memiliki kinerja yang wajar.   Bahasa dinamis memang lebih lambat dari bahasa statis, tapi seberapa besar dampaknya?   Kode program aplikasi ini juga memakai dynamic finders di simple-jpa untuk mencari entity berdasarkan kodenya.   Penggunaan method dinamis sering kali dianggap lebih lambat dibanding memanggil method secara biasa. Tapi seberapa besar selisih lambatnya? Hari ini saya menemukan jawabannya: layak dipakai :)

Percobaan 3

Perjalanan belum selesai!   Kali ini saya akan mengubah kode program sehingga tidak hanya membaca dan membuat object baru di memori, tapi juga menuliskan hasil proses ke database.   Kali ini, saya perlu men-flush() EntityManager setiap 1000 kali baris diproses, sebelum men-clear() EntityManager tersebut.   Perintah flush() akan memaksa Hibernate untuk menuliskan perubahan tersebut ke database dengan melakukan eksekusi SQL.   Walaupun demikian, perubahan yang permanen tetap akan terjadi hanya setelah perintah commit() diberikan.   Jadi, bila saya membatalkan aplikasi di tengah perjalanan, maka tidak akan ada tabel yang isinya berubah.

Lalu, berapa banyak waktu yang dibutuhkan?  Hasil pengujian menunjukkan bahwa untuk memproses 91.190 baris dibutuhkan waktu selama 38 menit.   Dengan demikian kecepatannya adalah 40 TPS.   Hasilnya adalah 91.190 record di tabel Invoice, 17.484 record di tabel pembayaran, dan 129.049 record di tabel inventory.   Nilai 40 TPS adalah hal wajar dan bisa diterima, tapi masalahnya adalah data yang dihasilkan tidak akurat!!   Banyak baris duplikat ditemukan di tabel inventory.   Mengapa demikian?

Percobaan 4

Hal ini berkaitan dengan penggunaan @Canonical di Groovy.   Keinginan saya untuk mendapatkan ‘kenyamanan’ serba otomatis akhirnya membuat saya memperoleh ‘hukuman’.   simple-jpa memberikan @Canonical pada seluruh domain classes yang ada. Annotation ini akan membuat Groovy menciptakan constructor, equals(), hashCode() dan toString() secara otomatis.   Hibernate (lebih tepatnya Java) akan memakai hashCode() dan equals() untuk membandingkan apakah dua buah entity adalah entity yang sama atau berbeda.   Masalahnya: hashCode() dan equals() yang dihasilkan oleh @Canonical sepertinya tidak memperhatikan atribut pada superclass.   Padahal untuk sebuah entity yang diturunakn dari entity lainnya,  biasanya id atau natural primary key yang bisa dipakai untuk membandingkan kesamaan akan terletak di superclass.   Solusinya, saya perlu membuat isi equals() dan hashCode() secara manual pada domain class yang bermasalah.

Setelah dijalankan ulang, tidak ada lagi record yang duplikat di tabel inventory.   Saya juga sempat memanfaatkan kesempatan untuk meningkatkan efisiensi kode program.   Hasil akhirnya, seluruh baris bisa diproses dalam waktu 32 menit (47 TPS).   Data yang dihasilkan adalah 91.190 record di tabel invoice, 17.485 record di tabel pembayaran, dan 73.413 record di tabel inventory.

Kecepatan ini sudah lebih dari cukup mengingat ini adalah aplikasi desktop yang dipakai oleh pengguna tunggal dan hanya sesekali berinteraksi dengan sistem lainnya.

Percobaan 5

Apakah masih bisa lebih cepat?  simple-jpa 0.4.2 memungkinkan pengaturan flush mode untuk EntityManager secara global.   Nilai yang diperboleh adalah "AUTO" atau "COMMIT".   Secara default, di Hibernate JPA, nilainya adalah "AUTO".   Nilai ini menyebabkan Hibernate terkadang akan men-flush() secara otomatis.   Pada saat memanggil sebuah query,  Hibernate bisa saja akan men-flush() bila dirasa perlu.   Bila nilai flush mode adalah "COMMIT", maka proses flush() hanya akan dikerjakan pada saat transaksi di-commit() atau saat flush() dipanggil secara manual.   Pada dokumentasi JPA, disebutkan bahwa nilai "COMMIT" tidak disarankan karena terdapat kemungkinan menghasilkan data yang tidak konsisten. Tapi tidak ada salahnya saya mencoba.

Untuk itu, saya menambahkan baris berikut ini di Config.groovy:

griffon.simplejpa.entityManager.defaultFlushMode = "COMMIT"

Saya cukup terkejut melihat kecepatannya meningkat drastis.   Bahkan saya bisa dengan aman membuang flush() dan clear() yang tadinya secara periodik membersihkan EntityManager agar tidak lambat.   Seluruh perubahan akan diproses ke dalam memori terlebih dahulu tanpa menyentuh database (butuh memori yang besar?), lalu setelah selesai, mereka baru dimasukkan ke database secara keseluruhan.   Hasilnya? Transaksi dapat diselesaikan dalam waktu 8 menit (190 TPS)!   Atau dengan kata lain, untuk jangka waktu 1 detik, aplikasi yang saya uji ini dapat memproses hingga maksimal 190 penyimpanan data invoice baru termasuk segala business process-nya. Ini adalah nilai yang cukup menggembirakan.

Ikuti

Get every new post delivered to your Inbox.

Bergabunglah dengan 36 pengikut lainnya.