Struktur Aplikasi Java dengan Spring dan Maven
28 May 2013Pada bagian sebelumnya kita telah membahas konfigurasi awal dan studi kasus yang akan digunakan untuk menunjukkan fitur Spring JDBC.
Di artikel bagian kedua ini, kita akan membahas tentang kerangka aplikasi yang akan dibuat. Sebelum membuat implementasi detail, sangat penting kita buat dulu kerangkanya supaya jelas apa saja bagian-bagian dalam aplikasi dan bagaimana mereka saling terhubung.
Artikel ini merupakan bagian kedua dari rangkaian artikel Spring JDBC, yaitu
- Konfigurasi koneksi database
- Struktur Aplikasi
- Insert, update, dan delete data
- Query data
- Mengetes Akses Database
Daftar class yang akan dibuat
Class yang akan dibuat kita bagi menjadi empat fungsi utama, yaitu:
- domain object : class yang mewakili struktur data dalam aplikasi kita
- interface business service : class yang mendefinisikan daftar fitur-fitur dalam aplikasi
- implementasi business service : implementasi dari interface business service. Kalau di interface hanya ada nama method, argumen, dan tipe data kembalian (return value), di sini sudah ada implementasi konkritnya, yaitu bagaimana query database, logika perhitungan, dan sebagainya.
- automated test : memeriksa apakah method kita berjalan benar itu melelahkan. Jadi kita buatkan kode program untuk mengetesnya, sehingga tes yang sama bisa dijalankan berulang-ulang tanpa membuat kita lelah. Lebih lanjut tentang automated test bisa dibaca di artikel lain yang membahas masalah ini.
Domain Object
Sesuai dengan skema database, kita akan membuat tiga class, yaitu:
- Produk
- Penjualan
- PenjualanDetail
Buat yang sudah pernah coding JDBC biasanya akan bertanya,
Kenapa repot-repot membuat domain class, kemudian harus konversi bolak balik? Kan bisa saja kita kirim
ResultSet
ke tampilan, ataupun insert langsung dari array kePreparedStatement
.
Pertanyaan ini biasanya muncul dari programmer PHP yang terbiasa langsung menampilkan kembalian mysql_fetch_array
dalam looping tabel.
Ada beberapa alasan:
-
Sebenarnya bisa saja kita buat aplikasi dengan menggunakan tipe data yang disediakan Java seperti Integer, String, Map, List, dan lainnya. Tapi akibatnya kode program kita menjadi sulit dimengerti. Coba bandingkan, lebih mudah dimengerti
public void simpan(Produk p)
ataupublic void simpan(Map p)
? Dengan membuat tipe data sesuai istilah yang digunakan di domain kita, maka kode program akan lebih mudah dipahami. -
Java merupakan bahasa yang strongly-typed, dia memeriksa tipe data/class dari tiap variabel. Pada ilustrasi di atas, method
public void simpan(Map p)
akan menerima apapun data yang kita masukkan ke dalam variabelp
. Kalau ada kesalahan dalam nama variabel (misalnya nama ditulis name), baru akan terdeteksi pada waktu aplikasi dijalankan. Berbeda denganpublic void simpan(Produk p)
yang akan langsung menimbulkan pesan error apabila kita isi dengan tipe data selainProduk
. Bug yang ditemukan pada waktu coding (compile-time) akan jauh lebih cepat diperbaiki daripada bug yang baru ditemukan pada waktu aplikasi dijalankan (runtime). Programmer PHP ada benarnya juga. Di bahasa PHP memang domain class ini tidak diperlukan, karena PHP tidak ada pemeriksaan compile-time. Tapi karena kita menggunakan Java, ada baiknya kita manfaatkan pemeriksaan compile-time ini. -
Memisahkan antara layer database dan layer antarmuka. Apabila ada perubahan skema database, asalkan fitur di tampilan tidak berubah, kita cukup mengubah mapping domain object dan skema database. Tidak perlu mengubah kode program di layer antarmuka.
-
Pustaka siap pakai untuk validasi. Di Java, ada yang namanya JSR-303, yaitu suatu pustaka yang berguna untuk validasi. Dengan menggunakan JSR-303 ini kita tidak perlu lagi melakukan pengecekan
if(produk.getKode() == null)
. Cukup kita gunakan deklarasi@NotNull private String kode;
dalam classProduk
Interface Business Service
Interface di Java artinya class yang methodnya abstrak semua. Lebih detail tentang method abstrak bisa dibaca di artikel ini. Ada beberapa alasan kenapa kita harus memisahkan interface dan implementasinya, antara lain:
-
pada waktu membuat aplikasi client-server, kita cukup memberikan domain object dan interface ini kepada programmer aplikasi client. Sedangkan implementasinya (yang berisi kode program akses database) tetap di server. Ini akan meringankan ukuran aplikasi client, karena tidak perlu menyertakan implementasi (beserta library pendukungnya yang biasanya besar) yang tidak dia gunakan.
-
kita bebas mengubah strategi implementasi (misalnya ganti database dari MySQL menjadi PostgreSQL) tanpa perlu mengganggu aplikasi client
-
fitur declarative transaction yang dimiliki Spring akan lebih optimal bekerja bila kita memisahkan interface dan implementasi.
Interface ini cukup satu class saja, yaitu AplikasiPenjualanService
.
Implementasi Business Service
Ini merupakan implementasi dari interface AplikasiPenjualanService
. Pada prakteknya, ada dua variasi yang biasa saya gunakan dalam membuat implementasi, yaitu:
- cukup membuat class implementasi service saja
- membuat class implementasi service dan juga class data access object (DAO)
Kapan memilih variasi yang mana?
- Bila menggunakan framework Spring Data JPA, kita harus pakai DAO karena frameworknya minta seperti itu
- Selain Spring Data JPA, bebas mau pakai yang mana. Pilih saja yang lebih rapi dan mudah maintenance. Untuk aplikasi kecil, class implementasi service saja sudah cukup. Kalau aplikasinya besar, akan lebih mudah membaca 10 class DAO yang masing-masingnya terdiri dari 100 baris kode daripada 1000 baris dalam satu class implementasi service. Walaupun demikian, tidak ada pertimbangan teknis yang signifikan (seperti isu performance dan lainnya) antara pakai DAO atau tidak.
Automated Test
Ini adalah kode program yang fungsinya mengetes kode program lainnya, dalam hal ini class implementasi dan class DAO. Konsep dasar tentang automated testing dibahas di artikel ini. Sedangkan untuk pengetesan database dibahas di sini.
Pada contoh aplikasi ini, kita menghadapi tantangan khusus, yaitu bagaimana caranya menggunakan test case yang sama untuk konfigurasi berbeda. Nantinya aplikasi ini akan dikembangkan untuk mendemonstrasikan akses database menggunakan framework lain seperti Hibernate, Spring Data JPA, dan JDBC polos tanpa framework. Logika pengetesan akan sama persis, yaitu:
- test insert
- test update
- test delete
- test cari berdasarkan id
- test ambil semua data dari tabel tertentu
- test cari data dengan kriteria tertentu
Tabel database yang diakses sama, sample data sama, bahkan nama method yang dijalankan juga sama. Bedanya hanyalah class implementasi mana yang digunakan dan konfigurasi mana yang dipakai.
Untuk itu, kita akan menggunakan teknik khusus yang disebut abstract junit test case
. Secara garis besar, langkahnya seperti ini:
- Buat semua method test di superclass. Superclass ini memiliki abstract method, sehingga dengan sendirinya dia juga abstract.
- Untuk mendapatkan class implementasi dan melakukan inisialisasi konfigurasi, gunakan abstract method
- Buat subclass untuk masing-masing implementasi (Spring JDBC, Hibernate, dst) yang hanya berisi implementasi dari abstract method tersebut.
Agar lebih jelas, silahkan lihat superclassnya dan subclass untuk Spring JDBC.
Struktur folder
Sekian banyak class, bagaimana penempatannya? Silahkan lihat struktur folder berikut:
Tidak ada yang istimewa dari struktur di atas, cuma struktur folder standar Maven. Mari kita lihat source code aplikasi.
Di sini kita bisa lihat class sudah diatur ke dalam package berbeda sesuai fungsinya, yaitu domain, service, dao. Untuk implementasi service dengan Spring JDBC kita buatkan package tersendiri. Selanjutnya kita lihat lokasi file konfigurasi.
File konfigurasi ditaruh dalam package. Sebetulnya ditaruh di top level juga boleh, ini hanya sekedar kebiasaan saja.
Lokasi penempatan test class bisa dilihat di atas. Abstract class yang saya ceritakan di atas terlihat di package com.muhardin.endy.training.java.aksesdb.service
, sedangkan implementasi konfigurasinya ada di subpackage springjdbc
di bawahnya.
Setelah kita melihat penempatan file dan folder, mari kita lihat kerangka kode program di masing-masing class/file. Supaya bisa mendapatkan big-picture, kita akan lihat kerangka class dan method saja. Implementasinya menyusul pada bagian selanjutnya.
Domain Object
Class Produk
Class ini merupakan padanan tabel m_produk di database. Dia memiliki beberapa property sesuai dengan kolom di database. Berikut penjelasannya
Nama Property | Nama Kolom Database | Tipe Data Java | Tipe Data MySQL |
---|---|---|---|
id | id | Integer | integer |
kode | kode | String | varchar |
nama | nama | String | varchar |
harga | harga | BigDecimal | decimal(19,2) |
Berikut kode program untuk class Produk.
package com.muhardin.endy.training.java.aksesdb.domain;
import java.math.BigDecimal;
public class Produk {
private Integer id;
private String kode;
private String nama;
private BigDecimal harga;
// getter dan setter generate dari IDE
}
Class Penjualan
Mapping Java ke SQL
Nama Property | Nama Kolom Database | Tipe Data Java | Tipe Data MySQL |
---|---|---|---|
id | id | Integer | integer |
waktuTransaksi | waktu_transaksi | Date | datetime |
Kode program class Penjualan
package com.muhardin.endy.training.java.aksesdb.domain;
// import generate dari IDE
public class Penjualan {
private Integer id;
private Date waktuTransaksi;
private List<PenjualanDetail> daftarPenjualanDetail
= new ArrayList<PenjualanDetail>();
// getter dan setter generate dari IDE
}
Class Penjualan Detail
Mapping Java ke SQL
Nama Property | Nama Kolom Database | Tipe Data Java | Tipe Data MySQL |
---|---|---|---|
id | id | Integer | integer |
penjualan | id_penjualan | Penjualan | integer foreign key |
produk | id_produk | Produk | integer foreign key |
jumlah | jumlah | Integer | integer |
harga | harga | BigDecimal | decimal(19,2) |
Kode program class PenjualanDetail
package com.muhardin.endy.training.java.aksesdb.domain;
import java.math.BigDecimal;
public class PenjualanDetail {
private Integer id;
private Penjualan penjualan;
private Produk produk;
private BigDecimal harga;
private Integer jumlah;
// getter dan setter generate dari IDE
}
Yang perlu diperhatikan di sini adalah perbedaan cara perlakuan relasi antara Java dan database.
Di Java, kita perlu mendefinisikan relasi di dua tempat, yaitu variabel daftarPenjualanDetail
di class Penjualan
dan variabel penjualan
di class PenjualanDetail
. Sedangkan di database, relasi ini cukup didefinisikan melalui foreign key
id_penjualan
di tabel t_penjualan_detail
.
Perbedaan lain, di database relasi ini cukup diwakili satu nilai saja, yaitu nilai foreign key.
Sedangkan di Java diwakili satu class penuh (Produk
atau Penjualan
) yang di dalamnya memuat banyak nilai.
Untuk menjembatani perbedaan ini, kita perlu membuat mapper untuk mengubah data dari database menjadi object Java dan sebaliknya. Contoh kode program untuk mapper ini akan dibahas pada bagian selanjutnya.
Interface Business Service
Ini adalah daftar fitur yang ada di aplikasi, didefinisikan berupa class/interface dan method.
package com.muhardin.endy.training.java.aksesdb.service;
// import generate dari IDE
public interface PenjualanService {
// service berkaitan dengan produk
void simpan(Produk p);
Produk cariProdukById(Integer id);
Produk cariProdukByKode(String kode);
Long hitungSemuaProduk();
List<Produk> cariSemuaProduk(Integer halaman, Integer baris);
Long hitungProdukByNama(String nama);
List<Produk> cariProdukByNama(String nama, Integer halaman, Integer baris);
// service yang berkaitan dengan transaksi
void simpan(Penjualan p);
Penjualan cariPenjualanById(Integer id);
Long hitungPenjualanByPeriode(Date mulai, Date sampai);
List<Penjualan> cariPenjualanByPeriode(Date mulai, Date sampai, Integer halaman, Integer baris);
Long hitungPenjualanDetailByProdukDanPeriode(Produk p, Date mulai, Date sampai);
List<PenjualanDetail> cariPenjualanDetailByProdukDanPeriode(Produk p, Date mulai, Date sampai, Integer halaman, Integer baris);
}
Implementasi Business Service
Implementasi dari interface di atas kita bagi menjadi dua kategori, yaitu class implementasi service yang nantinya akan memanggil class DAO. Pertimbangan dan alasan mengapa begini sudah dijelaskan di atas.
Class ServiceSpringJdbc
Class ini sebetulnya hanya memanggil class DAO saja, jadi baiklah kita tampilkan seluruh isinya di sini.
package com.muhardin.endy.training.java.aksesdb.service.springjdbc;
// import statement generate dari IDE
@Service @Transactional
public class PenjualanServiceSpringJdbc implements PenjualanService{
@Autowired private ProdukDao produkDao;
@Autowired private PenjualanDao penjualanDao;
@Autowired private PenjualanDetailDao penjualanDetailDao;
@Override
public void simpan(Produk p) {
produkDao.simpan(p);
}
@Override
public Produk cariProdukById(Integer id) {
return produkDao.cariById(id);
}
@Override
public Produk cariProdukByKode(String kode) {
return produkDao.cariByKode(kode);
}
@Override
public Long hitungSemuaProduk() {
return produkDao.hitungSemua();
}
@Override
public List<Produk> cariSemuaProduk(Integer halaman, Integer baris) {
return produkDao.cariSemua(halaman, baris);
}
@Override
public Long hitungProdukByNama(String nama) {
return produkDao.hitungByNama(nama);
}
@Override
public List<Produk> cariProdukByNama(String nama, Integer halaman, Integer baris) {
return produkDao.cariByNama(nama, halaman, baris);
}
@Override
public void simpan(Penjualan p) {
penjualanDao.simpan(p);
}
@Override
public Penjualan cariPenjualanById(Integer id) {
return penjualanDao.cariById(id);
}
@Override
public Long hitungPenjualanByPeriode(Date mulai, Date sampai) {
return penjualanDao.hitungByPeriode(mulai, sampai);
}
@Override
public List<Penjualan> cariPenjualanByPeriode(Date mulai, Date sampai,
Integer halaman, Integer baris) {
return penjualanDao.cariByPeriode(mulai, sampai, halaman, baris);
}
@Override
public Long hitungPenjualanDetailByProdukDanPeriode(Produk p,
Date mulai, Date sampai) {
return penjualanDetailDao.hitungByProdukDanPeriode(p, mulai, sampai);
}
@Override
public List<PenjualanDetail> cariPenjualanDetailByProdukDanPeriode(Produk p,
Date mulai, Date sampai, Integer halaman, Integer baris) {
return penjualanDetailDao.cariByProdukDanPeriode(p, mulai, sampai, halaman, baris);
}
}
Class DAO
Class DAO akan kita bahas secara mendetail di bagian selanjutnya. Pada kesempatan ini kita hanya tampilkan deklarasi class dan method saja supaya jelas mana method yang dipanggil dari implementasi service di atas.
ProdukDAO
package com.muhardin.endy.training.java.aksesdb.dao.springjdbc;
// import generate dari IDE
@Repository
public class ProdukDao {
public void simpan(Produk p) {}
public Produk cariById(Integer id) {}
public Produk cariByKode(String kode) {}
public Long hitungSemua() {}
public List<Produk> cariSemua(Integer halaman, Integer baris) {}
public Long hitungByNama(String nama) {}
public List<Produk> cariByNama(String nama, Integer halaman, Integer baris) {}
private class ResultSetJadiProduk implements RowMapper<Produk> {
@Override
public Produk mapRow(ResultSet rs, int i) throws SQLException {}
}
}
PenjualanDao
package com.muhardin.endy.training.java.aksesdb.dao.springjdbc;
// import generate dari IDE
@Repository
public class PenjualanDao {
public void simpan(Penjualan p) {}
public Penjualan cariById(Integer id) {}
public Long hitungByPeriode(Date mulai, Date sampai) {}
public List<Penjualan> cariByPeriode(Date mulai, Date sampai, Integer halaman, Integer baris) {}
private class ResultSetJadiPenjualan implements RowMapper<Penjualan> {
@Override
public Penjualan mapRow(ResultSet rs, int i) throws SQLException {}
}
}
PenjualanDetailDao
package com.muhardin.endy.training.java.aksesdb.dao.springjdbc;
// import generate dari IDE
@Repository
public class PenjualanDetailDao {
public void simpan(final PenjualanDetail p) {}
public List<PenjualanDetail> cariByPenjualan(Penjualan p) {}
public Long hitungByProdukDanPeriode(Produk p, Date mulai, Date sampai) {}
public List<PenjualanDetail> cariByProdukDanPeriode(Produk p, Date mulai, Date sampai, Integer halaman, Integer baris) {}
private class ResultSetJadiPenjualanDetail implements RowMapper<PenjualanDetail> {
@Override
public PenjualanDetail mapRow(ResultSet rs, int i) throws SQLException {}
}
}
Automated Test
Seperti dijelaskan di atas, automated test kita bagi menjadi dua kategori, yaitu abstract class yang menampung semua logika pengetesan, dan concrete class yang menyediakan konfigurasi.
Abstract Base Class
ProdukServiceTest
package com.muhardin.endy.training.java.aksesdb.service;
// import generate dari IDE
public abstract class ProdukServiceTest {
public abstract PenjualanService getPenjualanService();
public abstract DataSource getDataSource();
@Before
public void bersihkanDataTest() throws Exception {}
@Test
public void testSimpanProduk() {}
@Test
public void testCariProdukById() {}
@Test
public void testCariProdukByKode() {}
@Test
public void testHitungSemuaProduk() {}
@Test
public void testCariSemuaProduk() {}
@Test
public void testHitungProdukByNama() {}
@Test
public void testCariProdukByNama() {}
}
PenjualanServiceTest
package com.muhardin.endy.training.java.aksesdb.service;
// import generate dari IDE
public abstract class PenjualanServiceTest {
public abstract PenjualanService getPenjualanService();
public abstract DataSource getDataSource();
@Before
public void bersihkanDataTest() throws Exception {}
@Test
public void testSimpanPenjualan() throws Exception {}
@Test
public void testCariPenjualanById(){}
@Test
public void testHitungPenjualanByPeriode(){}
@Test
public void testCariPenjualanByPeriode(){}
@Test
public void testHitungPenjualanDetailByProdukDanPeriode(){}
@Test
public void testCariPenjualanDetailByProdukDanPeriode(){}
}
Seperti kita lihat di atas, kedua class tersebut memiliki dua abstract method, yaitu:
public abstract PenjualanService getPenjualanService()
public abstract DataSource getDataSource();
Kedua object PenjualanService
dan DataSource
didapatkan dari konfigurasi Spring.
Konfigurasi Spring dibuat berdasarkan teknologi yang digunakan.
Konfigurasi Spring JDBC berbeda dengan konfigurasi Hibernate ataupun Spring Data JPA.
Dengan teknik ini, bila di kemudian hari kita membuat implementasi dengan Hibernate atau Spring Data JPA, kita tidak perlu lagi copy-paste test class, cukup buat subclass yang menyediakan kedua object tersebut.
Berikut adalah subclassnya
Implementasi Test Business Service
Karena hanya beberapa baris dan tidak butuh banyak penjelasan, kita tampilkan di sini full source code, bukan hanya kerangkanya saja.
ProdukServiceSpringJdbcTest
package com.muhardin.endy.training.java.aksesdb.service.springjdbc;
// import generate dari IDE
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath*:com/muhardin/**/spring-jdbc-ctx.xml")
public class ProdukServiceSpringJdbcTest extends ProdukServiceTest {
@Autowired private DataSource dataSource;
@Autowired private PenjualanService penjualanService;
@Override
public PenjualanService getPenjualanService() {
return penjualanService;
}
@Override
public DataSource getDataSource() {
return dataSource;
}
}
PenjualanServiceSpringJdbcTest
package com.muhardin.endy.training.java.aksesdb.service.springjdbc;
// import generate dari IDE
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath*:com/muhardin/**/spring-jdbc-ctx.xml")
public class PenjualanServiceSpringJdbcTest extends PenjualanServiceTest {
@Autowired private DataSource dataSource;
@Autowired private PenjualanService penjualanService;
@Override
public PenjualanService getPenjualanService() {
return penjualanService;
}
@Override
public DataSource getDataSource() {
return dataSource;
}
}
Seperti kita lihat di atas, dataSource
dan penjualanService
disediakan melalui Dependency Injection.
Cara kerjanya sebagai berikut:
- Kita jalankan JUnit melalui IDE atau Maven. IDE atau Maven akan membaca semua file dalam folder
src/test/java
dan memproses semua class yang namanya berakhiranTest
sepertiPenjualanServiceSpringJdbcTest
. IDE/Maven juga memprosesPenjualanServiceTest
, tapi karena classnya abstract maka tidak diproses lebih lanjut. - JUnit melihat annotation
@RunWith
, jadi dia tidak menjalankan sendiri melainkan menyuruhSpringJUnit4ClassRunner
untuk menjalankan test SpringJUnit4ClassRunner
membaca annotation@ContextConfiguration
, lalu menggunakan nilai di dalamnya untuk melakukan inisialisasiApplicationContext
, kemudian mengisi variabel yang ditandai dengan@Autowired
- Karena
PenjualanServiceSpringJdbcTest
merupakan subclass dariPenjualanServiceTest
, maka dia akan mewarisi semua method@Test
yang dimilikiPenjualanServiceTest
. Method@Test
ini akan dijalankan oleh IDE/Maven. - Pada waktu method
@Test
dijalankan, bila perlu objectPenjualanService
, maka akan didapat dengan cara memanggil methodgetPenjualanService
. Karena methodnya sudah dibuatkan implementasinya (tidak abstract lagi) dan sudah ada isinya, maka method@Test
dapat bekerja dengan baik.
Demikianlah bagian kedua dari tutorial mengakses database menggunakan Spring JDBC. Pada bagian ini kita sudah melihat bagaimana cara pengaturan file/folder dan interaksi antar class/method. Di bagian selanjutnya kita akan lihat bagaimana cara menjalankan perintah SQL.