Epilog ~ Kata Penutup ~

Setelah lama tidak mengunjungi situs blog sendiri, saya merasa ada yang perlu berubah. Salah satu contohnya adalah ukuran font yang terlalu kecil; seiring waktu berlalu, saya kini menggunakan monitor dengan resolusi yang lebih tinggi sehingga tulisan yang kecil semakin sulit dibaca. Berjalan pada plan gratis di WordPress.com, saya sama sekali tidak bisa melakukan modifikasi seperti perubahan CSS, menambahkan sedikit JavaScript dan sebagainya. Untuk melakukan itu semua, saya perlu beralih ke plan Premium atau Business. Oleh sebab itu, saya memutuskan untuk beralih ke penyedia baru yang memungkinkan saya berkreasi secara bebas.

Ingin fleksibilitas? Saya bisa menyewa server cloud seperti DigitalOcean, Linode, Heroku, AWS EC2 ataupun Google Compute Engine. Tapi semua itu tidak gratis (walaupun ada trial-nya)! Setelah memutar otak cukup lama, akhirnya saya memutuskan untuk menggunakan Github Pages. Walaupun hanya bisa melakukan hosting situs statis, Github Pages sudah mendukung HTTPS dan custom domain tanpa biaya sepeserpun. Saya pun segera membuat repository baru di GitHub yang dipergunakan sebagai source untuk situs yang dihasilkan.

Tidak dipungkiri memakai Jekyll untuk Github Pages tidak semudah di WordPress.com. Saya harus beradaptasi dengan struktur proyek dan format template yang dipakai.
Sesaat setelah menyiapkan proyek Jekyll, saya harus memilih salah satu themes untuk dipakai. Pilihannya tentu saja tidak sebanyak di WordPress.com. Namun, saya bisa melakukan kustomisasi sepuasnya bila ada bagian yang ingin saya ubah; cukup dengan dengan men-copy file di folder _layouts atau _includes milik theme ke proyek. Untuk sebuah situs yang berisi banyak tulisan untuk dibaca dalam jangka waktu lama, saya membutuhkan sebuah design yang sederhana tanpa banyak gangguan. Animasi indah dan gambar warna warni mungkin cocok untuk landing page yang berfungsi untuk menarik perhatian; namun sebagai pembaca yang sering menghabiskan waktu berjam-jam membaca dokumentasi, saya tahu betapa pentingnya menjaga agar pikiran bisa tetap konsentrasi pada tulisan 🙂 Oleh sebab itu, saya memakai theme bawaan, jekyll/minima. Saya hanya menambahkan Bootstrap 4.1 dan melakukan perubahan secukupnya. Hasil akhirnya dapat dilihat di https://blog.jocki.me.

Jekyll secara bawaan sudah memiliki fasilitas category dan tag. Tapi tidak lebih dari itu: ada beberapa fitur di situs WordPress.com yang harus dikorbankan. Salah satu contohnya adalah pencarian; perlu diingat bahwa Jekyll menghasilkan halaman statis dan tidak ada database untuk di-query. Salah satu solusinya adalah melakukan indexing saat Jekyll menghasilkan situs, kemudian ikut meng-upload index tersebut ke Github Pages, dan lakukan pencarian lewat JavaScript. Contoh implementasinya bisa dilihat di https://github.com/slashdotdash/jekyll-lunr-js-search. Catatan: Github Pages hanya mendukung plugin Jekyll tertentu saja! Karena tidak ingin terlalu repot, saya akhirnya memutuskan untuk menggunakan layanan Algolia pada plan bebas biaya (gratis). Untuk situs kecil yang jarang dikunjungi, saya pikir ini akan cukup. Cara saya menggunakan Algolia dan hasil akhirnya bisa dilihat di https://blog.jocki.me/pemograman/2018/05/18/memakai-algolia-untuk-fitur-pencarian-di-jekyll.

Karena sering menulis dengan topik yang berbeda-beda, saya sangat suka melihat tag cloud di blog ini untuk mengingatkan saya topik yang mana yang sudah terlalu sering dibahas dan mana yang jarang dibahas. Jekyll tidak menyediakan fasilitas serupa, tapi saya bisa menggunakan Highcharts untuk keperluan tersebut, seperti yang terlihat di https://blog.jocki.me/about. Fasilitas lain seperti arsip per bulan dan top posts terpaksa saya abaikan karena tidak ada plugin siap pakai yang kompatibel dengan Github Pages untuk mengimplementasikannya.

Sebenarnya theme jekyll/minima sudah menyediakan integrasi ke Disqus untuk komentar dan Twitter agar pengguna bisa men-follow. Akan tetapi saya tidak mengaktifkannya karena tujuan utama situs tersebut adalah sebagai jurnal pribadi. Saya adalah seorang yang pelupa; mencatat apa yang pernah saya lakukan akan membantu saya merekam aktifitas ke otak 🙂 Semua situs blog saya tidak pantas digunakan untuk referensi karena saya tidak pernah melakukan revisi pada artikel yang sudah diterbitkan.

Saya juga tidak sedang mempengaruhi pembaca untuk mengikuti metode saya. Ini adalah alasan mengapa saya jarang menggunakan kata “kamu” di blog saya. Preferensi dan metode pemograman seorang programmer selalu berubah sesuai dengan perkembangan zaman dan pengalaman programmer tersebut. Sebagai contoh, dulu saya senang mengembangkan aplikasi monolithic dengan Java Enterprise Edition. Tapi sekarang, saya lebih memilih microservices dan event driven. Mengapa preferensi saya berubah? Kebutuhan kinerja, dukungan infrastruktur cloud functions / lambda yang sederhana, serta kemampuan sisi klien yang semakin canggih (bandingkan Angular 6 dengan Backbone.js atau Knockout.js). Ada juga bagian yang belum berubah: misalnya, saya tetap percaya database relasional adalah yang terbaik untuk menyimpan domain classes untuk aplikasi enterprise. Tapi, sebagai alternatif, saya percaya bahwa menggabungkannya dengan database lain akan membuat database SQL semakin berguna. Misalnya untuk pencarian produk, saya menggunakan ElasticSearch dan untuk komentar dan audit log, saya menggunakan MongoDB. Dengan bantuan message queue yang baik, komunikasi antar database berbeda bukan merupakan hambatan melainkan jalan keluar untuk menghasilkan aplikasi yang memiliki kinerja sangat baik. Sepuluh tahun yang lalu saat saya masih berkutat dengan Oracle Database, saya sama sekali tidak membayangkan arsitektur seperti sekarang. Dengan teknologi dan pengalaman sepuluh tahun yang lalu, apa yang saya lakukan sekarang adalah sesuatu yang akan saya hindari saat itu.

Sekarang, setelah men-deploy situs blog yang baru, saya bisa bebas menambahkan JavaScript dan CSS. Sebagai contoh, pada https://blog.jocki.me/pemograman/2018/05/27/query-dns-lewat-https.html, saya membuat halaman yang memanggil Cloudflare DNS melalui HTTPS untuk men-resolve nama domain menjadi IP address. Halaman seperti ini tidak mungkin bisa saya buat bila tetap menggunakan WordPress.com pada plan gratis, karena WordPress.com hanya memungkinkan membuat artikel yang berisi tulisan, gambar dan video.

Sebagai penutup, karena ini adalah artikel terakhir pada blog ini, saya mencantumkan statistik halaman untuk situs ini pada saat tulisan ini dibuat:

Kunjungan Per Tahun

Kunjungan Per Tahun

Tabel Kunjungan Per Tahun

Tabel Kunjungan Per Tahun

Statistik Per Tahun

Statistik Per Tahun

Artikel Yang Populer

Artikel Yang Populer

Kunjungan Per Negara

Kunjungan Per Negara

Menambah atau mengubah endpoint yang dihasilkan Spring Data Rest

Pada artikel Membuat Restful Web Service Dengan Spring Data REST, saya menghasilkan REST API secara otomatis berdasarkan domain model dan Spring Data Repository. API yang dihasilkan pun telah mendukung HATEOAS dengan implementasi HAL. Dengan Spring Data REST, saya tidak perlu lagi menulis kode program controller dan Spring HATEOAS secara manual untuk setiap domain model yang ada.

Sebagai latihan, kali ini saya akan memakai Spring Data REST melalui Spring Boot. Pada saat membuat proyek baru, saya menambahkan dependency berupa Web, Rest Repositories dan JPA. Setelah proyek selesai dibuat, saya menambahkan entity baru seperti berikut ini:

package com.example.demo.domain;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotBlank;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Pelanggan {

    @Id
    @GeneratedValue
    private Long id;

    @NotBlank @Email
    private String email;

    @NotBlank
    private String nama;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getNama() {
        return nama;
    }

    public void setNama(String nama) {
        this.nama = nama;
    }
}

Setelah itu, saya membuat sebuah repository untuk mengakses dan melakukan operasi CRUD terhadap Pelanggan seperti berikut ini:

package com.example.demo.repository;

import com.example.demo.domain.Pelanggan;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PelangganRepository extends JpaRepository<Pelanggan, Long> {
}

Sebagai langkah terakhir, saya akan menggunakan embedded database pada latihan kali ini. Untuk itu, saya menambahkan baris berikut ini pada file build.gradle:

...
dependencies {
  compile('com.h2database:h2:1.4.196')
  ...
}

Sekarang, bila saya menjalankan aplikasi Spring Boot ini, sesuai dengan spesifikasi HATEOAS, saya bisa mengakses http://localhost:8080/ untuk melihat daftar endpoints yang tersedia seperti yang terlihat pada gambar berikut ini:

Daftar endpoints yang tersedia

Daftar endpoints yang tersedia

Saya bisa melakukan operasi CRUD terhadap pelanggan melalui URI http://localhost:8080/pelanggans. Spring Data REST menggunakan nama plural (pelanggan menjadi pelanggans) sesuai spesifikasi REST, walaupun hal ini sebenarnya tidak relevan untuk Bahasa Indonesia. Selain itu, API yang dihasilkan juga mendukung operasi halaman (pagination) dan pengurutan (sorting).

Saya akan mencoba menyimpan pelanggan baru dengan memberikan request POST ke http://localhost:8080/pelanggans berisi JSON berikut ini (pastikan menyertakan Content-Type berupa application/json):

{
  "email": "phantom@diamondogs.pain",
  "nama": "Venom"
}

Saya akan memperoleh kembalian seperti pada gambar berikut ini:

JSON yang dikembalikan saat menyimpan pelanggan baru

JSON yang dikembalikan saat menyimpan pelanggan baru

Sesuai dengan spesifikasi HAL, JSON yang dikembalikan mengandung _links yang berisi referensi ke operasi berikutnya yang bisa dilakukan terhadap entity ini. Sampai disini, entity juga sudah tersimpan ke database.

Bayangkan bila saya harus melakukan langkah di atas secara manual. Saya perlu membuat sebuah controller baru. Selain itu, untuk tetap mendukung HATEOAS, saya perlu memakai Spring HATEOAS dan mendaftarkan _links secara manual. Cukup merepotkan, bukan?

Walaupung Spring Data REST sangat membantu, ada kalanya saya perlu menyediakan operasi lain selain CRUD yang belum disediakan oleh Spring Data REST secara otomatis. Selain itu saya juga mungkin perlu memodifikasi operasi yang tersedia. Sebagai contoh, saya umumnya perlu mengirim email ke pelanggan setelah menyimpannya ke database. Beruntungnya, Spring Data REST tetap memungkinkan saya untuk mengubah operasi yang sudah ada.

Untuk itu, saya perlu membuat controller baru seperti berikut ini:

package com.example.demo.web;

import com.example.demo.domain.Pelanggan;
import com.example.demo.repository.PelangganRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.hateoas.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.mail.Address;
import javax.mail.internet.MimeMessage;

@RepositoryRestController
public class PelangganController {

    private final PelangganRepository pelangganRepository;
    private final JavaMailSender mailSender;

    @Autowired
    public PelangganController(PelangganRepository pelangganRepository, JavaMailSender mailSender) {
        this.pelangganRepository = pelangganRepository;
        this.mailSender = mailSender;
    }

    @RequestMapping(method = RequestMethod.POST, value = "/pelanggans")
    public @ResponseBody ResponseEntity<?> save(@RequestBody Resource<Pelanggan> pelangganResource, PersistentEntityResourceAssembler assembler) {
        // Menyimpan pelanggan
        Pelanggan pelanggan = pelangganResource.getContent();
        pelanggan = pelangganRepository.save(pelanggan);

        // Mengirim email
        SimpleMailMessage email = new SimpleMailMessage();
        email.setTo(pelanggan.getEmail());
        email.setSubject("Selamat datang");
        email.setText(String.format("Hi, selamatt datang, %s", pelanggan.getNama()));
        mailSender.send(email);

        // Mempersiapkan kembalian
        PersistentEntityResource resource = assembler.toResource(pelanggan);
        return ResponseEntity.ok(resource);
    }
}

Kali ini, controller yang saya buat tidak menggunakan annotation @Controller melainkan @RepositoryRestController yang menandakan bahwa saya ingin memodifikasi hasil dari Spring Data REST. Method di controller saya akan menerima request dalam bentuk Resource dan juga mengembalikan resource agar mendukung HATEOAS. Agar tidak repot menghasilkan _links dengan Spring HATEOAS, saya cukup menggunakan PersistentEntityResourceAssembler untuk menghasilkan resource yang sama persis seperti yang dihasilkan oleh Spring Data REST sebelum dimodifikasi.

Menghindari CORS saat memakai Spring Boot bersama dengan Webpack

Pada artikel Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama, saya membuat proyek baru yang menggabungkan back end dan front end. Untuk menjalankan back end, saya memberikan perintah ./gradlew bootRun yang akan membuat instance server Tomcat baru. Saya bisa meng-eksekusi end points milik back end di http://localhost:8080/api. Setiap kali kode back end berubah, server ini akan diperbaharui. Sementara itu, untuk menjalankan front end, saya memberikan perintah yarn dev yang juga akan menjalankan server baru di http://localhost:9090. Setiap kali kode front end berubah, server ini akan diperbaharui.

Apa yang salah bila server front end berbeda dengan back end? Hampir semua browser modern sudah mendukung fasilitas Cross-origin Resource Sharing (CORS) yang akan memblokir akses end points milik front end dari domain berbeda. Tentunya saya tidak ingin setiap API milik back end saya bisa diakses oleh front end milik orang lain sesuka hati. Akan tetapi, saat mengembangkan kode program, server front end dan back end adalah dua server berbeda. Fasilitas CORS akan membuat front end saya gagal memanggil back end. Memang Spring Boot memiliki annotation @CrossOrigin yang bisa membuat back end saya dipakai semua orang sesuka hati sehingga front end saya yang berada di server berbeda bisa memanggilnya. Akan tetapi, ini membuat saya merasa kurang aman.

Apakah ada solusi lain? Yup! Saya beruntung karena template webpack yang dipakai oleh vue-cli sudah memperhitungkan hal ini. Bila saya memperhatikan isi file dev-server.js yang dijalankan oleh yarn dev, terlihat bahwa http-proxy-middleware sudah disertakan. Proyek ini pada dasarnya adalah sebuah HTTP proxy (memakai http-proxy) berbasis JavaScript. Dengan http-proxy-middleware, saya ingin pada saat front end mengakses http://localhost:9090/api, secara transparan, ia akan memanggil http://localhost:8080/api. Karena proxy ini transparan dari sisi browser, saya tidak akan menemukan masalah CORS lagi.

Untuk itu, saya akan mengubah file config/index.js pada bagian dev.proxyTable sehingga terlihat seperti berikut ini:

module.exports = {
  ...
  dev: {
    ...
    proxyTable: {
      '/api': 'http://localhost:8080'
    }
  }
}

Pada konfigurasi di atas, setiap kali saya mengakses url yang diawali oleh /api di server front end seperti http://localhost:9090/api/snakes, akses tersebut akan diteruskan ke server back end di http://localhost:8080/api/snakes secara transparan. Dengan demikian, saya tidak akan mendapatkan kesalahan CORS lagi.

Memakai Bootstrap bersama dengan Vue.js dan Webpack

Pada artikel Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama, saya membuat sebuah website kosong yang menggunakan Vue.js dan berbasis Webpack. Salah satu hal menarik yang saya sukai dari Vue.js adalah setiap komponen (single file component) memiliki deklarasi CSS-nya masing-masing. Agar CSS hanya berlaku untuk sebuah komponen (dan juga komponen lain yang dikandunginya), saya tinggal menambahkan <style scoped>. Selain itu, saya juga bisa menggunakan CSS Module dengan menggunakan <style module>.

Walaupun deklarasi CSS per komponen adalah hal yang baik, akan tetapi, ada saatnya saya juga membutuhkan CSS yang global untuk seluruh bagian di website. Sebagai contoh, akan sulit sekali bagi saya untuk menulis ulang CSS seperti Bootstrap grid di setiap komponen. Bagaimana cara menambahkan Bootstrap pada proyek Vue.js yang berbasis Webpack?

Satu hal yang perlu saya pertimbangkan adalah komponen JavaScript di Bootstrap masih menggunakan jQuery sementara filosofi Vue.js yang reaktif membuat saya ingin menghindari jQuery sebisa mungkin. Beruntungnya, saya menemukan https://bootstrap-vue.js.org yang menyediakan Bootstrap 4 dimana komponen-nya telah ditulis ulang sebagai komponen Vue.js yang tidak menggunakan jQuery. Untuk menggunakan library tersebut, saya segera menambahkan baris berikut ini pada package.json:

"dependencies": {
    ...
    "bootstrap-vue": "^0.18.0"
},

Library ini akan menyertakan bootstrap@^4.0.0-alpha.6 yang berisi Bootstrap 4. Mengapa tidak mengubah file index.html dan menambahkan baris seperti <link rel="stylesheet" href="..."> dan <script src="...">? Salah satu alasannya adalah akan lebih konsisten bila seluruh dependensi proyek diletakkan pada file package.json. Bila saya ingin memakai versi Bootstrap yang berbeda, misalnya, saya hanya perlu memperbaharui package.json tanpa mengubah index.html.

Berikutnya, saya menambahkan baris berikut ini pada awal file main.js:

import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Proyek Webpack yang dihasilkan oleh vue-cli sudah dilengkapi konfigurasi yang memakai css-loader (lihat file buildScripts\util.js) sehingga saya bisa men-import file css di dalam kode program JavaScript.

Berikutnya, saya perlu mengaktifkan bootstrap-vue dengan menambahkan kode program berikut ini di main.js:

Vue.use(BootstrapVue)

Sekarang, bila saya menjalankan website ini dengan yarn run dev, saya akan menemukan bahwa CSS Bootstrap (dan juga CSS lain yang saya import dikemudian hari) akan diletakkan secara inline ke dalam tag <style> seperti yang terlihat pada gambar berikut ini:

Inline CSS pada saat `run dev`

Inline CSS pada saat `run dev`

Walaupun CSS inline lebih sederhana, ia tidak disarankan untuk dipakai di lingkungan produksi. Beruntungnya, proyek Webpack yang dihasilkan oleh vue-cli sudah memperhitungkan hal ini. Pada file buildScripts\util.js, terlihat bahwa tersedia option extract yang akan menggunakan plugin extract-text-webpack-plugin untuk menghasilkan file CSS terpisah. Seperti yang bisa ditebak, extract akan bernilai true bila perintah ini dikerjakan pada lingkungan produksi (lihat file buildScripts/vue-loader.conf.js). Untuk membuktikannya, saya akan menjalankan perintah yarn run build. Output dari perintah ini dapat dijumpai di folder dist. Kali ini, CSS akan diletakkan pada file terpisah seperti yang terlihat pada gambar berikut ini:

File CSS terpisah saat memakai `yarn run build`

File CSS terpisah saat memakai `yarn run build`

Memperbaharui Output Webpack Di Browser Secara Otomatis

Pada artikel Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama, saya membuat proyek Spring Boot yang menggunakan Webpack di front end. Webpack adalah bundler yang juga berfungsi melakukan transformasi konten. Sebagai contoh, Webpack akan melakukan transpiling kode program ES2016 saya ke dalam kode program JavaScript ‘lama’ yang bisa dimengerti semua browser berkat babel-loader. Selain itu, saya bisa menulis style dalam format Scss (Sassy CSS) yang kemudian akan diterjemahkan oleh sass-loader menjadi CSS biasa yang bisa dimengerti oleh semua browser. Semua hasil dari Webpack ini akan disimpan kedalam sebuah Jar.

Apa yang terjadi bila saya menjalankan Spring Boot dengan gradlew bootRun lalu mengubah kode program HTML atau JavaScript yang ada? Hasil perubahan ini tidak akan terlihat pada browser! Mengapa demikian? Karena gradlew bootRun hanya memanggil Webpack pada saat pertama kali dijalankan. Saat saya mengubah kode program di HTML atau JavaScript, Webpack tidak akan dipanggil. Hal ini tentu akan sangat merepotkan karena saya perlu menjalankan gradlew bootRun berkali-kali setiap kali melakukan perubahan di front end.

Beruntungnya, konfigurasi Webpack yang dihasilkan vue-cli sudah mendukung live reload. Template yang saya pakai memiliki script buildScripts/dev-server.js yang menggunakan fasilitas dari webpack-dev-middleware untuk menghasilkan live reload. Ini hampir sama seperti apa yang dilakukan oleh webpack-dev-server. Bila saya membuka file package.json, terlihat bahwa scripts dev atau start akan melakukan hal yang sama yaitu mengerjakan buildScripts/dev-server.js.

Agar bisa memanggil script ini secara mudah, saya bisa menambahkan kode program berikut ini pada build.gradle (milik proyek front end):

...
task runDev(dependsOn: yarn, type: YarnTask) {
    args = ['run', 'dev']
}
...

Sekarang, saya bisa menjalankan proyek front end (tanpa harus menjalankan Spring Boot) dengan menggunakan perintah berikut ini:

$ ./gradlew runDev
:frontend:nodeSetup UP-TO-DATE
:frontend:yarnSetup UP-TO-DATE
:frontend:yarn UP-TO-DATE
:frontend:runDev
yarn run v0.27.5
$ node buildScripts/dev-server.js
> Starting dev server...
 DONE  Compiled successfully in 1904ms00:08:40

> Listening at http://localhost:8080

 75% EXECUTING
> :frontend:runDev

Perintah di atas akan membuka browser yang menampilkan halaman front end saya. Sekarang, bila saya melakukan perubahan pada HTML atau JavaScript, halaman tersebut akan langsung diperbaharui secara otomatis. Saya juga bisa menemukan output seperti berikut ini di console:

 WAIT  Compiling...00:00:12

 DONE  Compiled successfully in 154ms00:00:13

Sekarang, saya bisa mengerjakan front end dan melihat hasilnya secara cepat.

Bagaimana bila saya ingin menjalankan Spring Boot dan dev-server.js secara bersamaan? Karena keduanya sama-sama menggunakan port 8080, salah satu akan gagal. Untuk itu, saya akan menggubah port yang dipakai oleh dev-server.js menjadi 9090 dengan mengubah port di config/index.js.

Belajar menilai seberapa sempurna rancangan REST API

Membuat API berbasis REST pada dasarnya adalah sesuatu yang sangat mudah: buat sebuah method di controller dan kembalikan hasil berupa JSON. Selesai! Yup, cukup sampai disini bagi programmer yang sedang belajar atau membuat proyek jangka pendek yang tidak ingin berkembang. Pada tulisan kali ini, saya akan melihat dari sisi arsitektur, dimana REST API tersebut akan dipakai dalam jangka lama dan mungkin akan dipanggil oleh klien baru yang belum terpikirkan saat ini.

Salah satu cara untuk menilai implementasi REST API adalah dengan membandingkannya dengan Richardson Maturity Model. Gambar berikut ini memperlihatkan perkembangan REST menurut model tersebut:

Richardson Maturity Model

Richardson Maturity Model

Pada Level 0, REST API dianggap sebagai API jarak jauh yang dipanggil melalui Web (Http) dan mengembalikan JSON. Tidak lebih dari sekedar pengganti function atau method (yang dikerjakan jarak jauh)! Sebagai contoh, API berikut ini masuk dalam kategori Level 0:

POST http://api.server.com/buatProduk
{
   nama: "produk1",
   harga: 1000
}

POST http://api.server.com/hapusProduk
{
   id: 1
}

POST http://api.server.com/daftarPelanggan
{
   email: "ocelat@dd.com",
   nama: "ocelot"
}

POST http://api.server.com/hapusPelanggan
{
   id: 1
}

Pada Level 1, terdapat konsep resource. Setiap API merupakan operasi yang akan melakukan sesuatu pada resource yang ada. Sebagai contoh, API berikut ini termasuk dalam kategori Level 1:

POST http://api.server.com/produk/buat
{
   nama: "produk1",
   harga: 1000
}

POST http://api.server.com/produk/hapus
{
   id: 1
}

POST http://api.server.com/pelanggan/daftar
{
   email: "ocelat@dd.com",
   nama: "ocelot"
}

POST http://api.server.com/pelanggan/hapus
{
   id: 1
}

Pada contoh di atas, setiap API masih dipanggil dengan request POST yang populer dari dulu. Biasanya, GET dipakai bila kita tidak ingin mengirim data tambahan seperti JSON. Bila ingin mengirimkan JSON atau mengirim data dalam jumlah besar, POST akan digunakan. Padahal selain GET dan POST, HTTP juga memiliki request method lainnya seperti HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT, dan PATCH.

Sebuah request yang sukses akan mengembalikan respon 200. Selain respon 200, HTTP juga memiliki definisi response status code yang lain seperti 500 untuk menandakan kesalahan, 201 untuk menandakan resource berhasil dibuat, dan sebagainya.

Pada Level 2, REST API akan memanfaatkan HTTP request method dan response status code. Sebagai contoh, API berikut ini masuk dalam kategori Level 2:

POST http://api.server.com/produk
{
   nama: "produk1",
   harga: 1000
}

PUT http://api.server.com/produk/1
{
   nama: "produk1 dengan nama baru",
   harga: 2000
}

DELETE http://api.server.com/produk/1

POST http://api.server.com/pelanggan
{
   email: "ocelat@dd.com",
   nama: "ocelot"
}

DELETE http://api.server.com/pelanggan/1

Pada Level 3, REST API akan menggunakan HATEOAS. Sebagai contoh, saya membuat REST API yang mendukung HATEOAS di artikel Membuat RESTFul Web Service Dengan Spring Data REST. Kenapa memakai HATEOAS? Beberapa keuntungan yang ditawarkannya antara lain:

  1. Klien tidak perlu men-hardcode link untuk melakukan operasi terhadap resource yang diterima.  Dengan demikian, perubahan nama atau lokasi API tidak menimbulkan perubahan di kode program klien.
  2. Klien dapat mengetahui operasi apa yang diperbolehkan dan tidak diperbolehkan terhadap resource.   Sebagai contoh, pada UI yang berisi daftar produk, bila tidak ada link untuk menghapus produk yang dikembalikan bersamaan dengan produk tersebut (misalnya karena user yang login tidak berhak untuk operasi tersebut), maka saya bisa menyembunyikan tombol hapus.

Sayangnya, hingga saat ini tidak ada implementasi yang standar untuk HATEOAS.  Salah satu yang menjanjikan adalah HAL (Hypertext Application Language).  Daftar library yang mengimplementasikan HAL dapat dijumpai di https://github.com/mikekelly/hal_specification/wiki/Libraries.

REST API dari Paypal dan Netflix adalah contoh API yang sudah mendukung HATEOAS.  Sementara itu, kebanyakan penyedia REST API publik lainnya hanya berada pada Level 2 karena mereka berfokus pada kemudahan penggunaan (sementara HATEOAS tidak begitu populer).   Perlu diingat ini bahwa mereka adalah API publik yang ditujukan untuk dikonsumsi oleh developer luar dari berbagai kalangan.  Fokus ini tentu akan berbeda, misalnya, pada saat merancang arsitektur microservices untuk dipakai dalam perusahaan.

Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama

Salah satu pertanyaan yang sering muncul pada saat akan memulai proyek baru dari awal adalah apakah kode program front end dan back end harus dipisahkan menjadi dua proyek yang berbeda? Proyek front end umumnya adalah proyek berbasis JavaScript yang menggunakan framework seperti Angular, Vue.js, React, dan sebagainya. Build tool yang umumnya digunakan oleh proyek front end meliputi Grunt, Yarn, Webpack, npm, dan sejenisnya. Sementara itu, proyek back end umumnya ditulis dalam Java, PHP, C#, dan sebagainya. Build tool yang umum meliputi Gradle, Ant, Composer, CMake, dan sebagainya. Karena dunia keduanya yang berbeda jauh, apakah pemisahan proyek secara fisik selalu merupakan pilihan yang tepat?

Tidak selamanya front end dan back end harus dipisahkan menjadi dua proyek berbeda. Bila anggota tim lebih banyak yang full stack, pemisahan proyek justru malah akan merepotkan. Metodologi pengembangan software seperti Scrum, misalnya, memiliki unit pekerjaan berupa story seperti “pelanggan bisa mendaftar baru” dan “pelanggan bisa melihat laporan laba rugi per tahun”. Agar bisa menyelesaikan story yang sedang dikerjakannya, developer umumnya harus mengubah front end dan juga back end.

Beberapa framework yang dogmatis mempermudah penggabungan kode program front end dan back end pada satu proyek yang sama. Sebagai contoh, Laravel 5.4 yang dilengkapi dengan Laravel Mix (berbasis Webpack) sangat mempermudah penggunaan front end Vue.js di dalam satu proyek Laravel yang sama. Lalu bagaimana dengan framework di dunia Java seperti Spring Boot? Walaupun tidak semudah di Laravel, saya akan mencoba membuat proyek Spring Boot (back end) + Webpack + Vue.js (front end) pada artikel ini.

Saya akan mulai dengan membuat sebuah proyek Spring Boot baru melalui Spring Initializr (https://start.spring.io). Setelah menjalankan IntelliJ IDEA, saya segera memilih New, Project…. Setelah memilih Spring Initializr, saya men-klik tombol Next. Karena saya ingin menggunakan Gradle, saya mengisi dialog yang muncul seperti yang terlihat pada gambar berikut ini:

Membuat Proyek Spring Boot Baru

Membuat Proyek Spring Boot Baru

Pada langkah berikutnya, saya memilih dependency yang diperlukan. Karena ini merupakan sebuah percobaan, saya hanya memberikan tanda centang pada Web. Setelah men-klik Next, saya bisa mengisi nama proyek dan lokasi penyimpanannya. Setelah men-klik Finish, IDEA akan men-download proyek dan menampilkan sebuah proyek Spring Boot yang masih kosong seperti yang terlihat pada gambar berikut ini:

Struktur Proyek Spring Boot Yang Baru Dibuat

Struktur Proyek Spring Boot Yang Baru Dibuat

Seandainya saja saya menggunakan front end yang ramah terhadap Java seperti Thymeleaf dan JSF, maka saya hanya perlu meletakkan kode program mereka ke direktori /src/main/webapp. Akan tetapi, ada kalanya saya perlu menggunakan front end yang lebih berat seperti Angular dan Vue.js. Keuntungannya adalah saya bisa memanfaatkan teknologi yang umum dipakai programmer front end seperti npm, Yarn, dan Webpack. Walaupun pada dunia Java sudah ada upaya menjadikan library front end ke dalam apa yang disebut sebagai Webjars, saya tetap merasa penggunaan npm lebih alami bagi pencinta JavaScript.

Bagaimana cara memasukkan proyek Vue.js ke dalam proyek Spring Boot yang baru saja dibuat ini? Cara yang paling adalah dengan menggunakan Vue Cli (https://github.com/vuejs/vue-cli). Jika ini adalah pertamakalinya saya menggunakan Vue Cli, saya perlu men-install-nya dengan memberikan perintah berikut ini:

$ npm install -g vue-cli

Setelah itu, masih berada di direktori proyek Spring Boot, saya memberikan perintah berikut ini:

$ vue init webpack frontend

Vue Cli akan menghasilkan sebuah proyek kosong untuk Vue.js berbasis Webpack seperti yang terlihat pada gambar berikut ini:

Proyek Vue.js di dalam proyek Spring Boot

Proyek Vue.js di dalam proyek Spring Boot

Perlu diperhatikan bahwa frontend hanyalah sebuah folder biasa. Sampai disini, saya bisa saja mulai bekerja dengan Vue.js dan memberikan perintah npm pada saat berada dalam folder frontend. Walaupun demikian, akan lebih baik bila saya bisa sama-sama mengendalikan front end dan back end melalui Gradle. Berhubung Gradle mendukung multi-projects, saya akan menjadikan folder frontend sebagai sebuah sub-project.

Langkah pertama yang saya lakukan adalah membuat sebuah file baru bernama build.gradle di folder frontend dengan isi seperti berikut ini:

plugins {
  id 'com.moowork.node' version '1.2.0'
  id 'java'
}

repositories {
  mavenCentral()
}

node {
  version = '8.1.3'
  yarnVersion = '0.27.5'
  download = true
}

task runBuild(dependsOn: yarn, type: YarnTask) {
  args = ['run', 'build']
}

task runDev(dependsOn: yarn, type: YarnTask) {
  args = ['run', 'dev']
}

jar {
  from project.projectDir.absolutePath + '/dist'
  eachFile { details ->
    details.path = details.path.startsWith('META-INF')?: 'static/' + details.path
  }
  includeEmptyDirs = false
}

jar.dependsOn runBuild

Pada file di atas, saya menggunakan plugin com.moowork.node yang akan men-download Node.js dan Yarn ke dalam folder .gradle. Sebagai informasi, Yarn adalah sebuah klien npm yang masih bisa menggunakan file package.json seperti klien npm standard. Salah satu kelebihan Yarn adalah ia memiliki penyimpanan global di folder ~/.cache/yarn (sama halnya dengan Gradle yang memiliki folder ~/.gradle). Bila saya membuat proyek baru yang memakai library yang sudah di-download sebelumnya, Yarn tidak akan membutuhkan koneksi internet lagi karena ia akan memakai yang sudah tersimpan di ~/.cache/yarn (proses build pun terasa lebih cepat karena tidak perlu men-download ulang). Ini berbeda dengan cache klien npm standard (~/.npm) yang hanya sementara dan tidak permanen. Karena Yarn kompatibel dengan klien npm standard, saya bisa mengganti perintah npm run build dengan yarn run build.

Spring Boot secara otomatis akan menyajikan file yang berada di folder /public dan /static sehingga bisa diakses oleh pengguna. Oleh sebab itu, file Gradle di atas akan menyalin seluruh file yang ada di folder dist ke dalam folder static di dalam file Jar yang dihasilkan nantinya.

Salah satu masalah yang saya jumpai pada proyek Vue.js yang dihasilkan oleh vue-cli adalah build scripts yang diletakkan di folder build. Hal ini akan menimbulkan sedikit kekacauan karena folder build memiliki arti berbeda di Gradle yang akan digunakan sebagai tempat output (misalnya file Jar yang dihasilkan Gradle akan diletakkan di folder ini). Oleh sebab itu, saya mengganti nama folder build menjadi buildScripts. Tidak lupa juga, saya men-update file package.json supaya menggunakan nama folder yang baru.

Berikutnya, pada proyek utama (Spring Boot), saya menambahkan sebuah file baru bernama settings.gradle yang isinya berupa:

rootProject.name = 'latihan'
include 'frontend'

Sebagai langkah terakhir, saya menambahkan dependency ke subproyek frontend di file build.gradle milik proyek utama, seperti pada:

dependencies {
   compile project(':frontend')
   ...
}

Sekarang, bila saya melihat daftar perintah Gradle yang bisa dijalankan untuk proyek ini, saya akan menjumpai seperti yang terlihat pada gambar berikut ini:

Daftar perintah Gradle untuk proyek ini

Daftar perintah Gradle untuk proyek ini

Mari jalankan proyek ini dengan men-double click bootRun dibagian application atau dengan memberikan perintah berikut ini:

$ ./gradlew bootRun
Starting a Gradle Daemon (subsequent builds will be faster)
:frontend:compileJava NO-SOURCE
:frontend:processResources NO-SOURCE
:frontend:classes UP-TO-DATE
:frontend:nodeSetup UP-TO-DATE
:frontend:yarnSetup UP-TO-DATE
:frontend:yarn UP-TO-DATE
:frontend:runBuild
yarn run v0.27.5
$ node buildScripts/build.js

Starting to optimize CSS...
Processing static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css...
Processed static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css, before: 431, after: 363, ratio: 84.22%
Hash: 424b3c987da691d210b2ING
Version: webpack 2.6.1ECUTING
Time: 5139msrunBuild
                                                  Asset       Size  Chunks             Chunk Names
                  static/js/app.eaedbed942638ee1e5dd.js    11.7 kB       0  [emitted]  app
               static/js/vendor.4e561224e1f68ab595ac.js     107 kB       1  [emitted]  vendor
             static/js/manifest.4373dcd698dd3ea60ed7.js    1.51 kB       2  [emitted]  manifest
    static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css  363 bytes       0  [emitted]  app
              static/js/app.eaedbed942638ee1e5dd.js.map    36.7 kB       0  [emitted]  app
static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css.map  910 bytes       0  [emitted]  app
           static/js/vendor.4e561224e1f68ab595ac.js.map     857 kB       1  [emitted]  vendor
         static/js/manifest.4373dcd698dd3ea60ed7.js.map    14.6 kB       2  [emitted]  manifest
                                             index.html  445 bytes          [emitted]  

  Build complete.

  Tip: built files are meant to be served over an HTTP server.
  Opening index.html over file:// won't work.

Done in 9.22s.
:frontend:jar UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:findMainClass
:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.4.RELEASE)

2017-07-10 20:07:40.825  INFO 4098 --- [           main] com.example.latihan.LatihanApplication   : Starting LatihanApplication on desktop with PID 4098 (/home/user/IdeaProjects/latihan/build/classes/main started by user in /home/userIdeaProjects/latihan)
2017-07-10 20:07:40.828  INFO 4098 --- [           main] com.example.latihan.LatihanApplication   : No active profile set, falling back to default profiles: default
2017-07-10 20:07:40.913  INFO 4098 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5f71c76a: startup date [Mon Jul 10 20:07:40 WIB 2017]; root of context hierarchy
2017-07-10 20:07:42.200  INFO 4098 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2017-07-10 20:07:42.213  INFO 4098 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2017-07-10 20:07:42.215  INFO 4098 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.15
2017-07-10 20:07:42.364  INFO 4098 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2017-07-10 20:07:42.366  INFO 4098 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1456 ms
2017-07-10 20:07:42.478  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2017-07-10 20:07:42.776  INFO 4098 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5f71c76a: startup date [Mon Jul 10 20:07:40 WIB 2017]; root of context hierarchy
2017-07-10 20:07:42.839  INFO 4098 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2017-07-10 20:07:42.841  INFO 4098 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2017-07-10 20:07:42.867  INFO 4098 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-10 20:07:42.868  INFO 4098 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-10 20:07:42.902  INFO 4098 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-10 20:07:42.922  INFO 4098 --- [           main] oConfiguration$WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
2017-07-10 20:07:43.017  INFO 4098 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-07-10 20:07:43.074  INFO 4098 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-07-10 20:07:43.081  INFO 4098 --- [           main] com.example.latihan.LatihanApplication   : Started LatihanApplication in 2.57 seconds (JVM running for 3.034)
<============-> 92% EXECUTING
> :bootRun

Bila perintah ini dikerjakan pertama kalinya, ia akan men-download Node.js dan Yarn. Selain itu, ia juga akan men-download library di package.json yang belum ada di folder node_modules. Setelah itu, ia akan menjalankan Webpack yang secara tidak langsung bekerja melalui loader-nya. Sebagai contoh, vue-loader akan bekerja menerjemahkan komponen Vue dalam file .vue dan babel-loader akan melakukan transpiling sehingga saya bisa memakai ES2016 tanpa khawatir tidak bisa dijalankan oleh browser-browser populer. Setelah selesai, saya bisa membuka http://localhost:8080 untuk mengakses aplikasi.

Sampai disini, saya sudah membuat proyek baru yang menggabungkan kode program front end dan back end. Saya juga memakai Gradle untuk mengendalikan kode program front end (yang secara tidak langsung akan memakai Yarn dan Webpack yang dipakai oleh proyek front end).

Bagaimana dengan deployment? Bila saya memberikan perintah:

$ ./gradlew war

Saya akan memperoleh sebuah file War bernama latihan-0.0.1-SNAPSHOT.war di lokasi build/libs. File ini dapat langsung di-deploy di application server produksi (misalnya Tomcat atau JBoss). Di dalam file War ini sudah termasuk proyek front end saya yang bisa ditemukan di WEB-INF\lib\frontend.jar. Jadi, Gradle akan membuat Jar untuk front end terlebih dahulu. Setelah itu, Jar akan dipaketkan bersama-sama dengan Jar lain yang dibutuhkan oleh back end di WEB-INF\lib.

Memakai Graph Database Dengan Neo4j

Selama beberapa dekade terakhir ini, database relasional berbasis tabel boleh dibilang merupakan jenis database yang paling sering dipakai dan paling dewasa. Database relasional tidak akan hilang dalam jangka waktu dekat ini, tapi mereka memiliki keterbatasan sendiri. Sebagai contoh, database relasional akan kewalahan melakukan query yang dibutuhkan untuk keperluan jejaring sosial seperti Facebook dan Twitter. Database relasional memang memiliki JOIN untuk menggabungkan tabel, tapi JOIN yang mereka miliki tidak memiliki arah dan label. Salah satu jenis database yang diciptakan untuk mengembalikan makna ‘relasional’ tersebut adalah graph database.

Graph yang dimaksud pada graph database adalah definisi graph pada graph theory yang merupakan bagian dari ilmu matematika diskrit. Paper pertama yang membahas graph theory ditulis oleh Leonhard Eulear pada tahun 1736 yang membahas tentang permasalahan Konigsberg Bridge.

Pada kesempatan ini, saya akan mencoba menggunakan sebuah graph database populer, Neo4j. Saya harus mulai dari mana? Pada database relasional, saya bisa mulai dengan merancang tabel dengan merancang skema dengan menggunakan ERD. Berbeda dari database relasional, sebuah graph database tidak memiliki skema seperti tabel dan kolom yang berbeda dengan isi (record). Segala sesuatunya adalah data! Oleh sebab itu, pada saat merancang untuk graph database, saya bisa langsung memakai contoh data. Sebagai latihan, saya akan membuat database yang menampung produk yang dibeli oleh pengguna seperti berikut ini:

Contoh rancangan untuk graph database

Contoh rancangan untuk graph database

Pada rancangan di atas, masing-masing lingkaran mewakili sebuah node. Setiap node terhubung ke node lain melalui garis yang memiliki arah yang disebut sebagai edge. Baik node maupun edge boleh memiliki properties. Berkebalikan dari database relasional, nilai properties pada node atau edge akan lebih baik bila sebisa mungkin di-‘normalisasi’ menjadi node sehingga bisa dipakai secara efisien di query.

Setelah menentukan rancangan database, sekarang saatnya untuk memasukkan data. Saya sudah menyiapkan beberapa data dalam bentuk CSV untuk dimasukkan sebagai isi dari database Neo4j. Saya akan mulai dengan membuat seluruh node yang mewakili produk. Untuk itu, setelah menjalankan Neo4j, saya membuka browser dan menampilkan http://localhost:7474. Ini adalah halaman web dimana saya bisa mengelola database Neo4j dan juga mengerjakan query (dalam bentuk Cypher). Sebagai contoh, saya memberikan perintah seperti pada gambar berikut ini:

Mengisi data dari file CSV

Mengisi data dari file CSV

Saya juga melakukan hal yang sama untuk menciptakan node yang mewakili pengguna, misalnya dengan memberikan perintah Cypher berikut ini:

USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM "file:C:/user.csv" AS row
CREATE (:User {id: row.id});

Karena kedua node ini akan sering dicari berdasarkan id, maka saya bisa memberikan index. id bersifat unik sehingga saya bisa menggunakan perintah CREATE CONSTRAINTS yang akan menciptakan unique index yang lebih cepat dibandingkan index biasa. Sebagai contoh, saya memberikan perintah berikut ini:

CREATE CONSTRAINT ON (p:Produk) ASSERT p.id IS UNIQUE;
CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE

Sekarang adalah saatnya untuk membuat edge BELI dari User ke Produk. Saya melakukannya dengan memberikan perintah seperti berikut ini:

USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM "file:C:/buys.csv" AS row
MATCH (user:User {id: row.user_id})
MATCH (produk:Produk {id: row.produk_id})
MERGE (user)-[:BELI]->(produk);

Setelah ini, saya bisa mulai belajar mencari data. Bila pada database relasional terdapat SQL, maka Neo4j menggunakan query khusus yang disebut sebagai Cypher. Bahasa query ini juga deklaratif seperti SQL dan dirancang agar mudah dipahami. Sebagai contoh, untuk menampilkan 25 node user pertama, saya dapat menggunakan Cyhper seperti:

MATCH (n:User) RETURN n LIMIT 25;
Hasil query dalam bentuk tabel

Hasil query dalam bentuk tabel

Tidak ada yang berbeda dari database relasional bila node dikembalikan dalam bentuk tabel. Agar lebih menarik, saya bisa menampilkan hasil query dalam bentuk grafis seperti yang terlihat pada gambar berikut ini:

Hasil query dalam bentuk graph

Hasil query dalam bentuk graph

Tidak seperti hasil query dari database relasional yang sulit dicerna secara langsung, hasil query pada graph database cukup menarik untuk diamati. Pengguna juga bisa menemukan pola data secara visual.

Contoh lain dari query Cypher adalah:

MATCH (u:User {id:"xxx"})-[:BELI]->(p:Produk)
RETURN p;

Klausa MATCH pada query di atas akan mencari node User yang memiliki id "xxx" dan memiliki asosiasi BELI terhadap node Produk. Setelah itu, klausa RETURN akan mengembalikan seluruh node Produk yang ada. Dengan kata lain, query ini akan mengembalikan seluruh produk yang dibeli oleh pengguna dengan id "xxx". Tentu saja query seperti ini juga bisa dilakukan secara mudah dengan menggunakan SQL di database relasional.

Untuk menunjukkan sesuatu yang tidak gampang dilakukan melalui SQL, saya akan memberikan query Cypher seperti berikut ini:

MATCH (u1:User)-[:BELI]->(p:Produk)<-[:BELI]-(u2:User) 
RETURN p;

Hasilnya akan terlihat seperti pada gambar berikut ini:

Hasil query dalam bentuk graph

Hasil query dalam bentuk graph

Dengan menggunakan arah panah seperti -[:BELI]-> dan <-[:BELI]-, query di atas akan mengembalikan seluruh node Produk yang minimal sudah dibeli oleh 2 pengguna. Karena saya tidak peduli dengan node User pada query di atas, saya bisa mengabaikannya dengan mengubah query menjadi seperti berikut ini:

MATCH ()-[:BELI]->(p:Produk)<-[:BELI]-()
RETURN p;

Selain mengembalikan node, saya juga bisa mengembalikan property sehingga memperoleh hasil dalam bentuk tabel. Sebagai contoh, saya bisa memberikan query seperti berikut ini:

MATCH (u1:User)-[:BELI]->(p:Produk)<-[:BELI]-(u2:User)
RETURN u1.id AS user1, p.url AS produk, u2.id AS user2;

Ingin sesuatu yang lebih sulit dilakukan melalui query SQL? Sebagai contoh, saya akan mencari produk lain yang dibeli oleh pengguna lain yang juga membeli produk yang sama dengan seorang pengguna dengan menggunakan query seperti berikut ini:

MATCH (u1:User)-[:BELI]->(p1:Produk)<-[:BELI]-(u2:User),
      (u2)-[:BELI]->(p2:Produk)
RETURN p1, p2;
Hasil query dalam bentuk graph

Hasil query dalam bentuk graph

Karena graph database memang dirancang untuk keperluan seperti Cypher di atas, ia bukan hanya menawarkan kemudahan, tapi juga kinerja yang lebih baik dibandingkan query serupa yang memakai database relasional. Walaupun penelusuran tetapi dilakukan satu per satu seperti pada JOIN di database relasional, setiap node bisa memiliki path yang berbeda. Begitu path untuk node sudah melenceng dari yang diharapkan, ia bisa segera diabaikan sehingga penelusuran bisa segera dilanjutkan.

Saya juga menggabungkan MATCH di Cypher di atas sehingga menjadi seperti berikut ini:

MATCH (u1:User)-[:BELI]->(p1:Produk)<-[:BELI]-(u2:User)-[:BELI]->(p2:Produk)
RETURN p1, p2;

Tergantung pada selera, versi yang terakhir ini mungkin lebih mudah dipahami dibandingkan dengan versi sebelumnya.

Sama seperti di SQL, untuk mebatasi query di Cypher saya juga dapat menggunakan klausa WHERE. Sebagai contoh, bila saya hanya tertarik pada produk dengan id 'xxx', maka saya bisa memberikan query seperti berikut ini:

MATCH (u1:User)-[:BELI]->(p1:Produk)<-[:BELI]-(u2:User)-[:BELI]->(p2:Produk)
WHERE p1.id = "xxx"
RETURN u1, p1, u2, p2;
Hasil query dalam bentuk graph

Hasil query dalam bentuk graph

Pada gambar di atas, id 5969 adalah nilai internal yang diberikan untuk produk 'xxx'. Terlihat bahwa user seperti user 329, 3953, 5149 dan sejenisnya membeli produk 'xxx' bersamaan dengan produk lain.

Pada query diatas, akan ada banyak produk ganda yang ditampilkan karena beberapa pengguna berbeda bisa saja melihat banyak produk yang sama. Sama seperti di SQL, untuk mengembalikan hanya produk yang unik, saya dapat menggunakan klausa DISTINCT seperti pada contoh berikut ini:

MATCH (u1:User)-[:BELI]->(p1:Produk)<-[:BELI]-(u2:User)-[:BELI]->(p2:Produk)
WHERE p1.id = "xxx" 
RETURN DISTINCT p1, u2, p2;

Membuat Fitur “Produk Serupa” Dengan PredictionIO

PredictionIO (https://prediction.io) adalah sebuah engine yang dirancang untuk mudah dipakai dalam menerapkan machine learning. Lalu apa manfaatnya? Produk ini tidak hanya berguna untuk peneliti, tetapi juga bisa diterapkan langsung pada situs e-commerce. Salah satunya adalah PredictionIO bisa digunakan untuk menghasilkan daftar “produk serupa” berdasarkan perilaku dari pengunjung lain. Tanpa machine learning, pembuat situs biasanya menampilkan produk yang acak berdasarkan tag tertentu. Dengan menghasilkan daftar “produk serupa” secara pintar, pengguna akan memperoleh saran yang lebih akurat sehingga penjualan bisa meningkat, terutama bagi situs e-commerce yang memiliki banyak produk bervariasi.

Sebagai latihan, saya akan menjalankan engine PredictionIO pada sebuah server Linux yang terpisah dari server web. Salah satu kelebihan PredictionIO adalah masing-masing algoritma dikelompokkan dalam apa yang disebut dengan engine template. Saya dapat menemukan daftar engine template siap pakai di https://templates.prediction.io. Sesungguhnya sebuah engine template bukan hanya mengandung algoritma, tetapi komponen lengkap yang disebut DASE (Data, Algorithm, Serving, Evaluator). Developer juga bisa memodifikasi kode program engine template (dalam bahasa Scala) sesuai dengan kebutuhan.

Karena ingin memunculkan “produk serupa”, saya akan menggunakan template PredictionIO/template-scala-parallel-complementarypurchase yang dapat dijumpai di https://templates.prediction.io/PredictionIO/template-scala-parallel-complementarypurchase. Berdasarkan informasi dari dokumentasi, algoritma yang dipakai oleh engine template ini mengikuti konsep association rule learning (https://en.wikipedia.org/wiki/Association_rule_learning).

Sebagai langkah awal, saya memberikan perintah berikut ini untuk men-download kode program engine template:

$ pio template get PredictionIO/template-scala-parallel-complementarypurchase latihanEngine
Please enter author's name: latihan
Please enter the template's Scala package name (e.g. com.mycompany): com.latihan
Please enter author's e-mail address: 
Author's name:         latihan
Author's e-mail:       
Author's organization: com.latihan
Would you like to be informed about new bug fixes and security updates of this template? (Y/n) n
Retrieving PredictionIO/template-scala-parallel-complementarypurchase
There are 5 tags
Using tag v0.3.1
Going to download https://github.com/PredictionIO/template-scala-parallel-complementarypurchase/archive/v0.3.1.zip
Redirecting to https://codeload.github.com/PredictionIO/template-scala-parallel-complementarypurchase/zip/v0.3.1
Replacing org.template.complementarypurchase with com.latihan...
Processing latihanEngine/build.sbt...
Processing latihanEngine/engine.json...
Processing latihanEngine/src/main/scala/Algorithm.scala...
Processing latihanEngine/src/main/scala/DataSource.scala...
Processing latihanEngine/src/main/scala/Engine.scala...
Processing latihanEngine/src/main/scala/Preparator.scala...
Processing latihanEngine/src/main/scala/Serving.scala...
Engine template PredictionIO/template-scala-parallel-complementarypurchase is now ready at latihanEngine

Perintah di atas akan menciptakan sebuah folder bernama latihanEngine yang berisi kode program yang bisa saya modifikasi sesuai keperluan. Karena akan bekerja pada folder ini, maka saya perlu pindah ke folder ini dengan memberikan perintah:

$ cd latihanEngine

Setelah itu, saya siap untuk menjalankan PredictionIO dengan memberikan perintah seperti berikut ini:

$ pio-start-all

Saya bisa memerika apakah semua komponen dijalankan dengan baik dengan menggunakan perintah seperti berikut ini:

$ pio status
PredictionIO
  Installed at: /home/snake/PredictionIO
  Version: 0.9.2

Apache Spark
  Installed at: /home/snake/PredictionIO/vendors/spark-1.3.0
  Version: 1.3.0 (meets minimum requirement of 1.3.0)

Storage Backend Connections
  Verifying Meta Data Backend
  Verifying Model Data Backend
  Verifying Event Data Backend
  Test write Event Store (App Id 0)
[INFO] [HBLEvents] The table predictionio_eventdata:events_0 doesn't exist yet. Creating now...
[INFO] [HBLEvents] Removing table predictionio_eventdata:events_0...

(sleeping 5 seconds for all messages to show up...)
Your system is all ready to go.

Perintah di atas memerika apakah komponen yang dipakai oleh PredictionIO semuanya sudah siap dipakai. Terlihat bahwa PredictionIO memakai Apache Spark untuk mengerjakan algoritma secara paralel dan scalable. Selain itu, data yang dikumpulkan akan disimpan ke dalam Apache HBase. Ini adalah sebuah database No-SQL yang dirancang untuk dipakai pada HDFS (Hadoop File System).

Langkah awal pada machine learning adalah mengumpulkan data sebanyak mungkin untuk keperluan training. PredictionIO akan membuat sebuah web service server berbasis REST yang dapat dipakai oleh aplikasi web untuk memberikan data. Untuk itu, saya perlu memperoleh sebuah access key terlebih dahulu dengan memberikan perintah:

$ pio app new LatihanWeb
[INFO] [HBLEvents] The table predictionio_eventdata:events_2 doesn't exist yet. Creating now...
[INFO] [App$] Initialized Event Store for this app ID: 2.
[INFO] [App$] Created new app:
[INFO] [App$]       Name: LatihanWeb
[INFO] [App$]         ID: 2
[INFO] [App$] Access Key: A7iLxxkf7RGBpiTtxMGu8zi4EZ0Kfcox05HTwYDCteCn5weqtdnoGyEaRCRYX2CM

Bagian yang paling penting dari output di atas adalah access key yang perlu saya berikan saat memanggil event server yang diciptakan oleh PredictionIO. Untuk memastikan event server berjalan dengan baik, saya bisa mengakses URL seperti http://192.168.10.100:7070 dari browser dimana 192.168.10.100 adalah IP server yang menjalankan PredictionIO. Saya akan memperoleh hasil seperti {"status":"alive"}. Bila memakai terminal, saya juga bisa menggunakan perintah curl seperti berikut ini:

$ curl 192.168.10.100:7070
{"status":"alive"}

Sekarang, saya siap untuk mengirimkan event kepada event server. Sebagai latihan, saya akan memakai access logs untuk web distribution dari Amazon CloudFront milik sebuah situs. Saya hanya tertarik pada kolom ke-12 (cs-uri-query) dimana saya mengambil nilai parameter item_id yang mewakili item yang dilihat pengguna dan visitor_id yang mewakili pengenal unik untuk pengguna tersebut. Engine complementary purchase yang saya pakai secara default mendukung event buy yang membutuhkan parameter berupa user dan item. Agar sederhana, saya tidak akan melakukan perubahan kode program dan menganggap event buy tersebut sama seperti view. Saya kemudian membuat sebuah script Perl untuk membaca dan mengakses event server seperti berikut ini:

#!/usr/bin/perl

$LOGFILE = "access.log";
open(LOGFILE) or die("Tidak dapat membaca $LOGFILE");
while () {
  next if 1..2;
  $uri = (split(' ', $_))[11];
  if ($uri =~ m/item_id%253D(\w+)%2526/) {
    $itemId = $1;
  } else {
    print "Tidak dapat menemukan item_id di $_\n";
  }
  if ($uri =~ m/visitor_id%253D([\w-]+)&/) {
    $browserId = $1;
  } else {
    print "Tidak dapat menemukan visitor_id di $_\n";
  }
  ($postEvent = <<"CURL") =~ s/\n+//gm;
curl -i -X POST http://192.168.10.100:7070/events.json?accessKey=A7iLxxkf7RGBpiTtxMGu8zi4EZ0Kfcox05HTwYDCteCn5weqtdnoGyEaRCRYX2CM \
-H "Content-Type: application/json" \
-d '{
  "event": "buy",
  "entityType": "user",
  "entityId": "$browserId",
  "targetEntityType": "item",
  "targetEntityId": "$itemId"
}'
CURL
  system($postEvent);
}

Script di atas pada dasarnya akan menggunakan curl untuk menambahkan event pada event server. Seluruh event yang terkumpul akan diletakkan pada database Apache HBase. Untuk melihat apakah event sudah tersimpan dengan baik, saya bisa membuka URL seperti http://192.168.10.100:7070/events.json?accessKey=A7iLxxkf7RGBpiTtxMGu8zi4EZ0Kfcox05HTwYDCteCn5weqtdnoGyEaRCRYX2CM melalui browser.

Bila selama percobaan, event yang diberikan tidak benar atau perlu dihapus, saya bisa menggunakan perintah seperti berikut ini:

$ pio app data-delete LatihanWeb
[INFO] [App$] Data of the following app (default channel only) will be deleted. Are you sure?
[INFO] [App$]     App Name: LatihanWeb
[INFO] [App$]       App ID: 2
[INFO] [App$]  Description: None
Enter 'YES' to proceed: YES
[INFO] [HBLEvents] Removing table predictionio_eventdata:events_2...
[INFO] [App$] Removed Event Store for this app ID: 2
[INFO] [HBLEvents] The table predictionio_eventdata:events_2 doesn't exist yet. Creating now...
[INFO] [App$] Initialized Event Store for this app ID: 2.
[INFO] [App$] Done.

Setelah event terkumpul, sekarang saatnya untuk menjalankan engine. Tapi sebelumnya, saya perlu membuka file engine.json dan menguna nilai appName menjadi latihanWeb (sesuai dengan nama yang sama berikan saat mengerjakan pio app new). Setelah itu, saya memberikan perintah berikut ini:

$ pio build --verbose
...
[INFO] [Console$] Your engine is ready for training.

Perintah di atas akan men-download dependency Ivy yang dibutuhkan (dengan menggunakan Scala sbt, sejenis Gradle di Groovy) dan men-compile kode program yang sebelumnya dihasilkan oleh engine template. Setelah proses building selesai, langkah berikutnya adalah melakukan training untuk predictive model dengan memberikan perintah ini:

$ pio train
...
[INFO] [CoreWorkflow$] Training completed successfully.

Perintah ini akan menjalankan proses training di Apache Spark. Bila terdapat kesalahan yang berkaitan dengan java.lang.StackOverflowError, maka stack size bisa ditingkatkan menjadi lebih besar, misalnya 16 MB dengan membuat environment variable bernama SPARK_JAVA_OPTS yang memiliki isi seperti -Xss16M.

Setelah proses training selesai, saya bisa men-deploy engine agar hasilnya dapat diakses melalui REST. Sebagai contoh, saya memberikan perintah berikut ini:

$ pio deploy
...
[INFO] [MasterActor] Bind successful. Ready to serve.

Untuk memastikan engine sudah berjalan dengan baik, saya bisa mencoba mengakses URL seperti http://192.168.10.100:8000. Bila hasilnya muncul seperti harapan, maka saya siap memodifikasi aplikasi web untuk mengakses engine ini.

Untuk mendapatkan rekomendasi produk sejenis, server web (atau server lain yang membutuhkan) perlu mengirimkan JSON yang mengandung sebuah id item di key "items" dan jumlah rekomendasi yang dibutuhkan di key "num". Sebagai contoh, saya bisa mengakses engine seperti berikut ini:

$ curl -H "Content-Type: application/json" -d '{"items": ["xxx"], "num": 3}' http://192.168.10.100:8000/queries.json
{
  "rules": [
    {
     "cond": ["xxx"], 
     "itemScores":[
        {"item":"hasil1", "support":3.92541707556427E-4, "confidence":0.166666,"lift":424.583333},
        {"item":"hasil2","support":3.925417075564279E-4,"confidence":0.166666,"lift":424.58333333},
        {"item":"hasil3", "support":3.925417075564279E-4,"confidence":0.1666666,"lift":424.58333333}]
    }
  ]
}

Sebagai contoh, pada hasil yang saya peroleh, 3 rekomendasi untuk sebuah produk iPhone 5s adalah produk ZenFone 2, iPhone 5s dari penjual berbeda, dan iPhone 6. Hasil ini cukup masuk akal secara sekilas. Yang perlu diperhatikan adalah hasil ini tidak menyertakan riwayat produk yang sudah pernah dilihat oleh user tersebut, melainkan rekomendasi produk serupa berdasarkan pola kunjungan pengguna lain.

Bila mencoba dengan data dengan variasi kunjungan yang terbatas, misalnya snapshot untuk beberapa menit dimana masing-masing item barang hanya dikunjungi satu dua kali oleh pengguna, saya bisa mengubah parameter algoritma di engine.json menjadi seperti berikut ini:

"algorithms": [
    {
      "name": "algo",
      "params": {
        "basketWindow" : 300,
        "maxRuleLength" : 2,
        "minSupport": 0,
        "minConfidence": 0,
        "minLift" : 0,
        "minBasketSize" : 2,
        "maxNumRulesPerCond": 5
      }
    }
]

Mengubah nilai minSupport, minConfidence dan minLift menjadi 0 akan mengurangi kualitas hasil yang diperoleh. Sebagai contoh, nilai default untuk minSupport adalah 0.1 yang berarti item harus muncul minimal 10% untuk seluruh transaksi agar ia disertakan pada hasil. Oleh sebab itu, pengaturan seperti ini sebaiknya tidak dipakai pada kasus nyata.

Belajar Memakai Specification Di Spring Data JPA

Pada artikel Memakai Querydsl Dengan Spring Data JPA, saya menunjukkan bagaimana memakai Querydsl di Spring Data JPA. Pada kesempatan kali ini, saya akan mencoba membuat query dinamis dengan menggunakan fasilitas bawaan Spring Data JPA yaitu Specification. Karena interface ini merupakan bawaan dari Spring Data JPA, saya tidak perlu lagi menambahkan referensi ke Jar eksternal.

Mengapa perlu membuat query secara dinamis? Sebagai contoh, saya memiliki sebuah tabel PrimeFaces di facelet seperti berikut ini:

<p:dataTable var="vProduk" value="#{entityList}" selection="#{entityList.selected}" selectionMode="single" rowKey="#{vProduk.id}" 
    paginator="true" rows="10" id="tabelProduk" lazy="true" filterDelay="1000" resizableColumns="true">
    <p:column headerText="Kode" sortBy="#{vProduk.kode}" filterBy="#{vProduk.kode}">
        <h:outputText value="#{vProduk.kode}" />
    </p:column>
    <p:column headerText="Nama" sortBy="#{vProduk.nama}" filterBy="#{vProduk.nama}">
    <h:outputText value="#{vProduk.nama}" />
    </p:column>
    <p:column headerText="Jenis" sortBy="#{vProduk.jenis.nama}" filterBy="#{vProduk.jenis}" filterOptions="#{filterJenisProduk}" width="150">              
        <h:outputText value="#{vProduk.jenis.nama}" />
    </p:column>
    <p:column headerText="Harga (Rp)" sortBy="#{vProduk.harga}" filterBy="#{vProduk.harga}">
        <h:outputText value="#{vProduk.harga}" style="float: right">
            <f:convertNumber type="number" />
        </h:outputText>
    </p:column>
    <p:column headerText="Qty" sortBy="#{vProduk.qty}" filterBy="#{vProduk.qty}" width="100">
        <h:outputText value="#{vProduk.qty}" style="float: right">
            <f:convertNumber type="number" />
        </h:outputText>
    </p:column>               
    <p:ajax event="rowSelect" update=":mainForm:buttonPanel" />               
</p:dataTable>

Tampilan HTML untuk tabel di atas akan terlihat seperti pada gambar berikut ini:

Tabel dengan fasilitas filtering per kolom

Tabel dengan fasilitas filtering per kolom

Pada tampilan di atas, pengguna bisa melakukan pencarian secara bebas di setiap kolom. Sebagai contoh, pengguna bisa mencari berdasarkan kode dan jenis. Selain itu, pengguna juga bisa mencari berdasarkan kode dan jenis dan harga. Atau, pengguna juga bisa memilih mencari berdasarkan jenis dan harga. Kombinasi seperti ini akan sangat banyak bila ingin dibuat query-nya secara manual satu per satu. Ini adalah contoh kasus yang tepat untuk memakai fasilitas seperti Querydsl dan Specification.

Karena filter table seperti ini sering digunakan pada tabel yang berbeda, saya akan mulai dengan membuat sebuah implementasi Specification yang bisa dipakai ulang seperti pada kode program berikut ini:

public class FilterSpecification<T> implements Specification<T> {

  public enum Operation { EQ, LIKE, MIN };

  private List<Filter> filters = new ArrayList<>();

  public void and(String name, Operation op, Object...args) {
    filters.add(new Filter(name, op, args));
  }

  public void and(Filter filter) {
    filters.add(filter);
  }

  @Override
  public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {              
    Predicate result = cb.conjunction();
    for (Filter filter: filters) {
      result = cb.and(result, filter.toPredicate(cb, root));            
    }
    return result;      
  }

  class Filter {

    private String name;
    private Operation op;
    private Object[] args;

    public Filter(String name, Operation op, Object... args) {
      this.name = name;
      this.op = op;
      this.args = args;
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public Predicate toPredicate(CriteriaBuilder cb, Root<T> root) {
      Expression path = toPath(root, name);
      switch (op) {
      case EQ:
        return cb.equal(path, args[0]);
      case LIKE:                                
        return cb.like(path, (String) args[0]);
      case MIN: 
        return cb.greaterThanOrEqualTo(path, Integer.parseInt((String) args[0]));
      default:
        return null;
      }
    }

    @SuppressWarnings("rawtypes")
    public Expression toPath(Root root, String name) {
      Path result = null;
      if (name.contains("_")) {
        for (String node: name.split("_")) {
          node = StringUtils.uncapitalize(node);                    
          if (result == null) {
            result = root.get(node);
          } else {
            result = result.get(node);
          }
        }
      } else {
        result = root.get(name);
      }
      return (Expression) result;
    }

  }     

}

Setiap kali pengguna mengetik (atau memilih) kriteria filter di tabel, setelah delay selama 1000 ms, method load() di LazyDataModel akan dipanggil. Method ini perlu melakukan query di database untuk memperoleh hasil sesuai kriteria yang diberikan. Untuk itu, saya bisa menggunakan FilterSpecification yang saya buat seperti pada kode program berikut ini:

FilterSpecification<Produk> filterSpecification = new FilterSpecification<>();
if (getFilter("kode") != null) {
  filterSpecification.and("kode", Operation.LIKE, getFilterAsSearchExpr("kode"));
}
if (getFilter("nama") != null) {
  filterSpecification.and("nama", Operation.LIKE, getFilterAsSearchExpr("nama"));
}
if (getFilter("jenis") != null) {
  filterSpecification.and("jenis_id", Operation.EQ, getFilter("jenis"));
}
if (getFilter("harga") != null) {
  filterSpecification.and("harga", Operation.MIN, (String) getFilter("harga"));
}
if (getFilter("qty") != null) {
  filterSpecification.and("qty", Operation.MIN, (String) getFilter("qty"));
}

Method and() akan mendaftarkan sebuah Filter baru yang berisi nama path, operasi dan nilai pencarian. Untuk mengakses path di dalam path lain, saya menggunakan tanda garis bawah. Kode program yang memungkinkan hal ini terjadi dapat dilihat di method FilterSpecification.toPath().

Method toPredicate() hanya akan dipanggil pada saat Specification ini dipakai oleh salah satu method di repository Spring Data JPA. Agar bisa memakai Specification di repository Spring Data JPA, saya perlu menurunkan interface repository dari JpaSpecificationExecutor seperti yang terlihat pada kode program berikut ini:

public interface ProdukRepository extends JpaRepository<Produk, Long>, JpaSpecificationExecutor<Produk> {}

Sekarang, saya dapat memakai salah satu method yang mendukung Specification di ProdukRepository. Sebagai contoh, saya bisa melakukan filtering dengan menggunakan Specification sekaligus memperoleh hasil per halaman dengan Pageable dengan memanggil repository seperti berikut ini:

produkRepository.findAll(filterSpecification, pageable);

Dengan kode program yang sederhana dan mudah dipahami ini, saya kini bisa melakukan pencarian untuk kombinasi kolom berbeda. Sebagai contoh, bila saya mencari nama produk yang mengandung kata RING KEH dengan jenis HGP dan harga minimal Rp 100.000, saya akan memperoleh hasil seperti pada gambar berikut ini:

Melakukan filtering pada tabel

Melakukan filtering pada tabel