Panduan Mapping Asosiasi JPA


Artikel ini berisi contoh relasi antar-objek dan hubungannya dengan relasi tabel (database) disertai dengan class diagram dan contoh kode program.

Java Persistence API (JPA) adalah bagian dari Java sejak tahun 2006.   Versi terbaru JPA adalah versi 2.0 (Desember 2009).   JPA merupakan sebuah spesifikasi yang terdiri atas API untuk menyimpan object (disebut sebagai entity) ke dalam database relasional, termasuk juga spesifikasi Java Persistence Query Language (JP QL).   JP QL adalah bahasa query yang mirip SQL tetapi dipakai untuk object.  Karena JPA hanyalah sebuah spesifikasi, developer membutuhkan implementasinya untuk dipakai.   Contoh implementasi JPA adalah Hibernate, EclipseLink, DataNucleus, OpenJPA, dan sebagainya.   Secara teori, karena semua implementasi tersebut adalah JPA, maka developer bisa berpindah dari satu implementasi JPA ke implementasi JPA lainnya secara mudah.   Akan tetapi pada prakteknya, terkadang developer harus memakai fitur spesifik yang ada di salah satu implementasi yang membuat ia terikat pada sebuah implementasi JPA.   Sebagai contoh, saya kerap memakai annotation @Type untuk atribut Joda Time.   Sebenarnya ini adalah fitur spesifik dari Hibernate dan bukan bagian dari JPA (sehingga belum tentu ada di implementasi JPA lainnya).

Pada kesempatan ini, saya akan mencoba melakukan pemetaan asosiasi/relasi antar-object. Saya akan menggunakan plugin simple-jpa pada Griffon untuk mempermudah proses pembelajaran.

One-to-one Unidirectional

Berikut ini adalah UML class diagram yang menggambarkan hubungan one-to-one:

Relasi one-to-one unidirectional

Relasi one-to-one unidirectional

Pada relasi di atas, seorang Pegawai hanya memiliki satu NPWP (nomor pokok wajib pajak).   Seorang Pegawai tidak boleh memiliki lebih dari satu NPWP.   Demikian juga NPWP yang sama tidak mungkin dimiliki oleh Pegawai lain selain Pegawai tersebut.

Relasi di atas juga memperlihatkan sebuah arah unidirectional dari Pegawai ke Pajak. Dengan demikian, saya bisa memperoleh object Pajak dari object Pegawai; tetapi tidak sebaliknya.

Contoh mapping dari class diagram di atas adalah:

@DomainModel @Entity
@TupleConstructor @ToString
class Pegawai {

    @Size(min=10, max=10)
    String nomorPegawai

    @Size(min=3, max=100)
    String namaLengkap

    @Size(min=5, max=200)
    String alamat

    @OneToOne(cascade=CascadeType.PERSIST)
    Pajak pajak

}

@DomainModel @Entity
@TupleConstructor @ToString
class Pajak {

    @Size(min=20, max=20)
    String nomorPokokWajibPajak

    BigDecimal ptkp

    BigDecimal pph

}

Contoh struktur tabel yang dihasilkan secara otomatis:

mysql> desc pegawai;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| id           | bigint(20)   | NO   | PRI | NULL    |       |
| alamat       | varchar(200) | YES  |     | NULL    |       |
| createdDate  | datetime     | YES  |     | NULL    |       |
| deleted      | varchar(255) | YES  |     | NULL    |       |
| modifiedDate | datetime     | YES  |     | NULL    |       |
| namaLengkap  | varchar(100) | YES  |     | NULL    |       |
| nomorPegawai | varchar(10)  | YES  |     | NULL    |       |
| pajak_id     | bigint(20)   | YES  | MUL | NULL    |       |
+--------------+--------------+------+-----+---------+-------+

mysql> desc pajak;
+----------------------+---------------+------+-----+---------+-------+
| Field                | Type          | Null | Key | Default | Extra |
+----------------------+---------------+------+-----+---------+-------+
| id                   | bigint(20)    | NO   | PRI | NULL    |       |
| createdDate          | datetime      | YES  |     | NULL    |       |
| deleted              | varchar(255)  | YES  |     | NULL    |       |
| modifiedDate         | datetime      | YES  |     | NULL    |       |
| nomorPokokWajibPajak | varchar(20)   | YES  |     | NULL    |       |
| pph                  | decimal(19,2) | YES  |     | NULL    |       |
| ptkp                 | decimal(19,2) | YES  |     | NULL    |       |
+----------------------+---------------+------+-----+---------+-------+

Untuk meningkatkan konsistensi data, pengguna perlu memberikan unique constraints secara manual pada foreign key yang merujuk ke field id milik tabel pajak. Hal ini karena struktur tabel saat ini memungkinkan lebih dari satu Pegawai memiliki npwp (referensi ke Pajak) yang sama.

Contoh kode program yang membuat dan menyimpan entity:

Pegawai snake = new Pegawai("PP/01/05-1", "Solid Snake", "Small Ville")
snake.pajak = new Pajak("01.718.327.8-091.000")
persist(snake)

Contoh kode program yang membaca entity:

Pegawai snake = findPegawaiByNamaLengkap("Solid Snake")[0]
println "NPWP Solid Snake adalah ${snake.pajak.nomorPokokWajibPajak}"

One-to-one Bidirectional

Perbedaan antara relasi unidirectional dan bidirectional adalah pada relasi bidirectional, developer selalu bisa mengakses pasangan entity dari entity lainnya. Berikut ini adalah contoh UML Class Diagram untuk hubungan tersebut:

Relasi one-to-one bidirectional

Relasi one-to-one bidirectional

Satu-satunya perubahan adalah kini Pajak memiliki atribut Pegawai sehingga developer bisa mengakses Pegawai dari object Pajak (dan juga sebaliknya).

Contoh mapping:

@DomainModel @Entity
@TupleConstructor @ToString(excludes="pajak")
class Pegawai {

    @Size(min=10, max=10)
    String nomorPegawai

    @Size(min=3, max=100)
    String namaLengkap

    @Size(min=5, max=200)
    String alamat

    @OneToOne(cascade=CascadeType.PERSIST)
    Pajak pajak

}

@DomainModel @Entity
@TupleConstructor @ToString
class Pajak {

    @Size(min=20, max=20)
    String nomorPokokWajibPajak

    BigDecimal ptkp

    BigDecimal pph

    @OneToOne(mappedBy = "pajak")
    Pegawai pegawai

}

Contoh kode program yang membuat dan menyimpan entity:

Pegawai snake = new Pegawai("PP/01/05-1", "Solid Snake", "Small Ville")
snake.pajak = new Pajak(nomorPokokWajibPajak: "01.718.327.8-091.000", pegawai: snake)
persist(snake)

Pada relasi bidirectional, jangan lupa menyimpan nilai atribut pegawai untuk Pajak bila ingin menyimpan relasi tersebut dengan satu kali persist(). Selain itu, annotation @ToString pada class Pegawai perlu menyertakan excludes="pajak" agar tidak terjadi proses rekursif yang tidak pernah berakhir.

Contoh kode program yang membaca entity:

Pegawai snake = findPegawaiByNamaLengkap("Solid Snake")[0]
println "NPWP Solid Snake adalah ${snake.pajak.nomorPokokWajibPajak}"
...
Pajak pajak = findPajakByNomorPokokWajibPajak("01.718.327.8-091.000")[0]
println "Pegawai dengan NPWP 01.718.327.8-091.000 memiliki nomor induk ${pajak.pegawai.nomorPegawai}"

Pada domain class Pegawai di atribut pajak, annotation @OneToOne memiliki nilai atribut cascade berupa CascadeType.PERSIST. Hal ini berarti cascade hanya dilakukan pada operasi penyimpanan dan tidak berlaku pada operasi hapus (dan juga operasi lainnya!).   Dengan demikian, kode program berikut ini akan menghapus object Pajak yang berhubungan dengan seorang Pegawai tanpa menghapus Pegawai tersebut:

Pegawai snake = findPegawaiByNamaLengkap("Solid Snake")[0]
remove(snake.pajak)
snake.pajak = null

Begitu juga sebaliknya, sebuah Pajak bisa saja tidak dimiliki oleh Pegawai.   Sebagai contoh,  kode program berikut ini akan menghapus Pegawai tetapi tidak menghapus Pajak:

remove(snake)

Bila ingin menghapus Pegawai secara otomatis akan menghapus Pajak, maka saya dapat mengubah nilai cascade di Pegawai.groovy menjadi CascadeType.ALL seperti berikut ini:

@OneToOne(cascade=CascadeType.ALL)
Pajak pajak

Sekarang, kode program berikut ini:

remove(snake)

akan menghapus object Pegawai sekaligus Pajak yang berhubungan dengan Pegawai tersebut.

One-to-many Unidirectional

Berikut ini adalah contoh UML class diagram yang menggambarkan hubungan one-to-many:

Relasi one-to-many unidirectional

Relasi one-to-many unidirectional

Pada relasi di atas, sebuah Transaksi boleh tidak memiliki ItemTransaksi atau memiliki banyak ItemTransaksi.   Tergantung pada kebutuhan, multiplicity 0..* bisa diganti menjadi 1..* sehingga sebuah Transaksi wajib memiliki minimal sebuah ItemTransaksi.

Relasi di atas juga memperlihatkan bahwa sebuah domain class tidak hanya terdiri atas atribut, melainkan juga operasi.   Sebagai contoh, getTotal() adalah sebuah operasi di Transaksi yang akan menghitung total nominal Transaksi tersebut.   Nilai getTotal() tidak akan disimpan di database, tetapi dapat dipanggil kapan saja saat dibutuhkan (selama terdapat sebuah Transaksi).

Contoh mapping dari class diagram di atas adalah:

@DomainModel @Entity
@TupleConstructor @ToString
class Transaksi {

    @Size(min=5, max=5)
    @Column(unique=true)
    String kodeTransaksi

    @OneToMany(cascade=CascadeType.ALL)
    List<ItemTransaksi> listItemTransaksi = []

    BigDecimal getTotal() {
       listItemTransaksi.sum { ItemTransaksi it -> it.nominalTransaksi }
    }

}

@DomainModel @Entity
@TupleConstructor @ToString
class ItemTransaksi {

    @Size(min=3, max=50)
    String namaItem 

    @NotNull 
    BigDecimal nominalTransaksi
}

Pada definisi class di atas, Transaksi memiliki atribut listItemTransaksi yang berupa sebuah ListList adalah salah satu Collection.   Karena  ‘many’ pada hubungan ‘one-to-many’ berarti memiliki lebih dari satu nilai maka ia harus disimpan dalam struktur data yang memungkinkan penyimpanan lebih dari satu nilai (Collection).

Contoh struktur tabel yang dihasilkan secara otomatis:

mysql> desc transaksi;
+---------------+--------------+------+-----+---------+-------+
| Field         | Type         | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+-------+
| id            | bigint(20)   | NO   | PRI | NULL    |       |
| createdDate   | datetime     | YES  |     | NULL    |       |
| deleted       | varchar(255) | YES  |     | NULL    |       |
| kodeTransaksi | varchar(5)   | YES  | UNI | NULL    |       |
| modifiedDate  | datetime     | YES  |     | NULL    |       |
+---------------+--------------+------+-----+---------+-------+

mysql> desc itemtransaksi;
+------------------+---------------+------+-----+---------+-------+
| Field            | Type          | Null | Key | Default | Extra |
+------------------+---------------+------+-----+---------+-------+
| id               | bigint(20)    | NO   | PRI | NULL    |       |
| createdDate      | datetime      | YES  |     | NULL    |       |
| deleted          | varchar(255)  | YES  |     | NULL    |       |
| modifiedDate     | datetime      | YES  |     | NULL    |       |
| namaItem         | varchar(50)   | YES  |     | NULL    |       |
| nominalTransaksi | decimal(19,2) | NO   |     | NULL    |       |
+------------------+---------------+------+-----+---------+-------+

mysql> desc transaksi_itemtransaksi;
+----------------------+------------+------+-----+---------+-------+
| Field                | Type       | Null | Key | Default | Extra |
+----------------------+------------+------+-----+---------+-------+
| Transaksi_id         | bigint(20) | NO   | MUL | NULL    |       |
| listItemTransaksi_id | bigint(20) | NO   | PRI | NULL    |       |
+----------------------+------------+------+-----+---------+-------+

Walaupun hanya terdapat 2 domain class, tabel yang dihasilkan adalah 3 tabel.   Tabel transaksi_itemtransaksi berperan untuk menampung hubungan one-to-many yang berisi referensi ke id di dua tabel lainnya (foreign key).   Pada tabel transaksi_itemtransaksi, field listItemTransaksi_id bersifat unique.   Dengan demikian, sebuah instance ItemTransaksi hanya dapat dimiliki oleh satu instance Transaksi.   Bila tidak dibuat unique, maka hubungan akan menjadi many-to-many.

Contoh kode program yang membuat dan menyimpan entity:

Transaksi transaksi = new Transaksi("TR001")
transaksi.listItemTransaksi << new ItemTransaksi("ITEM1", new BigDecimal("99999.99"))
transaksi.listItemTransaksi << new ItemTransaksi("ITEM2", new BigDecimal("0.01"))
persist(transaksi)

Kode program di atas akan mengisi tabel dengan nilai seperti berikut ini:

mysql> select * from transaksi;
+----+---------------------+---------+---------------+--------------+
| id | createdDate         | deleted | kodeTransaksi | modifiedDate |
+----+---------------------+---------+---------------+--------------+
|  1 | 2013-04-16 07:01:41 | N       | TR001         | NULL         |
+----+---------------------+---------+---------------+--------------+

mysql> select * from itemtransaksi;
+----+---------------------+---------+--------------+----------+------------------+
| id | createdDate         | deleted | modifiedDate | namaItem | nominalTransaksi |
+----+---------------------+---------+--------------+----------+------------------+
|  1 | 2013-04-16 03:01:42 | N       | NULL         | ITEM1    |         99999.99 |
|  2 | 2013-04-16 04:01:42 | N       | NULL         | ITEM2    |             0.01 |
+----+---------------------+---------+--------------+----------+------------------+

mysql> select * from transaksi_itemtransaksi;
+--------------+----------------------+
| Transaksi_id | listItemTransaksi_id |
+--------------+----------------------+
|            1 |                    1 |
|            1 |                    2 |
+--------------+----------------------+

Contoh kode program yang membaca entity:

Transaksi transaksi = findTransaksiByKodeTransaksi("TR001")[0]
println "Transaksi adalah $transaksi"
println "Total nominal adalah ${transaksi.total}"

Hasilnya akan terlihat seperti berikut ini:

Transaksi adalah domain.Transaksi(TR001, [domain.ItemTransaksi(ITEM1, 99999.99), domain.ItemTransaksi(ITEM2, 0.01)])
Total nominal adalah 100000.00

One-to-many Bidirectional

Sebuah class diagram yang tidak menunjukkan direction secara jelas, biasanya akan dianggap sebagai bidirectional, seperti yang terlihat pada gambar berikut:

Relasi one-to-many bidirectional

Relasi one-to-many bidirectional

Pada diagram di atas, class Transaksi memiliki method tambahItemTransaksi() untuk mempermudah menambah ItemTransaksi dan mengisi nilai transaksi milik ItemTransaksi.

Contoh mapping dari class diagram di atas adalah:

@DomainModel @Entity
@TupleConstructor @ToString
class Transaksi {

    @Size(min=5, max=5)
    @Column(unique=true)
    String kodeTransaksi

    @OneToMany(cascade=CascadeType.ALL, mappedBy="transaksi")
    List<ItemTransaksi> listItemTransaksi = []

    void tambahTransaksi(ItemTransaksi itemTransaksi) {
        itemTransaksi.transaksi = this
        listItemTransaksi << itemTransaksi     
    }

    BigDecimal getTotal() {         
        listItemTransaksi.sum { ItemTransaksi it -> it.nominalTransaksi }
    }

}

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

    @Size(min=3, max=50)
    String namaItem

    @NotNull
    BigDecimal nominalTransaksi

    @ManyToOne
    Transaksi transaksi

}

Tidak seperti pada versi unidirectional, definisi domain class di atas hanya akan menghasilkan 2 tabel saja, seperti yang terlihat berikut ini:

mysql> desc transaksi;
+---------------+--------------+------+-----+---------+-------+
| Field         | Type         | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+-------+
| id            | bigint(20)   | NO   | PRI | NULL    |       |
| createdDate   | datetime     | YES  |     | NULL    |       |
| deleted       | varchar(255) | YES  |     | NULL    |       |
| kodeTransaksi | varchar(5)   | YES  | UNI | NULL    |       |
| modifiedDate  | datetime     | YES  |     | NULL    |       |
+---------------+--------------+------+-----+---------+-------+

mysql> desc itemtransaksi;
+------------------+---------------+------+-----+---------+-------+
| Field            | Type          | Null | Key | Default | Extra |
+------------------+---------------+------+-----+---------+-------+
| id               | bigint(20)    | NO   | PRI | NULL    |       |
| createdDate      | datetime      | YES  |     | NULL    |       |
| deleted          | varchar(255)  | YES  |     | NULL    |       |
| modifiedDate     | datetime      | YES  |     | NULL    |       |
| namaItem         | varchar(50)   | YES  |     | NULL    |       |
| nominalTransaksi | decimal(19,2) | NO   |     | NULL    |       |
| transaksi_id     | bigint(20)    | YES  | MUL | NULL    |       |
+------------------+---------------+------+-----+---------+-------+

Berikut ini adalah contoh kode program yang membuat dan menyimpan entity:

Transaksi transaksi = new Transaksi("TR001")
transaksi.tambahTransaksi(new ItemTransaksi("ITEM1", new BigDecimal("99999.99")))
transaksi.tambahTransaksi(new ItemTransaksi("ITEM2", new BigDecimal("0.01")))
persist(transaksi)

Kode program di atas akan menyimpan data ke dalam tabel berupa:

mysql> select * from transaksi;
+----+---------------------+---------+---------------+--------------+
| id | createdDate         | deleted | kodeTransaksi | modifiedDate |
+----+---------------------+---------+---------------+--------------+
|  1 | 2013-04-15 07:14:16 | N       | TR001         | NULL         |
+----+---------------------+---------+---------------+--------------+

mysql> select * from itemTransaksi;
+----+---------------------+---------+--------------+----------+------------------+--------------+
| id | createdDate         | deleted | modifiedDate | namaItem | nominalTransaksi | transaksi_id |
+----+---------------------+---------+--------------+----------+------------------+--------------+
|  1 | 2013-04-16 07:14:17 | N       | NULL         | ITEM1    |         99999.99 |            1 |
|  2 | 2013-04-16 07:14:17 | N       | NULL         | ITEM2    |             0.01 |            1 |
+----+---------------------+---------+--------------+----------+------------------+--------------+

Berikut ini adalah contoh kode program yang membaca data:

Transaksi transaksi = findTransaksiByKodeTransaksi("TR001")[0]
println "Transaksi adalah $transaksi"
println "Total nominal adalah ${transaksi.total}"
...
ItemTransaksi itr  = findItemTransaksiById(1l)
println "ItemTransaksi pertama adalah $itr"
println "ItemTransaksi tersebut milik transaksi ${itr.transaksi}"
println "ItemTransaksi tersebut bernilai ${itr.nominalTransaksi} dan " +
    "bagian dari transaksi dengan total nominal ${itr.transaksi.total}"

Hasil dari kode program di atas akan terlihat seperti berikut ini:

Transaksi adalah domain.Transaksi(TR001, [domain.ItemTransaksi(ITEM1, 99999.99), domain.ItemTransaksi(ITEM2, 0.01)])
Total nominal adalah 100000.00
...
ItemTransaksi pertama adalah domain.ItemTransaksi(ITEM1, 99999.99)
ItemTransaksi tersebut milik transaksi domain.Transaksi(TR001, [domain.ItemTransaksi(ITEM1, 99999.99), domain.ItemTransaksi(ITEM2, 0.01)])
ItemTransaksi tersebut bernilai 99999.99 dan bagian dari transaksi dengan total nominal 100000.00

Many-to-many Unidirectional

Berikut ini adalah contoh UML class diagram yang menggambarkan hubungan many-to-many unidirectional:

Relasi many-to-many unidirectional

Relasi many-to-many unidirectional

Pada diagram di atas, sebuah Kelas boleh tidak mengambil Matakuliah (misalnya karena seluruh mahasiswa sedang cuti) dan boleh mengambil satu atau lebih Matakuliah.   Sebuah Matakuliah wajib harus diikuti oleh minimal satu Kelas (boleh lebih dari satu Kelas).

Contoh mapping dari class diagram di atas adalah:

@DomainModel @Entity
@TupleConstructor @ToString
class Kelas {

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

    @NotNull
    Integer angkatan

    @ManyToMany(cascade=CascadeType.ALL)
    List<Matakuliah> listMatakuliah = []

}

@DomainModel @Entity
@TupleConstructor @ToString
class Matakuliah {

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

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

}

Mapping di atas akan menghasilkan struktur tabel seperti berikut ini:

mysql> desc kelas;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| id           | bigint(20)   | NO   | PRI | NULL    |       |
| angkatan     | int(11)      | YES  |     | NULL    |       |
| createdDate  | datetime     | YES  |     | NULL    |       |
| deleted      | varchar(255) | YES  |     | NULL    |       |
| kode         | varchar(3)   | YES  |     | NULL    |       |
| modifiedDate | datetime     | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+

mysql> desc matakuliah;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| id           | bigint(20)   | NO   | PRI | NULL    |       |
| createdDate  | datetime     | YES  |     | NULL    |       |
| deleted      | varchar(255) | YES  |     | NULL    |       |
| kode         | varchar(3)   | YES  |     | NULL    |       |
| modifiedDate | datetime     | YES  |     | NULL    |       |
| nama         | varchar(50)  | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+

mysql> desc kelas_matakuliah;
+-------------------+------------+------+-----+---------+-------+
| Field             | Type       | Null | Key | Default | Extra |
+-------------------+------------+------+-----+---------+-------+
| Kelas_id          | bigint(20) | NO   | MUL | NULL    |       |
| listMatakuliah_id | bigint(20) | NO   | MUL | NULL    |       |
+-------------------+------------+------+-----+---------+-------+

Berikut ini adalah contoh kode program yang membuat dan menyimpan entity:

Kelas kelas = new Kelas("A01", 2000)
kelas.listMatakuliah << new Matakuliah("TI1", "Pemograman Java")
kelas.listMatakuliah << new Matakuliah("TI2", "Teknik Kompilasi")
kelas.listMatakuliah << new Matakuliah("TI3", "Kecerdasan Buatan")
persist(kelas)

Berikut ini adalah contoh kode program yang membaca entity:

Kelas kelas = findKelasByKode("A01")[0]
println "Kelas adalah $kelas"
println "Kelas ini mengambil matakuliah:"
kelas.listMatakuliah.each { println "${it.kode} - ${it.nama}" }

Hasil dari kode program di atas akan terlihat seperti berikut ini:

Kelas adalah domain.Kelas(A01, 2000, [domain.Matakuliah(TI1, Pemograman Java), domain.Matakuliah(TI2, Teknik Kompilasi), domain.Matakuliah(TI3, Kecerdasan Buatan)])
Kelas ini mengambil matakuliah:
TI1 - Pemograman Java
TI2 - Teknik Kompilasi
TI3 - Kecerdasan Buatan

Many-to-many Bidirectional

Berikut ini adalah contoh UML class diagram yang menggambarkan hubungan many-to-many bidirectional:

Relasi many-to-many bidirectional

Relasi many-to-many bidirectional

Contoh mapping untuk class diagram di atas adalah:

@DomainModel @Entity
@TupleConstructor @ToString
class Kelas {

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

    @NotNull
    Integer angkatan

    @ManyToMany
    List<Matakuliah> listMatakuliah = []

    void tambahMatakuliah(Matakuliah matakuliah) {
        if (!matakuliah.listKelas.contains(this)) {
            matakuliah.listKelas.add(this)
        }        
        listMatakuliah << matakuliah
    }
}

@DomainModel @Entity
@TupleConstructor @ToString(excludes = "listKelas")
class Matakuliah {

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

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

    @ManyToMany(mappedBy="listMatakuliah")
    List<Kelas> listKelas = []

}

Berikut ini adalah contoh kode program yang membuat dan menyimpan entity:

Kelas kelasA01 = new Kelas("A01", 2000)
persist(kelasA01)

Kelas kelasA02 = new Kelas("A02", 2000)
persist(kelasA02)

Matakuliah pemogramanJava = new Matakuliah("TI1", "Pemograman Java")
persist(pemogramanJava)

Matakuliah teknikKompilasi = new Matakuliah("TI2", "Teknik Kompilasi")
persist(teknikKompilasi)

Matakuliah kecerdasanBuatan = new Matakuliah("TI3", "Kecerdasan Buatan")
persist(kecerdasanBuatan)

kelasA01.tambahMatakuliah(pemogramanJava)
kelasA01.tambahMatakuliah(teknikKompilasi)

kelasA02.tambahMatakuliah(teknikKompilasi)
kelasA02.tambahMatakuliah(kecerdasanBuatan)

Kode program di atas akan menghasilkan isi tabel seperti berikut ini:

mysql> select * from kelas;
+----+----------+---------------------+---------+------+--------------+
| id | angkatan | createdDate         | deleted | kode | modifiedDate |
+----+----------+---------------------+---------+------+--------------+
|  1 |     2000 | 2013-04-16 09:26:30 | N       | A01  | NULL         |
|  2 |     2000 | 2013-04-16 09:26:31 | N       | A02  | NULL         |
+----+----------+---------------------+---------+------+--------------+

mysql> select * from matakuliah;
+----+---------------------+---------+------+--------------+-------------------+
| id | createdDate         | deleted | kode | modifiedDate | nama              |
+----+---------------------+---------+------+--------------+-------------------+
|  1 | 2013-04-16 09:26:31 | N       | TI1  | NULL         | Pemograman Java   |
|  2 | 2013-04-16 09:26:31 | N       | TI2  | NULL         | Teknik Kompilasi  |
|  3 | 2013-04-16 09:26:31 | N       | TI3  | NULL         | Kecerdasan Buatan |
+----+---------------------+---------+------+--------------+-------------------+

mysql> select * from matakuliah_kelas;
+-------------------+--------------+
| listMatakuliah_id | listKelas_id |
+-------------------+--------------+
|                 1 |            1 |
|                 2 |            1 |
|                 2 |            2 |
|                 3 |            2 |
+-------------------+--------------+

Contoh kode program yang mencari dan membaca entitas:

Kelas kelasA01 = findKelasByKode("A01")[0]
println "Daftar Matakuliah Untuk Kelas A01:"
kelasA01.listMatakuliah.each { println "${it.kode} - ${it.nama}" }
println()

Kelas kelasA02 = findKelasByKode("A02")[0]
println "Daftar Matakuliah Untuk Kelas A02:"
kelasA02.listMatakuliah.each { println "${it.kode} - ${it.nama}" }
println()

println "Daftar Kelas Yang Mengambil Matakuliah Pemograman Java:"
findMatakuliahByKode("TI1")[0].listKelas.each { println "${it.kode} - ${it.angkatan}" }
println()

println "Daftar Kelas Yang Mengambil Matakuliah Teknik Kompilasi:"
findMatakuliahByKode("TI2")[0].listKelas.each { println "${it.kode} - ${it.angkatan}" }
println()

Tampilan dari kode program di atas akan terlihat seperti:

Daftar Matakuliah Untuk Kelas A01:
TI1 - Pemograman Java
TI2 - Teknik Kompilasi

Daftar Matakuliah Untuk Kelas A02:
TI2 - Teknik Kompilasi
TI3 - Kecerdasan Buatan

Daftar Kelas Yang Mengambil Matakuliah Pemograman Java:
A01 - 2000

Daftar Kelas Yang Mengambil Matakuliah Teknik Kompilasi:
A01 - 2000
A02 - 2000

Perihal Solid Snake
I'm nothing...

8 Responses to Panduan Mapping Asosiasi JPA

  1. Ping-balik: Panduan Mapping Collection | The Solid Snake

  2. Komang Hendra Santosa mengatakan:

    Mas, untuk BigDecimal ga bisa digenerate. Ketika digenerate di Model muncul sbb:

    ItemTransaksiModel.groovy

    @Bindable Long id
    @Bindable String namaItem
    // nominalTransaksi is not supported by generator. You will need to code it manually.
    null

    @Bindable String namaItemSearch
    @Bindable String searchMessage

    TransaksiModel.groovy

    @Bindable Long id
    @Bindable String kodeTransaksi
    null

    @Bindable String kodeTransaksiSearch
    @Bindable String searchMessage

    Apa ada yang salah ya?

    • Solid Snake mengatakan:

      BigDecimal dan BigInteger di support di simple-jpa 0.3, saat ini masih sedang di-coba, belum di-release. Untuk angka akan memakai komponen JFormattedTextField yang men-format sesuai dengan titik dan koma sesuai locale masing2.

  3. Komang Hendra Santosa mengatakan:

    Ah saya coba code yg ini:

    @DomainModel @Entity
    @TupleConstructor @ToString
    class Transaksi {

    @Size(min=5, max=5)
    @Column(unique=true)
    String kodeTransaksi

    @OneToMany(cascade=CascadeType.ALL)
    List listItemTransaksi = []

    BigDecimal getTotal() {
    listItemTransaksi.sum { ItemTransaksi it -> it.nominalTransaksi }
    }

    }

    @DomainModel @Entity
    @TupleConstructor @ToString
    class Transaksi {

    @Size(min=5, max=5)
    @Column(unique=true)
    String kodeTransaksi

    @OneToMany(cascade=CascadeType.ALL)
    List listItemTransaksi

    }

    ketika saya run-app muncul error:

    2013-04-17 16:10:14,259 [main] ERROR griffon.util.GriffonExceptionHandler – Unca
    ught Exception
    javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to bui
    ld EntityManagerFactory
    at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Con
    figuration.java:915)
    at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Con
    figuration.java:890)
    at org.hibernate.ejb.HibernatePersistence.createEntityManagerFactory(Hib
    ernatePersistence.java:57)
    at SimpleJpaGriffonAddon.addonPostInit(SimpleJpaGriffonAddon.groovy:38)
    at griffon.core.GriffonAddon$addonPostInit.call(Unknown Source)
    at griffon.core.GriffonAddon$addonPostInit.call(Unknown Source)
    at org.codehaus.griffon.runtime.util.AddonHelper$_handleAddonsAtStartup_
    closure3.doCall(AddonHelper.groovy:110)
    at org.codehaus.griffon.runtime.util.AddonHelper.handleAddonsAtStartup(A
    ddonHelper.groovy:108)
    at org.codehaus.griffon.runtime.core.DefaultAddonManager.doInitialize(De
    faultAddonManager.java:33)
    at org.codehaus.griffon.runtime.core.AbstractAddonManager.initialize(Abs
    tractAddonManager.java:101)
    at org.codehaus.griffon.runtime.util.GriffonApplicationHelper.initialize
    AddonManager(GriffonApplicationHelper.java:394)
    at org.codehaus.griffon.runtime.util.GriffonApplicationHelper.prepare(Gr
    iffonApplicationHelper.java:149)
    at org.codehaus.griffon.runtime.core.AbstractGriffonApplication.initiali
    ze(AbstractGriffonApplication.java:231)
    at griffon.swing.AbstractSwingGriffonApplication.bootstrap(AbstractSwing
    GriffonApplication.java:75)
    at griffon.swing.AbstractSwingGriffonApplication.run(AbstractSwingGriffo
    nApplication.java:132)
    at griffon.swing.SwingApplication.run(SwingApplication.java:45)
    at griffon.swing.SwingApplication.main(SwingApplication.java:37)
    Caused by: org.hibernate.AnnotationException: Collection has neither generic typ
    e or OneToMany.targetEntity() defined: domain.Transaksi.listItemTransaksi
    at org.hibernate.cfg.annotations.CollectionBinder.getCollectionType(Coll
    ectionBinder.java:643)
    at org.hibernate.cfg.annotations.CollectionBinder.bind(CollectionBinder.
    java:536)
    at org.hibernate.cfg.AnnotationBinder.processElementAnnotations(Annotati
    onBinder.java:1954)
    at org.hibernate.cfg.AnnotationBinder.processIdPropertiesIfNotAlready(An
    notationBinder.java:766)
    at org.hibernate.cfg.AnnotationBinder.bindClass(AnnotationBinder.java:68
    5)
    at org.hibernate.cfg.Configuration$MetadataSourceQueue.processAnnotatedC
    lassesQueue(Configuration.java:3443)
    at org.hibernate.cfg.Configuration$MetadataSourceQueue.processMetadata(C
    onfiguration.java:3397)
    at org.hibernate.cfg.Configuration.secondPassCompile(Configuration.java:
    1341)
    at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.jav
    a:1737)
    at org.hibernate.ejb.EntityManagerFactoryImpl.(EntityManagerFactor
    yImpl.java:94)
    at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Con
    figuration.java:905)
    … 16 more
    [delete] Deleting directory G:\latihan\testing\staging\windows

    • Solid Snake mengatakan:

      List listItemTransaksi harusnya adalah List<ItemTransaksi> listItemTransaksi. Editor blog ini terkadang menyaring template seperti <ItemTransaksi > karena dianggap tag HTML. Saya akan memperbaikinya. Coba lihat kembali contoh kode programnya yang telah saya ubah.

  4. Ping-balik: Panduan Mapping Inheritance | The Solid Snake

  5. Komang Hendra Santosa mengatakan:

    mas mw tanya nih mengenai Mapping Asosiasi JPA

    saya ada kendala ketika melakukan search, add item, save, delete item. Bisa minta tolong koreksi skirp saya mas?

    — kendala: pada konversi list ke String dan BigDecimal
    def search = {
    if (model.kodeBrg?.length() > 0) {
    //execInsideUIAsync { model.pembelianList.clear() }
    List result = findBarangByKodeBrg(model.kodeBrg)
    execInsideUIAsync {
    model.namaBrg = result.namaBrg
    model.harga = result.hargaBeli
    }
    }
    }

    –kendala: kodeBrg dan qty tidak tampil pada table tapi masuk di database, tidak dpt menambah item ke-2, dst tampilan item ke-2 hanya masuk namaBrg dan total pada table tp tidak masuk ke database
    def addItem = {
    Pembelian pembelian = new Pembelian(model.noNota, model.tanggal, model.kodeSupplier.selectedItem, model.userID.selectedItem)
    pembelian.addPembelian(new DetailPembelian(model.kodeBrg, model.jumlah, new BigDecimal(“${model.harga}”)))

    if (!validate(pembelian)) return_failed()

    pembelian.total = pembelian.getTotal()

    if (model.id == null) {
    // Insert operation
    if (findPembelianByNoNota(pembelian.noNota)?.size() > 0) {
    //model.errors[‘noNota’] = app.getMessage(“simplejpa.error.alreadyExist.message”)
    pembelian.addPembelian(new DetailPembelian(model.kodeBrg, model.jumlah, new BigDecimal(“${model.harga}”)))
    }
    persist(pembelian)

    execInsideUIAsync { model.pembelianList < 0) {
    model.errors[‘noNota’] = app.getMessage(“simplejpa.error.alreadyExist.message”)
    return_failed()
    }
    persist(pembelian)

    DetailPembelian dp

    execInsideUIAsync { model.pembelianList << [kodeBrg: dp.kodeBrg, namaBrg: '', qty: dp.jumlah, harga: dp.harga, total: pembelian.total] }
    } else {
    // Update operation
    Pembelian selectedPembelian = model.pembelianSelection.selected[0]
    selectedPembelian.noNota = model.noNota
    selectedPembelian.tanggal = model.tanggal
    selectedPembelian.kodeSupplier = model.kodeSupplier.selectedItem
    selectedPembelian.total = model.total
    selectedPembelian.userID = model.userID.selectedItem
    merge(selectedPembelian)
    }
    model.clear()
    }

    — kendala: belum ngerti cara hapus item satu persatu
    def delete = {
    Pembelian pembelian = model.pembelianSelection.selected[0]

    def pembelianPersist = merge(pembelian)
    remove(pembelianPersist)
    execInsideUIAsync { model.pembelianList.remove(pembelian) }
    }

    –Pembelian.groovy–

    package domain

    import com.sun.istack.internal.NotNull
    import groovy.transform.*
    import simplejpa.DomainModel
    import javax.persistence.*
    import org.hibernate.annotations.Type
    import javax.validation.constraints.*
    import org.hibernate.validator.constraints.*
    import org.joda.time.*
    import org.joda.time.LocalDateTime

    @DomainModel @Entity
    @TupleConstructor @ToString
    class Pembelian {
    @Size(min=14, max=14)
    @Column(unique = true)
    String noNota

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

    @NotNull @ManyToOne
    Supplier kodeSupplier

    @NotNull
    Integer total

    @NotNull @ManyToOne
    User userID

    @OneToMany(cascade=CascadeType.ALL)
    @JoinColumn
    @OrderColumn
    List listDetailPembelian = []

    void addPembelian(DetailPembelian detailPembelian) {
    listDetailPembelian < (it.harga * it.jumlah) }
    }
    }

    –DetailPembelian.groovy–

    package domain

    import com.sun.istack.internal.NotNull
    import groovy.transform.*
    import simplejpa.DomainModel
    import javax.persistence.*
    import org.hibernate.annotations.Type
    import javax.validation.constraints.*
    import org.hibernate.validator.constraints.*

    @DomainModel
    @Entity
    @TupleConstructor
    @ToString(excludes = “pembelian”)
    class DetailPembelian {

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

    @NotNull
    Integer jumlah

    @NotNull
    BigDecimal harga
    }

    • Solid Snake mengatakan:

      Untuk menjawab kendala pertama, perlu dilakukan perubahan kode program menjadi seperti berikut ini:

      List result = findBarangByKodeBrg(model.kodeBrg)
      execInsideUIAsync {
         model.namaBrg = result[0].namaBrg
         model.harga = result[0].hargaBeli
      }
      

      Method findBarangByKodeBrg() dapat menghasilkan lebih dari satu baris yang ditampung dalam List. Bila kamu yakin yang dikembalikan hanya satu baris, maka ambil hanya elemen pertama. Untuk mengakses elemen pertama dari List, gunakan kode program seperti result[0]. Untuk mengakses elemen kedua, gunakan kode program seperti result[1], dan seterusnya.

      Untuk melakukan konversi dari String menjadi BigDecimal, cara yang disarankan adalah melalui constructor-nya, misalnya:

      String teks = "1234.12"
      BigDecimal bigDecimal = new BigDecimal(teks)
      

      Pada simple-jpa 0.3, dapat menggunakan numberTextField() yang akan mengurusi konversi secara otomatis. Lihat https://thesolidsnake.wordpress.com/2013/05/04/memakai-tipe-data-angka-di-plugin-simple-jpa/ untuk informasi lebih lanjut.

      Untuk menjawab kendala kedua, perhatikan potongan kode program berikut ini:

      DetailPembelian dp
      
      execInsideUIAsync { model.pembelianList << [kodeBrg: dp.kodeBrg, namaBrg: '', qty: dp.jumlah, harga: dp.harga, total: pembelian.total] }
      

      Pada kode program di atas, nilai variabel dp masih null sehingga bila dipakai secara langsung akan menimbulkan NullPointerException. Salah satu perubahan yang mungkin, misalnya mengubahnya menjadi:

      DetailPembelian dp = new DetailPembelian()
      

      Sebagai informasi, scaffolding di simple-jpa 0.3 kini sudah mendukung relasi one-to-many (atau master-detail) dimana detail akan ditampilkan dalam bentuk dialog. Baca Panduan Scaffolding Di plugin simple-jpa 0.3 untuk informasi lebih lanjut.

      Untuk menjawab kendala ketiga, apakah yang dimaksud adalah menghapus elemen dari List? Bila iya, bisa menggunakan kode program seperti berikut ini:

      Pembelian p = ...
      p.listDetailPembelian.remove(0); // akan menghapus DetailPembelian yang berada di index pertama
      DetailPembelian dp = ...
      p.listDetailPembelian.remove(dp);  // akan menghapus DetailPembelian yang diwakili oleh dp (harus objek yang sama)
      
      p.listDetailPembelian.clear(); // akan menghapus seluruh DetailPembelian yang ada
      

Apa komentar Anda?

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

Logo WordPress.com

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

Gambar Twitter

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

Foto Facebook

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

Foto Google+

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

Connecting to %s

%d blogger menyukai ini: