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.

Memakai @OrderColumn Di Hibernate JPA

Anggap saja saya membuat dua buah class, Faktur dan ItemFaktur.   Sebuah Faktur dapat memilih lebih dari satu ItemFaktur, dengan demikian hubungan mereka adalah one-to-many.   Saya menginginkan mereka mereka memiliki hubungan bidirectional: sebuah Faktur bisa mengetahui apa saja ItemFaktur-nya; sebaliknya sebuah ItemFaktur bisa mengetahui siapa Faktur ‘pemilik’-nya. Berikut adalah contoh implementasi yang saya buat (memakai Griffon dan simple-jpa):

package domain

import ...

@DomainModel @Entity @Canonical(excludes='itemFakturList')
class Faktur {

    @NotBlank
    String nomor

    @NotEmpty @OneToMany(cascade=CascadeType.ALL, mappedBy="faktur")
    List<ItemFaktur> itemFakturList = []

}

@DomainModel @Entity @Canonical
class ItemFaktur {

    @NotBlank
    String namaBarang

    @NotNull
    Integer jumlah

    @NotNull @ManyToOne
    Faktur faktur

}

Pada sebuah faktur, umumnya akan terdapat item faktur yang memiliki nomor urut.   Akan tetapi pada implementasi di atas, saya bisa mendapatkan hasil berurut hanya karena kebetulan generated id yang dihasilkan untuk ItemFaktur berurut.   Seandainya saya tiba-tiba menyisipkan sebuah ItemFaktur diantara ItemFaktur yang sudah ada, maka urutan akan menjadi salah! Untuk membuktikannya, saya akan membuat test case seperti ini di simple-jpa:

public void testFaktur() {
   Faktur faktur = new Faktur("FAKTUR1")
   ItemFaktur item1 = new ItemFaktur("BARANG1", 10, faktur)
   ItemFaktur item2 = new ItemFaktur("BARANG2", 10, faktur)
   ItemFaktur item3 = new ItemFaktur("BARANG3", 10, faktur)
   faktur.itemFakturList.addAll([item1, item2, item3])
   controller.persist(faktur)

   //
   // Hasil berurut sesuai dengan yang diharapkan
   //
   controller.destroyEntityManager()
   Faktur hasil1 = controller.findFakturByNomor("FAKTUR1")[0]
   assertEquals(3, hasil1.itemFakturList.size())
   assertEquals(item1, hasil1.itemFakturList[0])
   assertEquals(item2, hasil1.itemFakturList[1])
   assertEquals(item3, hasil1.itemFakturList[2])

   //
   // Menyisipkan item4 pada baris kedua
   //
   ItemFaktur item4 = new ItemFaktur("BARANG4", 10, hasil1)
   hasil1.itemFakturList.add(1, item4)
   controller.merge(hasil1)

   //
   // Urutan tidak lagi sesuai dengan harapan
   //
   controller.destroyEntityManager()
   Faktur hasil2 = controller.findFakturByNomor("FAKTUR1")[0]
   assertEquals(4, hasil2.itemFakturList.size())
   assertEquals(item1, hasil2.itemFakturList[0])
   assertEquals(item4, hasil2.itemFakturList[1])
   assertEquals(item2, hasil2.itemFakturList[2])
   assertEquals(item3, hasil2.itemFakturList[3])

   //
   // Seharusnya:       Yang Diperoleh:
   //
   // "BARANG1" 10      "BARANG1" 10
   // "BARANG4" 10      "BARANG2" 10
   // "BARANG2" 10      "BARANG3" 10
   // "BARANG3" 10      "BARANG4" 10
   //
}

Bila test case di atas dijalankan, saya akan memperoleh kesalahan seperti berikut ini:

junit.framework.AssertionFailedError: junit.framework.AssertionFailedError:
expected:<domain.ItemFaktur(BARANG4, 10, domain.Faktur(FAKTUR1))>
but was:<domain.ItemFaktur(BARANG2, 10, domain.Faktur(FAKTUR1))>

Pengguna tentu akan terkejut bila melihat urutan item di Faktur menjadi tidak sesuai seperti saat dimasukkan. Lalu, bagaimana solusinya?

Beruntungnya, pada JPA 2.0 terdapat @OrderColumn dan @OrderBy.   Annotation @OrderColumn dipakai bila kita menginginkan pengurutan secara otomatis sesuai dengan Collection yang dipakai.   Kolom di tabel yang dipakai untuk menyimpan pengurutan tidak di-ekspos ke aplikasi melainkan dikelola oleh JPA provider secara otomatis sesuai dengan urutan di List.   Sebaliknya, bila kolom tersebut akan di-isi oleh aplikasi secara manual, maka gunakan @OrderBy.

Saya segera mengubah definisi class saya menjadi seperti berikut ini:

...
@DomainModel @Entity @Canonical(excludes='itemFakturList')
class Faktur {

    @NotBlank
    String nomor

    @NotEmpty @OneToMany(cascade=CascadeType.ALL, mappedBy="faktur") @OrderColumn
    List<ItemFaktur> itemFakturList = []

}
...

Seharusnya ini mengatasi masalah, tapi tampaknya saya tidak selalu beruntung!   Saya malah memperoleh kesalahan seperti berikut ini:

org.hibernate.HibernateException: null index column 
for collection: domain.Faktur.itemFakturList

simple-jpa saat ini memakai Hibernate sebagai JPA provider, lebih tepatnya org.hibernate:hibernate-entitymanager:4.1.9.Final.   Ternyata, versi Hibernate yang dipakai tidak mendukung penggunaan @OrderColumn bersamaan dengan mappedBy.   Saya diwajibkan untuk mengisi nilai urut ini secara manual.   Mengisi nilai urut untuk setiap ItemFaktur setiap kali menyimpan, men-update, atau men-delete bukanlah hal yang sulit, tapi hal itu seharusnya adalah sesuatu yang transparan dan otomatis.   Bila harus membuat kode program baru yang serupa setiap kali menjumpai List yang harus berurut di database, saya khawatir kemungkinkan ada sesuatu yang terlupakan dan timbulnya bug menjadi semakin besar.   Jadi ini bukan persoalan malas dan tergantung pada framework, tapi ini adalah urusan menjaga kualitas ;)

Kabar gembiranya adalah pada versi 4.2.0.Final, Hibernate sudah mendukung penggunaan @OrderColumn bersamaan dengan mappedBy.   Informasi ini saya peroleh dari https://hibernate.atlassian.net/browse/HHH-5732.   Saya segera mencoba Hibernate 4.2.0.Final.

Bagaimana caranya memakai versi Hibernate yang berbeda dari simple-jpa?   Saya membuka file griffon-app\conf\BuildConfig.groovy, kemudian mengubah beberapa bagian sehingga terlihat seperti berikut ini:

...
griffon.project.dependency.resolution = {
    ...
    repositories {
        griffonHome()

        // uncomment the below to enable remote dependency resolution
        // from public Maven repositories
        mavenLocal()
        mavenCentral()
        ...
    }
    dependencies {
        // specify dependencies here under either 'build', 'compile', 'runtime' or 'test' scopes eg.
        compile'org.hibernate:hibernate-entitymanager:4.2.0.Final'
        ...
    }
}
...

Kemudian, saya membersihkan proyek dengan:

griffon clean

Setelah ini, saya kembali menjalankan test case di atas dengan perintah:

griffon test-app FakturTest.testFaktur

Akhirnya test case berhasil dilalui dengan baik tanpa ada kesalahan lagi.   Hibernate secara otomatis memberikan urutan di tabel seperti yang terlihat pada gambar berikut ini:

Nomor urut untuk List yang diisi secara otomatis

Nomor urut untuk List yang diisi secara otomatis

Memakai @Embeddable Di JPA

Pada rancangan di post sebelumnya (Apa Bedanya Merancang Sebuah Class Dan Sebuah Tabel?), saya menunjukkan bahwa merancang class adalah proses yang berbeda dengan merancang tabel.   Tapi pada akhirnya, class memiliki data yang perlu disimpan ke tabel.  Misalnya, bagaimana menyimpan class Diskon?   Setiap objek Faktur bisa memiliki satu object Diskon atau tidak sama sekali.   Begitu juga dengan objek ItemFaktur.  Bila masing-masing objek disimpan pada tabel sendiri (terdapat 3 tabel),  setiap kali membaca objek Faktur atau ItemFaktur, perlu dilakukan join ke tabel yang menyimpan data Diskon.  Ini bukan solusi yang baik dilihat dari segi database relasional.  Apakah ada solusi lain?

Pada OOP, dapat dijumpai dua jenis object, yaitu entity object dan value object.

Sebuah entity object, setelah disimpan ke dalam tabel, dapat dicari berdasarkan id.  Objek tersebut juga dapat di-edit (bersifat mutable).  Pada kasus ini, seluruh objek Faktur adalah entity object.  Pada JPA, definisi class untuk entity object perlu memiliki annotation @Entity.

Sebuah value object tidak memiliki pengenal (id), dengan demikian mereka biasanya tidak perlu di-cari.  Nilai dari value object biasanya tidak dapat diubah (bersifat immutable) sehingga untuk mengedit nilainya dilakukan dengan membuat objek baru.  Value object biasanya mewakili sebuah nilai, sebagai contoh, seluruh instance dari class String, BigDecimal, DateTime dan sebagainya adalah sebuah value object.   Begitu juga dengan class Diskon yang saya buat sebelumnya.   Instance dari class Diskon selalu menjadi bagian dari objek lain. Pada JPA, definisi class untuk value object perlu memiliki annotation @Embeddable.

Sama seperti value object lainnya, sebuah object Diskon sebaiknya disimpan sebagai bagian dari entity object yang memilikinya. Dengan demikian, walaupun terdapat dua class, Faktur dan Diskon, hanya satu tabel yang dibutuhkan karena nilai atribut untuk objek Diskon akan disimpan pada tabel untuk objek Faktur yang memilikinya.  Begitu juga dengan relasi antara ItemFaktur dan Diskon, nilai atribut untuk objek Diskon akan disimpan pada tabel untuk objek ItemFaktur yang memilikinya.

Spesifikasi JPA tidak mendukung inheritance pada class yang diberi annotation @Embeddable (ada juga implementasi JPA yang mendukung seperti EclipseLink).  Oleh sebab itu,  saya perlu merancang ulang representasi diskon menjadi seperti berikut ini:

Rancangan Class Diagram Dengan Embeddable Class

Rancangan Class Diagram Dengan Embeddable Class

Pada diagram di atas, saya juga mengganti asosiasi biasa dengan yang lebih spesifik yaitu komposisi.  Komposisi menunjukkan bahwa objek Diskon adalah bagian dari objek Faktur.  Bila objek Faktur dihapus, maka objek Diskon miliknya juga wajib dihapus karena sebuah Diskon tidak bisa berdiri sendiri.

Berikut ini adalah contoh implementasi class Faktur:

import ...

@DomainModel @Entity @Canonical
class Faktur {

    @NotBlank @Size(min=3, max=50)
    String nomor

    @NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
    LocalDate tanggal

    @Size(min=2, max=200)
    String keterangan

    @Embedded
    Diskon diskon

    @ElementCollection @OrderColumn(name="nomorUrut")
    List<ItemFaktur> itemList = []

    BigDecimal total() {
        BigDecimal total = itemList.sum { ItemFaktur it -> it.total() }
        diskon ? diskon.hasilDiskon(total): total
    }

    ...
}

Class ItemFaktur juga memiliki Diskon, seperti yang terlihat pada kode program berikut ini:

import ...

@Embeddable @Canonical
class ItemFaktur {

    @NotNull @ManyToOne
    Barang barang

    @NotNull @Min(0l)
    Integer jumlah

    @NotNull @Min(0l)
    BigDecimal harga

    @Embedded
    Diskon diskon

    @NotNull @ManyToOne
    Faktur faktur

    BigDecimal total() {
        (diskon ? diskon.hasilDiskon(harga): harga) * jumlah
    }
}

Class Diskon sendiri akan terlihat seperti berikut ini:

import ...

@Embeddable @Canonical
class Diskon {

    @Min(0l)
    BigDecimal persen = 0;

    @Min(0l)
    BigDecimal potonganLangsung = 0;

    BigDecimal hasilDiskon(BigDecimal nilai) {
        nilai - ((persen/100) * nilai) - potonganLangsung
    }

}

Berikut ini adalah contoh penggunaan class Diskon:

Faktur faktur1 = new Faktur()
faktur1.itemList << new ItemFaktur(diskon: new Diskon(persen: 27), jumlah: 80, harga: 133400)
faktur1.itemList << new ItemFaktur(diskon: new Diskon(persen: 27), jumlah: 50, harga: 98000)
faktur1.itemList << new ItemFaktur(diskon: new Diskon(persen: 27), jumlah: 10, harga: 133400)
faktur1.diskon = new Diskon(potonganLangsung: 218)
assertEquals(12341162, faktur1.total())

Dengan rancangan seperti ini, perubahan mekanisme perhitungan diskon hanya perlu dilakukan pada class Diskon.  Walaupun Diskon adalah sebuah class terpisah, nilai atribut-nya tidak tersimpan di sebuah tabel tersendiri, melainkan tergabung bersama dengan tabel faktur atau tabel faktur_itemlist, seperti yang terlihat pada gambar berikut ini:

Struktur Tabel Yang Dihasilkan

Struktur Tabel Yang Dihasilkan

Ikuti

Get every new post delivered to your Inbox.

Bergabunglah dengan 31 pengikut lainnya.