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.

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

Belajar Membuat Aplikasi Web Dengan Spring Framework

Banyak sekali pemula yang kewalahan dalam mempelajari pemograman web di Java karena kewalahan dengan sekian banyak pilihan yang ada. Walaupun Java EE 7 kini sudah semakin mirip dengan Spring, untuk saat ini ia masih perlu banyak belajar lagi agar semakin dewasa. Saat ini tidak semua server Java EE 7 memiliki perilaku yang sama. Sebagai contoh, unit testing yang melibatkan CDI dan EJB begitu mudah dilakukan di server GlassFish karena ia mendukung EJBContainer untuk menjalankan embeddable container. Server WildFly tampaknya tidak menganut filosofi serupa dan lebih menyarankan penggunaan Arquillian untuk keperluan pengujian. Masalah perbedaan akan lebih sering muncul bila memakai Java EE API yang jarang dibahas seperti JMS.

Bila Java EE 7 adalah jalur resmi, maka teknologi dari Spring adalah jalur tidak resmi yang masih tetap bertahan sampai sekarang. Boleh dibilang CDI dari Java EE 7 menawarkan banyak fasilitas yang bersaing dengan Spring Container. Lalu kenapa masih memakai Spring? Karena Spring memiliki ekosistem yang luas dan dewasa. Memakai Spring ibarat masuk ke sebuah jeratan dunia baru yang beda sendiri. Spring dapat dijalankan pada server apa saja asalkan mendukung servlet (misalnya server Tomcat yang lawas).

Pada kesempatan ini, saya akan membuat sebuah aplikasi web sederhana dengan menggunakan Spring dan beberapa API dari Java EE 7 yang di-deploy pada server Tomcat. Aplikasi ini tidak membutuhkan server khusus untuk Java EE 7. Kode program lengkap untuk aplikasi latihan ini dapat dijumpai di https://github.com/JockiHendry/basic-inventory-web. Hasil akhirnya dapat dijumpai di https://basicinventory-latihanjocki.rhcloud.com/app/home.

Aplikasi ini memakai Gradle untuk mengelola proses siklus hidup seperti building. Sebagai contoh, bila saya memberikan perintah gradle build, sebuah file bernama ROOT.war akan dihasilkan untuk di-deploy pada servlet container seperti Tomcat. Sebagai informasi, tidak seperti di PHP yang berbasis file, sebuah aplikasi web di Java berada dalam bentuk file tunggal yang berakhir *.war (web archive). File ini adalah sebuah file ZIP yang berisi seluruh file yang dibutuhkan untuk menjalankan aplikasi dan juga metadata seperti nama dan versi aplikasi. Gradle juga secara otomatis men-download JAR yang dibutuhkan. Pada latihan ini, seluruh JAR yang saya pakai dapat dilihat di build.gradle di bagian dependencies.

Di dunia ini tidak ada teknologi yang sempurna! Suatu hari nanti, saya mungkin perlu mengganti salah satu teknologi yang dipakai dengan yang berbeda. Saya mungkin suatu hari ini ingin beralih dari JSF ke GWT. Atau, mungkin saya ingin berpindah dari database relational menjadi database NoSQL seperti MongoDB. Tentu saja saya tidak ingin sampai harus membuat ulang aplikasi dari awal. Untuk itu, saya berjaga-jaga dengan menggunakan arsitektur layering dimana saya mengelompokkan kode program ke dalam layer yang berbeda sesuai dengan perannya. Sebagai contoh, saya membuat package seperti pada gambar berikut ini:

Struktur package di aplikasi web

Struktur package di aplikasi web

Seluruh kode program yang berkaitan dengan presentation layer terletak di package com.jocki.inventory.view. Sementara itu, seluruh kode program yang berkaitan dengan service layer berada di package com.jocki.inventory.service. Sisanya, kode program yang berkaitan dengan persistence layer (mengakses database secara langsung) berada di package com.jocki.inventory.repository.

Salah satu syarat dalam layering adalah layer yang satu hanya bisa mengakses layer sesudahnya atau sebelumnya. Pada contoh ini, presentation layer hanya bisa mengakses service layer secara langsung. Setelah itu, service layer mengakses persistence layer guna membaca data dari database. Walaupun demikian, saya juga tidak begitu kaku. Misalnya, karena memakai Spring Data JPA pada persistence layer, saya mengizinkan presentation layer untuk memanggil finders secara langsung dari persistence layer tanpa melalui service layer.

Package com.jocki.inventory.domain berisi domain class yang bisa diakses oleh layer mana saja. Isi dari package ini adalah JPA entity yang mewakili domain, misalnya Konsumen, Supplier dan Sales. Karena saya ingin mengakses domain class secara langsung termasuk di presentation layer, ada baiknya saya membuat seluruh domain class menjadi Serializable.

Sama seperti domain class, masalah validasi adalah persoalan cross cutting yang dibutuhkan semua layer. Saya menyerahkan validasi pada Bean Validation yang menggunakan annotation seperti @NotBlank, @Size dan @NotNull pada domain class. Dengan demikian, validasi yang sama dan konsisten dapat diterapkan mulai dari presentation layer hingga persistence layer. Validasi ini juga dipakai oleh JPA untuk menghasilkan tabel lengkap dengan konstrain sehingga data yang tidak valid tidak bisa disimpan di database.

Bahkan, berkat PrimeFaces yang menyediakan komponen tambahan bagi JSF, validasi ini juga akan dikerjakan oleh kode program JavaScript sebelum nilai dikirim ke server. PrimeFaces memungkinkan validasi client side bila saya menambahkan atribut validateClient="true" pada <p:commandButton>. Hasil validasi client side akan terlihat seperti pada gambar berikut ini:

Validasi secara otomatis

Validasi secara otomatis

Untuk menampilkan pesan kesalahan, saya menggunakan <p:message>. Agar label untuk setiap pesan kesalahan juga ikut menjadi berwarna merah, saya tidak menggunakan <h:outputLabel> biasa melainkan <p:outputLabel>. Secara default, pesan kesalahan berada dalam bahasa Inggris. Karena validasi dilakukan di sisi klien, pesan kesalahan di ValidationMessage.properties tidak dapat dipakai. Oleh sebab itu, saya perlu membuat file JavaScript yang berisi pesan kesalahan dalam bahasa Indonesia seperti:

PrimeFaces.locales['en_US'] = {
  ...
  messages: {
    ...
    "javax.validation.constraints.NotNull.message": "tidak boleh kosong",
    "javax.validation.constraints.Size.message": "harus diantara {0} dan {1}",
    "javax.faces.validator.BeanValidator.MESSAGE": "{0}"
    ...
  }
}

Package com.jocki.inventory.config berisi konfigurasi aplikasi web. Salah satu pergerakan yang terlihat di Spring adalah peralihan konfigurasi berbasis XML menjadi konfigurasi programmatis melalui annotation. Bukan hanya Spring, tapi juga di Java. Sejak Servlet 3.0 (bagian dari JEE 6), file konfigurasi web.xml tidak wajib ada. Sebagai informasi, dulunya, setiap servlet wajib didaftarkan di web.xml. Sebagai gantinya, developer bisa membuat file javax.servlet.ServletContainerInitializer di folder META-INF/services untuk memberikan servlet container agar mengerjakan sebuah class untuk mencari definisi servlet dan sebagainya. Beruntungnya, semua ini sudah diurus oleh Spring WebMVC sehingga saya tinggal membuat turunan AbstractAnnotationConfigDispatcherServletInitializer seperti berikut ini:

public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class[] { RootConfig.class, WebMvcConfig.class, WebFlowConfig.class };
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return null;
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/app/*" };
  }

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    servletContext.setInitParameter("javax.faces.DEFAULT_SUFFIX", ".xhtml");
    servletContext.setInitParameter("primefaces.FONT_AWESOME", "true");
    servletContext.setInitParameter("primefaces.CLIENT_SIDE_VALIDATION", "true");
    // Tambahkan konfigurasi development bila sedang tidak di-deploy di OpenShift
    if (System.getenv("OPENSHIFT_APP_NAME") == null) {
      servletContext.setInitParameter("javax.faces.PROJECT_STAGE", "Development");
      servletContext.setInitParameter("javax.faces.FACELETS_REFRESH_PERIOD", "1");
    } else {
      servletContext.setInitParameter("javax.faces.PROJECT_STAGE", "Production");
    }
    servletContext.addListener(ConfigureListener.class);
    super.onStartup(servletContext);
  }

}

Pada class di atas, saya menginstruksikan agar Spring selanjutnya membaca konfigurasi yang ada di class RootConfig, WebMvcConfig dan WebFlowConfig. Karena getServletMappings() mengembalikan /app/*, maka URL untuk aplikasi ini harus memiliki pola serupa seperti http://localhost/app/konsumen. Saya juga melakukan konfigurasi JSF pada class ini.

Aplikasi web biasanya membutuhkan state padahal protokol HTTP yang dipakai oleh website bersifat stateless. Oleh sebab itu, saya menggunakan Spring Web Flow (SWF) untuk memuluskan transisi antar halaman. SWF juga secara otomatis menerapkan POST-REDIRECT-GET sehingga tombol Back di browser dapat digunakan secara baik dan aman. Sebenarnya JSF 2.2 telah dilengkapi dengan dukungan deklarasi flow seperti pada SWF. Walaupun demikian, karena ini adalah fitur baru, saya merasa flow pada JSF masih terbatas sehingga saya memutuskan untuk tetap menggunakan SWF. Konfigurasi SWF dapat dilihat pada class WebFlowConfig yang merupakan turunan dari AbstractFacesFlowConfiguration. Class ini secara otomatis mendeklarasi mapping untuk akses ke URL seperti /javax.faces.resources/** untuk keperluan JSF.

Sebagai informasi, JavaServer Faces (JSF) adalah bagian dari Java EE yang memungkinkan developer memakai facelet dalam bentuk tag XML untuk menghasilkan komponen UI. Tujuannya adalah agar developer tidak perlu berhubungan langsung dengan semua operasi tingkat rendah yang melibatkan JavaScript, Ajax dan CSS styling. Bila komponen UI yang disediakan oleh JSF tidak cukup, pengguna bisa membuat komponen baru sendiri. Atau, pengguna juga dapat memakai komponen dari pihak ketiga seperti PrimeFaces, RichFaces, ICEfaces dan sebagainya.

Pengguna yang terbiasa membuat situs dengan PHP yang berorientasi file mungkin akan bertanya dimana sesungguhnya lokasi file dan bagaimana pemetaan file pada aplikasi web di Java. Berbeda jauh dari situs berorientasi file, sebuah servlet Java akan menerima input berupa URL tujuan lalu mengembalikan output sesuai dengan URL tersebut. Yup! Apapun URL-nya hanya akan ditangani oleh 1 servlet. Pada contoh di atas, saya secara tidak langsung hanya mendaftarkan org.springframework.web.servlet.DispatcherServlet dari Spring dan javax.faces.webapp.FacesServlet (dari Mojarra, implementasi JSF yang saya pakai). Setiap request URL akan ditangani oleh salah satu servlet di atas, lebih tepatnya adalah oleh DispatcherServlet milik Spring karena FacesServlet hanya dibutuhkan untuk inisialisasi JSF. Method yang menangani request akan terlihat seperti void service(HttpServletRequest request, HttpServletResponse response). Apa yang dikirim oleh pengguna mulai dari URL, parameter hingga cookie bisa dibaca melalui request. Untuk menulis hasil yang dikirim kepada pengguna, baik HTML, gambar, JSON, dan sebagainya, cukup tulis byte per byte melalui response.

Terlihat rumit? Saya tidak perlu melakukan pemograman tingkat rendah sampai ke servlet karena semua ini sudah ditangani oleh Spring WebMVC. Pada konfigurasi WebMvcConfig, saya membuat kode program seperti berikut ini:

@Configuration
@EnableWebMvc
@ComponentScan
public class WebMvcConfig extends WebMvcConfigurerAdapter {

  @Inject
  private WebFlowConfig webFlowConfig;

  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/error").setViewName("error");
  }

  @Bean
  public UrlBasedViewResolver faceletsViewResolver() {
    UrlBasedViewResolver resolver = new UrlBasedViewResolver();
    resolver.setViewClass(JsfView.class);
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".xhtml");
    return resolver;
  }

  ...
}

Pada konfigurasi di atas, saya memetakan URL /error dengan sebuah view bernama error di addViewControllers(). Dengan demikian, bila pengguna membuka URL seperti http://localhost/app/error, maka view dengan nama error akan ditampilkan. UrlBasedViewResolver yang saya pakai akan mencari view error di lokasi /WEB-INF/views/error.xhtml yang harus dalam bentuk facelet (JSF). Bila facelet tidak ditemukan, maka Spring mencari file dengan nama yang sama persis di src/main/webapp. Bila file tidak juga ditemukan, pesan kesalahan 404 akan diberikan pada pengguna. Pengguna tidak akan pernah bisa mengakses isi folder WEB-INF secara langsung walaupun folder ini terletak di dalam src/main/webapp.

Konfigurasi pada class RootConfig berisi hal diluar presentation layer seperti berikut ini:

@Configuration
@EnableJpaRepositories(basePackages="com.jocki.inventory.repository")
@EnableTransactionManagement
@ComponentScan("com.jocki.inventory")
public class RootConfig {

  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws NamingException {
    LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
    emf.setDataSource(dataSource());
    emf.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
    emf.setPackagesToScan("com.jocki.inventory.domain");

    Map<String,? super Object> jpaProperties = new HashMap<>();
    jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect");
    emf.setJpaPropertyMap(jpaProperties);

    return emf;
  }

  @Bean
  public DataSource dataSource() throws NamingException {
    JndiTemplate jndiTemplate = new JndiTemplate();
    DataSource dataSource = (DataSource) jndiTemplate.lookup("java:comp/env/jdbc/inventory");
    return dataSource;
  }

  @Bean
  public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emf);
    return transactionManager;
  }

}

Pada konfigurasi di atas, saya melakukan pengaturan JPA dan data source yang dipakai. Saya menambahkan annotation @EnableTransactionManagement sehingga transaksi pada service layer dapat diaktifkan. Selain itu, saya menambahkan @EnableJpaRepositories agar Spring Data JPA bisa secara otomatis menghasilkan implementasi dari interface repository yang ada. Saya juga menggunakan JNDI sehingga saya tidak perlu mendeklarasikan lokasi, nama pengguna dan password untuk database secara langsung di dalam kode program. Sebagai gantinya, server yang menjalankan aplikasi ini harus mendeklarasikan resource JNDI dengan nama jdbc/inventory yang berisi DataSource yang mewakili database yang hendak dipakai. Pada Tomcat, deklarasi ini dapat dilakukan dengan menambahkan baris seperti berikut ini pada file context.xml di folder conf:

<Resource auth="Container" driverClassName="com.mysql.jdbc.Driver" name="jdbc/inventory"
password="12345" type="javax.sql.DataSource" url="jdbc:mysql://localhost/latihandb"
username="namauser" validationQuery="/* ping */ SELECT 1"/>

Pada server percobaan, saya membuat resource jdbc/inventory yang merujuk pada database lokal. Untuk live server, tentunya resource ini harus merujuk pada database di live server. Peralihan ke database yang berbeda ini dapat berlangsung secara transparan tanpa harus mengubah kode program karena informasi koneksi database tidak tersimpan di dalam aplikasi.

Walaupun idealnya konfigurasi web.xml bisa dibuang sepenuhnya, untuk saat ini, saya masih tidak bisa lepas dari web.xml. Sebagai contoh, deklarasi halaman kesalahanan dan referensi JNDI tetap dibutuhkan pada web.xml:

<error-page>
  <location>/app/error</location>
</error-page>
<resource-ref>
  <description>Default Datasource</description>
  <res-ref-name>jdbc/inventory</res-ref-name>
  <res-type>javax.sql.DataSource</res-type>
  <res-auth>Container</res-auth>
</resource-ref>

Pada deklarasi di atas, bila terjadi kesalahan pada aplikasi web, maka URL /app/error akan ditampilkan. Sejak Servlet 3.0, saya bisa menggunakan 1 halaman error generik untuk seluruh jenis kesalahan yang ada. Sesuai dengan konfigurasi yang saya lakukan pada WebMvcConfig, URL ini akan menampilkan facelet yang terletak di src/main/webapp/WEB-INF/views/error.xhtml. Pada facelet ini, saya bisa mengakses informasi lebih lengkap mengenai kesalahan yang terjadi melalui EL seperti #{request.getAttribute('javax.servlet.error.message')} atau #{request.getAttribute('javax.servlet.error.status_code')}.

Sebuah halaman web biasanya memiliki template yang konsisten, misalnya mengandung bagian header dan footer. Oleh sebab itu, daripada mengetik ulang bagian yang sama berkali-kali (dan mengubahnya berkali-kali bila salah!), saya akan menggunakan fasilitas template dari JSF. Untuk itu, saya mendeklarasikan sebuah template di src/main/webapp/WEB-INF/layouts/standard.xml yang isinya seperti berikut ini:

<html ...>
<f:view contentType="text/html">
    <h:head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
        <title>Basic Inventory Web Demo</title>
        <h:outputStylesheet name="css/basic.css" /> 
        <h:outputScript name="js/locales.js" /> 
        <ui:insert name="headIncludes" />   
    </h:head>
    <h:body>
        <p:layout fullPage="true" style="border: 0px;">
            <p:layoutUnit position="west" resizable="true" collapsible="true" styleClass="main-menu">
                ...
            </p:layoutUnit>
            <p:layoutUnit position="center" styleClass="main-panel">
                <h:form id="mainForm">
                    <ui:insert name="content" />
                </h:form>
            </p:layoutUnit>
            <p:layoutUnit position="south" styleClass="main-panel">
                ...
            </p:layoutUnit>                       
        </p:layout>               
    </h:body>
</f:view>
<p:ajaxStatus onerror="PF('dialogKesalahan').show();"/>                                                   
<p:dialog header="Terjadi Kesalahan" id="dialogKesalahan" widgetVar="dialogKesalahan" modal="true" resizable="false">   
    <p>Telah terjadi kesalahan saat berkomunikasi dengan server.</p>
    <p>Silahkan perbaharui halaman atau ulangi beberapa saat kemudian!</p>
</p:dialog>
...
</html>

Pada facelet di atas, selain layout, saya juga menambahkan p:ajaxStatus secara global untuk setiap halaman yang ada sehingga pesan kesalahan akan selalu muncul bila terjadi kesalahan Ajax pada halaman mana saja seperti yang terlihat pada gambar berikut ini:

Tampilan pesan kesalahan pada Ajax

Tampilan pesan kesalahan pada Ajax

Salah satu kasus dimana pesan kesalahan ini akan selalu muncul adalah bila pengguna membuka sebuah halaman JSF dan meninggalkannya untuk jangka waktu yang lama. JSF menyimpan informasi komponen view yang dihasilkannya ke dalam session. Secara default, session di Tomcat diakhiri bila tidak ada aktifitas dari pengguna setelah 30 menit. Hal ini perlu dilakukan untuk mengirit memori. Bila pengguna mencoba memakai komponen JSF setelah session berakhir, ia akan menemukan setiap tombol tidak akan bekerja. Solusi untuk masalah ini cukup sederhana: pengguna harus me-refresh halaman untuk memulai session baru. PrimeFaces 5 sebenarnya memiliki komponen <p:ajaxExceptionHandler> untuk menghasilkan informasi kesalahan yang lebih jelas tetapi ia membutuhkan deklarasi halaman kesalahan secara eksplisit di web.xml.

File yang memakai template perlu menggunakan ui:composition dan mengisi nilai atribut template sesuai dengan lokasi file template yang hendak dipakai. Selain itu, pengguna template juga perlu menggunakan ui:define untuk menyisipkan nilai pada ui:insert di template. Sebagai contoh, facelet untuk pesan kesalahan akan terlihat seperti berikut ini:

<ui:composition ...  template="/WEB-INF/layouts/standard.xhtml">
    <ui:define name="content">
        <div class="ui-message-error ui-widget ui-corner-all">          
            <div class="ui-message-error-detail">
                <p><strong>Telah terjadi kesalahan saat menampilkan halaman ini!</strong></p>               
                <p>Status kesalahan: <strong><h:outputText value="#{request.getAttribute('javax.servlet.error.status_code')}"/></strong></p>
                ...
            </div>
        </div>
    </ui:define>
</ui:composition>

Saya mendeklarasikan setiap flow SWF dalam folder masing-masing di src/main/webapp/WEB-INF/flows. Sebagai contoh, URL seperti app/konsumen akan mengakses flow yang didefinisikan di src/main/webapp/WEB-INF/flows/konsumen/konsumen-flow.xml. SWF mendukung flow inheritance sehingga saya dapat memakai ulang pola flow yang sama berkali-kali. Sebagai contoh, semua flow untuk operasi CRUD diturunkan dari src/main/webapp/WEB-INF/flows/crud/crud-flow.xml yang memiliki aliran seperti:

Contoh flow di SWF

Contoh flow di SWF

Selain flow inheritance, SWF juga mendukung embedded flow. Ini adalah solusi yang tepat untuk Ajax. Sebagai contoh, pada flow di atas, bila pengguna melakukan aksi edit atau tambah, maka flow src/main/webapp/WEB-INF/flows/crud/entry/entry-flow.xml akan dikerjakan:

<flow ... abstract="true">

    <input name="selectedEntity" />    

    <view-state id="entry">
        <on-render>
            <set name="viewScope.updateMode" value="(selectedEntity != null) and (selectedEntity.id != null)" />
        </on-render>  
        <transition on="simpan" />
        <transition on="kembali" to="kembali" bind="false" validate="false" />
        <transition on="update" to="updateAction" />
    </view-state>     

    <action-state id="updateAction">                                        
        <transition on="success" to="kembali" />
        <transition on="error" to="entry" />  
    </action-state>

    <end-state id="kembali" />

</flow>

Karena saya menambahkan atribut abstract dengan nilai true, flow di atas tidak akan pernah bisa dipanggil secara langsung dengan URL seperti /app/crud/entry. Bila dipakai sebagai embedded flow, setiap perpindahan view-state pada flow di atas tidak akan menyebabkan request halaman baru (tidak ada POST-REDIRECT-GET) sehingga semuanya bisa berlangsung tanpa merefresh halaman secara penuh.

Contoh flow yang mengimplementasikan crud/entry adlaah konsumen/entry yang definisinya terlihat seperti berikut ini:

<flow ... parent="crud/entry">

  <view-state id="entry">

    <on-entry>
      <evaluate expression="kotaRepository.findAll()" result="viewScope.daftarKota" />
    </on-entry>

    <transition on="simpan">
      <evaluate expression="konsumenAction.simpan(selectedEntity, flowRequestContext)" />
    </transition>

    <transition on="kembali" to="kembali" bind="false" validate="false" />

    <transition on="update" to="updateAction" />

  </view-state>       

  <action-state id="updateAction">
     <evaluate expression="konsumenAction.update(selectedEntity, flowRequestContext)" />        
  </action-state>

</flow>

Pada saat view entry pertama kali dikerjakan, method findAll() dari kotaRepository akan dikerjakan dan hasilnya akan ditampung sebagai variabel di daftarKota (dengan view scope). Secara default, id dari view-state dipakai untuk menentukan facelet yang hendak ditampilkan, dalam hal ini adalah src/main/webapp/WEB-INF/konsumen/entry/entry.xhtml yang isinya seperti berikut ini:

<ui:composition ... template="/WEB-INF/layouts/standard.xhtml">
    <ui:define name="content">              
        <p:growl for="pesanInformasi" globalOnly="true" showDetail="true" />
        <p:panel id="panel" columns="3" styleClass="entry" header="#{updateMode? 'Edit Konsumen': 'Tambah Konsumen'}">
            <p:focus />
            <h:panelGrid columns="3" cellpadding="5">

                <p:outputLabel for="id" value="Id:" rendered="#{updateMode}"/>
                <p:inputText id="id" size="50" value="#{selectedEntity.id}" disabled="true" rendered="#{updateMode}"/>
                <p:message for="id" display="text" rendered="#{updateMode}"/>

                <p:outputLabel for="nama" value="Nama:" />
                <p:inputText id="nama" size="50" value="#{selectedEntity.nama}">
                    <p:clientValidator event="keyup" />
                </p:inputText>
                <p:message for="nama" display="text" />

                <p:outputLabel for="alamat" value="Alamat:" />
                <p:inputTextarea id="alamat" rows="5" cols="50" value="#{selectedEntity.alamat}">
                    <p:clientValidator event="keyup" />
                </p:inputTextarea>                                                    
                <p:message for="alamat" display="text" />

                <p:outputLabel for="kota" value="Kota:" />
                <p:selectOneMenu id="kota" value="#{selectedEntity.kota}" filter="true" converter="#{kotaConverter}">
                    <f:selectItems value="#{daftarKota}" var="vKota" itemValue="#{vKota}"/>
                  </p:selectOneMenu>  
                <p:message for="kota" display="text" />           

            </h:panelGrid>
            <div class="buttonPanel">
                <p:commandButton id="simpan" value="Simpan" icon="fa fa-floppy-o" action="simpan" validateClient="true" rendered="#{!updateMode}" update="mainForm"/>
                <p:commandButton id="update" value="Update" icon="fa fa-edit" action="update" validateClient="true" rendered="#{updateMode}" update="mainForm"/>                    
                <p:commandButton id="kembali" value="Kembali" icon="fa fa-arrow-left" action="kembali" immediate="true" />
            </div>                                    
        </p:panel>                            
    </ui:define>  
</ui:composition>

Pada facelet di atas, saya melakukan binding dari setiap komponen input ke variabel selectedEntity di flow melalui atribut value. Saya menambahkan p:clientValidator agar validasi JavaScript langsung dilakukan pada saat saya mengetik di komponen input. Khusus untuk p:selectOneMenu yang menampilkan pilihan daftar kota, saya perlu memberikan variabel daftar kota yang sudah saya query sebelumnya. Selain itu, saya juga perlu memberikan sebuah converter yang bisa menerjemahkan id kota menjadi nama kota dan sebaliknya (dapat dijumpai di src/main/java/view/converter/KotaConverter). Bila saya menampilkan view ini di browser dan nilai selectedEntity adalah null, saya akan memperoleh tampilan seperti berikut ini:

Contoh tampilan data entry

Contoh tampilan data entry

Bila seandainya selectedEntity tidak bernilai null, maka saya akan memperoleh tampilan seperti berikut ini:

Contoh tampilan edit

Contoh tampilan edit

Pada <p:commandButton>, saya menambahkan atribut update dengan nilai mainForm supaya saat tombol di-klik, hanya panel ini saja yang perlu diperbaharui melalui Ajax. Nilai action seperti 'simpan', 'update' atau 'kembali' harus sesuai dengan yang saya pakai di &lt;transition&gt; di deklarasi flow. Khusus untuk tombol kembali, saya menambahkan atribute immediate dengan nilai true agar validasi dan binding tidak dilakukan.

Pada view display, saya menggunakan sebuah <p:dataTable> yang dilengkapi dengan halaman, filter, dan pengurutan seperti yang terlihat pada gambar berikut ini:

Contoh tampilan tampilan tabel

Contoh tampilan tampilan tabel

Untuk meningkatkan kinerja, daripada men-query isi tabel sekaligus, saya hanya men-query 10 record saja per halaman. Begitu pengguna men-klik nomor halaman yang berbeda, kode program akan men-query 10 record lain yang dibutuhkan (melalui Ajax). Hal ini dapat dicapai dengan memberikan nilai "true" pada atribut lazy. Selain itu, saya juga perlu sebuah turunan LazyDataModel. Pada class ini, terdapat method seperti:

List load(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters)

yang perlu di-override. Beruntungnya, Spring Data JPA mendukung pembatasan query per halaman dengan menggunakan interface Pageable (bentuk konkrit-nya adalah PageRequest). Pageable juga dapat dipakai untuk menyimpan informasi pengurutan.

Untuk membuat persistence layer, saya cukup membuat interface seperti:

public interface KonsumenRepository extends JpaRepository<Konsumen, Long> {

  Page findByNamaLike(String nama, Pageable pageable);

  Page findByAlamatLike(String alamat, Pageable pageable);

  Page findByKota_NamaLike(String namaKota, Pageable pageable);

}

Spring Data JPA akan membuat implementasi dari interface ini secara otomatis. Saya hanya perlu menambahkan @Inject pada class lain yang perlu memakai interface ini. Seluruh method yang ada mengembalikan object Page. Object ini tidak hanya berisi daftar Konsumen yang di-query, tetapi juga informasi lain seperti jumlah seluruh konsumen yang ada. Informasi seperti ini dibutuhkan untuk membuat daftar halaman, menentukan apakah ini adalah halaman pertama atau halaman terakhir, dan sebagainya. Selain finders, JpaRepository sudah menyediakan method seperti save() dan delete() untuk melakukan operasi CRUD pada sebuah JPA entity.

Pada service layer, saya membuat class seperti:

@Service @Transactional
public class KonsumenService {

  @Inject
  private transient KonsumenRepository konsumenRepository;

  @Override
  public Konsumen simpan(Konsumen konsumen) {
    return konsumenRepository.save(konsumen);
  }

  @Override
  public Konsumen update(Konsumen konsumenBaru) {
    Konsumen konsumen = konsumenRepository.findOne(konsumenBaru.getId());
    konsumen.setNama(konsumenBaru.getNama());
    konsumen.setAlamat(konsumenBaru.getAlamat());
    konsumen.setKota(konsumenBaru.getKota());
    return konsumen;
  }

  @Override
  public void hapus(Konsumen konsumen) {
    konsumenRepository.delete(konsumen.getId());
  }

}

Saya menggunakan annotation @Service untuk memberi tanda bahwa class di atas adalah bagian dari service layer. Selain itu, saya juga memberikan annotation @Transactional agar setiap method di dalam class ini dikerjakan dalam sebuah transaksi database. Bila terjadi kesalahan selama eksekusi sebuah method yang diindikasi oleh sebuah Exception, maka proses rollback harus dilakukan sehingga tidak ada perubahan yang terjadi pada database. Saya tidak menggunakan save() pada method update() melainkan men-update satu per satu atribut yang ada karena pada domain class yang kompleks, sering kali method update() hanya boleh memperbaharui sebagian atribut saja.

Berkat Spring container, saya bisa memakai class service layer di presentation layer dengan menggunakan @Inject seperti pada:

@Component
public class KonsumenAction {

  @Inject
  private KonsumenService konsumenService;

  ...

}

Saya kemudian bisa memakainya seperti pada:

public String update(@NotNull Konsumen entity, RequestContext context) {
  try {
    konsumenService.update(entity);
    return "success";
  } catch (Exception ex) {
    addErrorMessage(ex.getMessage());
    return "error";
  }
}

public void addInfoMessage(String message) {
  FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_INFO, "Sukses", message);
  FacesContext.getCurrentInstance().addMessage("pesanInformasi", facesMessage);
}

public void addErrorMessage(String message) {
  FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Terjadi kesalahan", message);
  FacesContext.getCurrentInstance().addMessage("pesanInformasi", facesMessage);
}

Method simpan() mengembalikan sebuah String yang mewakili transisi yang akan dilakukan pada SWF. Selain itu, saya juga bisa menambahkan pesan informasi dalam bentuk FacesMessage yang bisa ditampilkan oleh PrimeFaces melalui p:growl seperti yang terlihat pada gambar berikut ini:

Tampilan pesan informasi melalui growl

Tampilan pesan informasi melalui growl

Karena KonsumenAction memiliki annotation @Component, maka singleton-nya sudah disediakan oleh Spring sehingga saya bisa langsung memakainya di SWF seperti pada contoh berikut ini:

<action-state id="updateAction">
    <evaluate expression="konsumenAction.update(selectedEntity, flowRequestContext)" />
    <transition on="success" to="kembali" />
    <transition on="error" to="entry" />          
</action-state>

Sebagai langkah terakhir, saya perlu men-deploy aplikasi web ini ke sebuah server. Bila saya menjalankan perintah gradle build, saya akan memperoleh sebuah file bernama ROOT.war di folder webapps. Masalahnya adalah kemana saya harus meletakkan file ini? Salah satu solusi gratis yang bisa saya pakai adalah dengan menggunakan OpenShift (https://www.openshift.com/) yang menyediakan layanan PaaS. Saya mulai dengan membuat sebuah cartridge DIY (Do It Yourself) baru dan men-install Tomcat 8 pada cartridge tersebut. Saya juga menambahkan sebuah cartridge MySQL untuk memperoleh sebuah database MySQL. Setelah men-setup script di folder .openshift dan memindahkan kode program ke server OpenShift dengan menggunakan Git, aplikasi bisa di-deploy dan dijalankan.

Salah satu hal yang harus diperhatikan saat memakai OpenShift adalah hanya port 15000 – 35530 yang boleh digunakan. Port 8080 adalah satu-satunya yang diperbolehkan diluar port yang diizinkan. Setiap request HTTP (di port 80) akan di forward ke port 8080 di server virtual saya. Saya juga perlu mengubah context.xml di server Tomcat 8 yang saya install agar memiliki resource bernama jdbc/inventory yang merujuk pada database MySQL. Saya bisa memperoleh informasi mengenai alamat host, port, user dan ip dengan melihat isi environment variable seperti OPENSHIFT_MYSQL_DB_HOST, OPENSHIFT_MYSQL_DB_PORT, OPENSHIFT_MYSQL_DB_USERNAME dan OPENSHIFT_MYSQL_DB_PASSWORD. Selain itu, sebagai pengguna gratis, aplikasi yang saya deploy akan memasuki idling state bila tidak pernah diakses selama 24 jam. Ini akan menyebabkan pengguna yang pertama kali mengakses aplikasi setelah idling state harus mengalami sedikit delay.

Membuat RESTful Web Service Dengan Spring Data REST

Pada tulisan tentang web service sebelumnya, saya selalu menggunakan SOAP. Selain memakai SOAP, solusi yang kini populer adalah REST. Yup! Saya sering berpikir SOA telah terkubur karena mulai jarang diperbicarakan. Tapi ternyata masih ada buku baru Thomas Erl yang diterbitkan pada Agustus 2012 yang berjudul “SOA with REST: Principles, Patterns & Constraints for Building Enterprise Solutions with REST“.

Saya tidak akan membicarakan teori disini, tapi apa kelebihan REST? Bila SOAP adalah “early-binding” (bayangkan sebuah variabel dengan tipe data yang ketat dan sudah pasti!) maka REST adalah “late-binding” (bayangkan variabel generik tanpa tipe data seperti di JavaScript). REST lebih ringan karena bisa dipakai cukup dengan memanggil request HTTP. REST juga tidak perlu memakai WSDL untuk mengetahui kontrak servis! Loh, jika tidak ada WSDL, lalu bagaimana mengetahui layanan apa yang disediakan? REST mensyaratkan penggunaan URI dan Request Method yang harus dipatuhi. Sebagai contoh:

REST pada dasarnya adalah mengakses HTTP layaknya browsing.  Dengan demikian, tidak dibutuhkan “kode program” khusus.  Semua bahasa yang mendukung akses HTTP dapat memakai REST.  Yang membedakan REST adalah pada REST terdapat peraturan-peraturan yang harus “dipatuhi” sehingga komunikasi bisa berjalan lancar.

Walaupun demikian, REST bukanlah protokol seperti SOAP, sehingga “peraturan”-nya boleh dilanggar.  Hal ini dapat membingungkan client yang memakai.  Yup! Kadang-kadang sulit menjelaskan pada mahasiswa yang berharap menemukan kode program atau teknik baru yang mutakhir;  REST berhubungan dengan disiplin dan ‘manajemen’.  Mahasiswa yang berfokus pada membuat sistem untuk skripsi lalu meninggalkannya begitu saja setelah lulus akan sulit merasakan manfaatnya.

Saya akan mulai membuat RESTful web service dengan memakai Spring Data REST. Saya mulai dengan membuat sebuah proyek baru dengan memilih File, New, Spring Template Project di Spring Tool Suite (STS).  Pada dialog yang muncul, saya memilih Spring MVC Project dan men-klik tombol Next.  Saya mengisi nama proyek dengan rest-api dan lokasi package dengan com.jocki.rest.  Lalu, saya men-klik tombol Finish.

Setelah proyek selesai dibuat, saya me-double click file pom.xml untuk memastikan bahwa properties org.springframework-version minimal adalah versi 3.0. Selain itu, saya akan menambahkan dependency berikut ini:

  • org.hibernate : hibernate-entitymanager : 3.6.0.Final (sebagai JPA provider)
  • org.springframework.data : spring-data-jpa : 1.1.0.RELEASE
  • org.springframework : spring-tx : ${org.springframework-version}
  • org.springframework : spring-orm : ${org.springframework-version}
  • org.springframework.data : spring-data-rest-webmvc : 1.0.0.RELEASE
  • com.h2database : h2 : 1.3.167 (sebagai database in-memory)

Pada Package Explorer, saya men-klik kanan folder src/main/java, men-klik kanan dan memilih menu New, Class. Saya mengisi Package dengan com.jocki.domain, dan mengisi Name dengan Buku. Class ini akan mewakili domain class saya, dimana isi kode programnya adalah:

package com.jocki.domain;

import java.io.Serializable;

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

@Entity
@Cacheable
public class Buku implements Serializable {

	private static final long serialVersionUID = 9099989372502423316L;

	@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
	private Long id;

	private String judul;

	private String isbn;

	private Integer harga;

	public Long getId() {
		return id;
	}

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

	public String getJudul() {
		return judul;
	}

	public void setJudul(String judul) {
		this.judul = judul;
	}

	public String getIsbn() {
		return isbn;
	}

	public void setIsbn(String isbn) {
		this.isbn = isbn;
	}

	public Integer getHarga() {
		return harga;
	}

	public void setHarga(Integer harga) {
		this.harga = harga;
	}

}

Untuk melakukan operasi persistensi (CRUD ke database embedded H2), saya akan menggunakan Spring Data JPA. Untuk itu, saya men-klik kanan pada folder src/main/java, kemudian memilih New, Interface. Pada Package, saya mengisi com.jocki.repository dan pada Name, saya mengisi BukuRepository. Kode program untuk interface tersebut akan terlihat seperti berikut ini:

package com.jocki.respository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.jocki.domain.Buku;

public interface BukuRepository extends JpaRepository<Buku, Long>{

}

Saya akan mulai dengan membuka folder src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml. Lalu, saya akan menambahkan definisi bean berikut ini:

<beans:bean class="org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration" />

Bean tersebut dibutuhkan untuk men-export seluruh repository saya secara otomatis sehingga operasi persistensi data pada domain object Buku dapat diakses melalui REST.

Berikutnya, saya perlu menyiapkan konfigurasi persistensi data. Saya akan mengubah file src/main/webapp/WEB-INF/spring/root-context.xml. Saya mendouble-click file ini, kemudian memilih tab Namespaces dan memastikan bahwa saya memberi tanda centang pada beans, context, jdbc, jpa, dan tx. Lalu saya menambahkan definisi berikut ini pada file root-context.xml:

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
  <property name="entityManagerFactory" ref="emf" /> 
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="jpaVendorAdapter"> 
     <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
  </property> 
  <property name="packagesToScan" value="com.jocki.domain" /> 
  <property name="jpaProperties">
    <props>
      <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
      <prop key="hibernate.hbm2ddl.auto">create-drop</prop>
      <prop key="hibernate.id.new_generator_mappings">true</prop>
    </props>
  </property>
</bean>
<context:annotation-config />
<context:component-scan base-package="com.jocki" />
<jpa:repositories base-package="com.jocki.repository" />
<jdbc:embedded-database id="dataSource" type="H2" />

Setelah ini, saya tinggal menjalankan tc Server.  Caranya adalah dengan men-klik kanan pada nama proyek, memilih menu Run As, Run on server.  Pada dialog yang muncul, saya memilih salah satu server yang tersedia, misalnya tc Server v2.7, lalu men-klik tombol Finish. Setelah server selesai dijalankan, browser secara otomatis akan terbuka pada URL http://localhost:8080/rest.

Spring Data REST mendukung HATEOAS (Hypermedia as the Engine of Application State) dimana terdapat links dari sebuah resource ke resource lainnya secara konsisten.  Yang muncul pertama kali di URL tersebut adalah seluruh daftar resource yang disediakan oleh Spring Data RESTseperti berikut ini:

{
  "links" : [ {
    "rel" : "buku",
    "href" : "http://localhost:8080/rest/buku"
  } ],
  "content" : [ ]
}

REST tidak mensyaratkan penggunaan format tertentu untuk konten.  Client dapat memilih format dengan menyertakan header Content-Type yang diinginkan seperti application/json atau application/xml.  Tapi untuk saat ini, Spring Data REST secara bawaan (tanpa perubahan!) hanya akan mengembalikan format application/json.

Untuk menguji REST, saya akan memakai tool curl.

Untuk melihat daftar seluruh resource “buku” yang ada di database,  saya perlu memberikan method GET pada URL http://localhost:8080/rest/buku seperti yang terlihat pada gambar berikut ini:

Memakai REST untuk melihat seluruh "buku" yang tersedia.

Operasi REST untuk melihat seluruh “buku” yang tersedia.

Karena saya menurunkan interface BukuRepository dari JpaRepository, maka secara otomatis saya memiliki fitur penghalamanan (paging).

Untuk melihat method apa saja yang didukung di RESTful web services, saya perlu memberikan request HTTP dengan method OPTIONS seperti yang terlihat pada gambar berikut ini:

Melihat operasi yang didukung oleh RESTful API

Melihat operasi yang didukung oleh RESTful API

Sekarang, saya akan menambah sebuah buku baru.  Untuk itu saya meng-hit URL dengan method POST seperti pada gambar berikut ini:

Membuat resource baru dengan REST

Membuat resource baru dengan REST

Saya terpaksa memberikan perintah tersebut di console Bash karena selalu gagal di console Windows.  Sekarang, bila saya melihat daftar “Buku” yang ada, saya akan menemukan hasil seperti pada gambar berikut ini:

Melihat seluruh "Buku" yang ada dengan REST

Melihat seluruh “Buku” yang ada dengan REST

Untuk menghapus buku dengan id 1, saya bisa melakukan request HTTP dengan method DELETE, seperti yang terlihat pada gambar berikut ini:

Menghapus resource dengan REST

Menghapus resource dengan REST

Perhatikan bahwa respon yang dikembalikan adalah 204 No Content, bukan 404 Not Found dan sebagainya.  Respon tersebut menunjukkan bahwa operasi hapus telah berhasil dilakukan dan tidak ada sesuatu yang perlu dikembalikan pada client.

Bagi yang ingin memakai tools berbasis GUI, bisa mencoba plugin Firefox seperti plugin RESTClient.  Tools ini mendukung authentication dengan OAuth2, sebuah pengamanan yang umum dipakai untuk RESTful web services.  Berikut ini adalah contoh tampilan plugin RESTClient di Firefox:

Tampilan plugin RESTClient di Firefox

Tampilan plugin RESTClient di Firefox

Pada prakteknya, tentu saja operasi REST tidak dipanggil melalui cURL atau plugin Firefox, melainkan oleh sistem lain atau aplikasi lain.  Hampir semua bahasa pemograman bisa melakukan request HTTP secara bawaan.   Beberapa bahkan sudah menyediakan akses REST secara mudah, misalnya Jersey di Java atau Zend_Rest_Client di PHP (yang ini  hanya mendukung XML dan banyak “melanggar” prinsip REST).  Seorang mahasiswa yang baru belajar mungkin akan bertanya, kenapa harus selalu melibatkan sistem lain?  Karena pada dasarnya web service dan SOA adalah mengenai komunikasi antar-sistem atau antar-program!  Penerapan pada sebuah sistem tunggal tanpa komunikasi cukup disebut SOP (Service Oriented Programming).

Spring Data REST memang mempermudah meng-export repository menjadi RESTful Web Services.  Tetapi RESTful Web Services tidak hanya berisi operasi CRUD saja, melainkan juga services (business logic).  Untuk mendapatkan kendali yang lebih penuh, RESTful API dapat dibuat dari Spring Web MVC controller yang telah dilengkapi annotation yang mendukung REST.

Membuat Implementasi Auditable Untuk Spring Data Auditing

Pada tulisan Memakai Fitur Auditing di Spring Data JPA, saya memakai AbstractAuditable agar cepat.  Tapi kadang-kadang apa yang disediakan oleh AbstractAuditable berbeda dengan keinginan saya.  Sebagai contoh, AbstractAuditable diturunkan dari AbstractPersistable yang mendefinisikan id dengan @GeneratedValue(strategy=GenerationType.AUTO).  Bagaimana jika saya ingin strategi yang berbeda?  Atau, bagaimana bila saya tidak ingin menyimpan createdDate?

Saya wajib mengimplementasikan interface Auditable pada setiap domain class yang ada.  Tapi daripada membuat implementasi di seluruh domain class yang ada (terjadi duplikasi), saya sebaiknya meletakkan implementasi disebuah abstract class, yang saya beri nama AuditableDomain.  Isi dari class ini saya copy-paste dari isi class AbstractAuditable beserta AbstractPersistable, yaitu:

import java.io.Serializable;
import java.util.Date;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.MappedSuperclass;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.joda.time.DateTime;
import org.springframework.data.domain.Auditable;

@MappedSuperclass
public abstract class AuditableDomain<U,PK extends Serializable> implements Auditable<U, PK>{

    private static final long serialVersionUID = -5057318377914867780L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private PK id;

    @ManyToOne
    private U createdBy;

    @ManyToOne
    private U lastModifiedBy;

    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;

    public U getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(final U createdBy) {
        this.createdBy = createdBy;
    }

    public DateTime getCreatedDate() {
        return null
    }

    public void setCreatedDate(final DateTime createdDate) {
                // tidak melakukan apa-apa
    }

    public U getLastModifiedBy() {
        return lastModifiedBy;
    }

    public void setLastModifiedBy(final U lastModifiedBy) {
        this.lastModifiedBy = lastModifiedBy;
    }

    public DateTime getLastModifiedDate() {
        return null == lastModifiedDate ? null : new DateTime(lastModifiedDate);
    }

    public void setLastModifiedDate(final DateTime lastModifiedDate) {
        this.lastModifiedDate = null == lastModifiedDate ? null : lastModifiedDate.toDate();
    }

    public PK getId() {
        return id;
    }

    protected void setId(final PK id) {
        this.id = id;
    }

    public boolean isNew() {
        return null == getId();
    }
}

Perbedaannya dengan class AbstractAuditable adalah pada class AuditableDomain, saya bisa mengubah annotation yang  dipakai sesuka hati saya, misalnya saya memakai strategi GenerationType.IDENTITY dalam menghasilkan id untuk domain class yang ada.   Selain itu, saya juga menghilangkan atribut createdDate karena domain class saya sudah punya atribut tanggal.

Saya memastikan bahwa class AuditableDomain memiliki annotation @MappedSuperclass sehingga Hibernate JPA akan memproses annotation di class ini bila seandainya class lain diturunkan dari class ini.   Sekarang, saya tinggal menurunkan semua domain class saya dari class AuditableDomain seperti yang terlihat dari kode program berikut ini:

@Entity
@Table(name="tblPemesanan")
public class Pemesanan extends AuditableDomain<User, Long> implements Serializable {
  ...
}

Memakai Fitur Auditing di Spring Data JPA

Auditing untuk domain class adalah salah satu fitur yang sering kali dibutuhkan.  Stakeholder perlu mengetahui siapa yang terakhir kali mengubah data sebuah entitas stok.  Mereka juga mungkin ingin tahu kapan sebuah stok dibuat dan dimodifikasi.   Oleh sebab itu saya perlu menambahkan atribut seperti createdBy, createdDate, lastModifiedBy, dan lastModifiedDate pada setiap domain class yang perlu di-audit.  Beruntungnya, jika saya memakai Spring Data JPA, saya tidak perlu menambahkan atribut beserta logic kode program tersebut secara manual.  Spring Data JPA sudah memiliki fitur auditing.  Syaratnya adalah saya harus menambahkan dependency ke artifact spring-aspects.

Dukungan auditing di Spring Data JPA bisa dibilang masih sangat sederhana bila dibandingkan dengan Hibernate Enver yang memiliki tabel riwayat audit.  Tapi kadang-kadang kebutuhan auditing tidak perlu selengkap itu.

Untuk memakai fasilitas auditing di Spring Data JPA, saya perlu mengimplementasikan interface Auditable<U, ID extends Serializable>.  Cara yang lebih cepat untuk memperoleh semua atribut auditing (createdBy, createdDate, lastModifiedBy, dan lastModifiedDate) adalah dengan menurunkan domain class dari class AbstractAuditable<U, PK extends Serializable>.  Sebagai contoh, bila saya ingin menambahkan fitur auditing di domain class Pemesanan,  maka kode program untuk domain class tersebut akan terlihat seperti:

@Entity
@Table(name="pemesanan")
public class Pemesanan extends AbstractAuditable<User, Long> implements Serializable {

  @ElementCollection(fetch=FetchType.EAGER)
  @CollectionTable(joinColumns=@JoinColumn(name="pemesanan_id"))
  private List<ItemPemesanan> listItemPemesanan;

  @Column(name="status")
  @private Status status;

  // ...
  // getter dan setter diabaikan
  // ...
}

Pada class Pemesanan, saya hanya perlu mendefinisikan atribut yang berhubungan dengan pemesanan.  Dengan meng-extends class AbstractAuditable<User, Long>, saya secara otomatis telah memperoleh atribut createdBy, createdDate, lastModifiedBy, dan lastModifiedDate beserta getter dan setter-nya.

Type parameter U di AbstractAuditable untuk class Pemesanan di atas diwakili oleh User, dimana User adalah sebuah domain class buatan saya yang mewakili pengguna yang dapat login ke aplikasi.  Type parameter ini menentukan nilai yang akan dikembalikan oleh getCreatedBy() dan getLastModifiedBy().

Type parameter PK di AbstractAuditable untuk class Pemesanan di atas diwakili oleh Long.   Tipe ini menunjukkan tipe untuk id (primary key) dari domain class yang bersangkutan.   Dengan menurunkan domain class dari AbstractAuditable, saya TIDAK perlu lagi mendefinisikan id secara manual karena domain class secara otomatis akan memilih method getId(), setId(), dan isNew().

Langkah berikutnya saya perlu menambahkan entity listener pada setiap domain class.  Cara no-brainer (ga pakai mikir) adalah dengan menambahkan annotation @EntityListener di setiap domain class yang ada.  Tapi ada cara otomatis yang lebih disarankan.  Saya perlu menambahkan file orm.xml yang akan memberikan definisi entity listener secara global.   Saya akan membuat file orm.xml bila belum ada, dengan men-klik kanan pada  folder src/main/resources/META-INF, memilih menu New, Other, lalu pada dialog yang muncul, saya memilih JPA, JPA ORM Mapping File, seperti yang terlihat pada gambar berikut ini:

Membuat file orm.xml

Membuat file orm.xml

Saya men-klik Next, lalu memastikan bahwa nama file adalah orm.xml.  Bila Eclipse tidak memberikan pilihan Next atau Finish, pastikan bahwa project facet JPA sudah diaktifkan (cara memeriksanya adalah dengan men-klik kanan nama proyek, memilih properties, lalu memilih Project Facets.  Saya boleh menghilangkan tanda centang di JPA setelah file ORM dibuat).  Saya kemudian men-klik tombol Next, mencentang pilih Add to persitence unit.  Setelah itu  saya men-klik Finish.  Kemudian saya melakukan perubahan sehingga isi file orm.xml terlihat seperti berikut ini:

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings version="2.0" xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd">
    <persistence-unit-metadata>
        <persistence-unit-defaults>
            <entity-listeners>
                <entity-listener class="org.springframework.data.jpa.domain.support.AuditingEntityListener" />
            </entity-listeners>
        </persistence-unit-defaults>
    </persistence-unit-metadata>
</entity-mappings>

Langkah berikutnya adalah membuat sebuah Spring bean yang meng-implementasi-kan inteface AuditorAware.  Bean ini diperlukan supaya Spring Data JPA tahu user mana yang sedang aktif atau sedang login.  Agar mudah, saya memakai ulang UserServiceImpl yang menyediakan services yang berkaitan dengan user.  Karena saya memakai Spring Security untuk menangani proses login, maka kode program di UserServiceImpl saya akan terlihat seperti:

@Service("userService")
@Repository
public class UserServiceImpl implements UserService, AuditorAware<User> {
   // ...
   // kode program yang sudah ada di UserServiceImpl tidak ditampilkan disini.
   // ...

   @Override
   public User getCurrentAuditor() {
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      if (authentication==null) return null;
      return getUser(authentication.getName());
   }
}

Langkah terakhir adalah mendaftarkan userService di file konfigurasi Spring.  Saya menambahkan baris berikut pada file konfigurasi Spring:

<jpa:auditing auditor-aware-ref="userService" />

Pada bagian kode program yang melakukan penyimpan, saya melakukan sedikit perubahan sehingga terlihat seperti berikut ini:

public Pemesanan savePemesanan(Pemesanan pemesanan) {
  if (!pemesanan.isNew()) {
    Pemesanan oldPemesanan = pemesananRepository.findOne(pemesanan.getId());
    pemesanan.setCreatedBy(oldPemesanan.getCreatedBy());
    pemesanan.setCreatedDate(oldPemesanan.getCreatedDate());
  }
  return pemesananRepository.saveAndFlush(pemesanan);
}

Karena saya selalu membuat domain object baru dari web front end, baik proses insert maupun update, maka kode program di atas diperlukan agar nilai atribut createdBy dan createdDate tidak hilang bila proses update berlangsung.   Jika seandainya saya tetap memakai domain object yang sama, maka saya dapat  langsung menyimpan dengan saveAndFlush() di repository.

Setelah ini, setiap kali  domain object Pemesanan dibuat, maka atribut createdBy dan createdDate. Begitu juga lastModifiedBy dan lastModifiedDate akan di-isi secara otomatis pada saat domain object Pemesanan diubah.  Nilai createdBy dan lastModifiedBy akan diambil dari user yang sedang login.  Nilai createdDate dan lastModifiedDate akan diambil dari waktu saat proses create atau insert berlangsung.

Memakai Querydsl Dengan Spring Data JPA

Spring Data JPA sangat mempermudah hidup saya karena sekarang saya cukup mengetik deklarasi method untuk mewakili query ke database. Tapi pada kasus tertentu, saya tetap merasa Spring Data repository saya masih bisa dirapikan. Sebagai contoh, saya memiliki sebuah tampilan seperti berikut ini:

Dialog Pencarian

Dialog Pencarian

Tampilan seperti ini memungkinkan pengguna untuk melakukan beberapa jenis pencarian, anggap saja pencarian “sama dengan” dan pencarian “mengandung“.  Seandainya ada 2 atribut di domain class yang dijadikan patokan pencarian, maka di Spring Data repository, saya harus membuat 4 method yang mewakili 4 query yang berbeda, seperti berikut ini:

public interface PemesananRepository extends JpaRepository<Pemesanan, Long> {

  @Query("..[query diabaikan]..")
  Page<Pemesanan> findPagePemesananByKodePemesanan(@Param("kodePemesanan") String kodePemesanan, Pageable pageable);

  @Query("..[query diabaikan]..")
  Page<Pemesanan> findPagePemesananByKodePemesanan(@Param("kodePemesanan") String kodePemesanan, Pageable pageable);  

  @Query("..[query diabaikan]..")
  Page<Pemesanan> findPagePemesananByNama(@Param("nama") String nama, Pageable pageable);  

  @Query("..[query diabaikan]..")
  Page<Pemesanan> findPagePemesananMengandungNama(@Param("nama") String nama, Pageable pageable);

}

Sekarang, bayangkan domain class Pemesanan diturunkan menjadi PemesananWeb dan PemesananKunjungan!  Kedua turunan Pemesanan ini akan ditampilkan dalam dua (2)  halaman yang berbeda!!  Karena setiap halaman wajib memiliki fitur pencarian, maka saya harus mengubah kode program di Spring Data repository menjadi mengandung delapan (8) method yang berbeda seperti berikut ini:

public interface PemesananRepository extends JpaRepository<Pemesanan, Long> {

  @Query("..[query diabaikan]..")
  Page<PemesananWeb> findPagePemesananWebByKodePemesanan(@Param("kodePemesanan") String kodePemesanan, Pageable pageable);

  @Query("..[query diabaikan]..")
  Page<PemesananWeb> findPagePemesananWebMengandungKodePemesanan(@Param("kodePemesanan") String kodePemesanan, Pageable pageable);  

  @Query("..[query diabaikan]..")
  Page<PemesananWeb> findPagePemesananWebByNama(@Param("nama") String nama, Pageable pageable);  

  @Query("..[query diabaikan]..")
  Page<PemesananWeb> findPagePemesananWebMengandungNama(@Param("nama") String nama, Pageable pageable);

  @Query("..[query diabaikan]..")
  Page<PemesananKunjungan> findPagePemesananKunjunganByKodePemesanan(@Param("kodePemesanan") String kodePemesanan, Pageable pageable);

  @Query("..[query diabaikan]..")
  Page<PemesananKunjungan> findPagePemesananKunjunganMengandungKodePemesanan(@Param("kodePemesanan") String kodePemesanan, Pageable pageable);  

  @Query("..[query diabaikan]..")
  Page<PemesananKunjungan> findPagePemesananKunjunganByNama(@Param("nama") String nama, Pageable pageable);  

  @Query("..[query diabaikan]..")
  Page<PemesananKunjungan> findPagePemesananKunjunganMengandungNama(@Param("nama") String nama, Pageable pageable);

}

Baru untuk pencarian saja, data repositorysaya sudah menjadi penuh!  Apa ada cara lain?   Jika saya berpikir untuk menghasilkan string JPQL/SQL secara manual dengan “if” , maka saya akan kehilangan fitur pageable (penghalaman) dari Spring Data.  Selain itu, menggunakan “if” dalam menghasilkan string JPQL/SQL rentan terhadap kesalahan!  Hal ini karena saya tidak akan mendapatkan “syntax error”  atau garis bawah warna merah di Eclipse jika saya salah ketik. Padahal impian semua developer adalah dapat secepat mungkin mengetahui bahwa dirinya telah berbuat kesalahan.  Yup! Developer yang tidak confidence atau sedang galau terhadap kode program yang barusan dibuatnya  biasanya akan setengah-tengah dalam melanjutkan membuat kode program.   Dan saya tidak bisa yakin JPQL/SQL yang saya buat sepenuhnya benar, bila saya belum menelusuri seluruh alur “if” yang ada!

Bagaimana dengan cara lain yang lebih elegan?  Saya bisa mencoba memakai Querydsl yang mirip dengan Criteria API.  Querydsl memungkinkan developer untuk menulis query melalui class, sehingga saat membuat kode program untuk query, developer dapat memakai Ctrl+Space dan memilih fungsi, nama atribut dan sebagainya.  Selain itu, karena ini JPQL/SQL dalam bentuk kode program, maka kesalahan ketik akan langsung ketahuan.

Sebelum dapat membuat kode program Querydsl, saya harus men-setup  Eclipse terlebih dahulu.  Langkah-langkah yang saya tempuh dapat dibaca di Men-setup Querydsl di Eclipse.

Setelah itu, saya dapat menghapus semua method (8 method pencarian di atas!) yang ada di Spring Data repository.  Saya perlu mengimplementasikan interface QueryDslPredicateExecutor.  Kode program berikut ini memperlihatkan isi Spring Data repository saya:

public interface PemesananRepository extends JpaRepository<Pemesanan, Long>, QueryDslPredicateExecutor<Pemesanan> {

  // isinya kosong

}

Selanjutnya, saya perlu menambahkan kode Querydsl pada kode program yang akan memanggil Spring Data repository.  Sebagai contoh, method yang dipakai untuk mencari pada PelangganWeb isinya akan terlihat seperti:

@Transactional(readOnly=true)
public Page getPagePencarianPemesananWeb(String field, String nilai, 
	JENISPENCARIAN jenisPencarian, Pageable pageable) {

  QPemesanan pemesanan = QPemesanan.pemesanan;
  QPemesananWeb pemesananWeb = pemesanan.as(QPemesananWeb.class);

  BooleanBuilder builder = new BooleanBuilder();

  if (field.equals("kodePemesanan")) {
	if (jenisPencarian==JENISPENCARIAN.SAMADENGAN) {				
		builder.and(pemesananWeb.kodePemesanan.equalsIgnoreCase(nilai));
	} else if (jenisPencarian==JENISPENCARIAN.MENGANDUNG) {
		builder.and(pemesananWeb.kodePemesanan.containsIgnoreCase(nilai));
	}
  } else if (field.equals("nama")) {
	if (jenisPencarian==JENISPENCARIAN.SAMADENGAN) {
		builder.and(pemesananWeb.pelangganWeb.nama.equalsIgnoreCase(nilai));
	} else if (jenisPencarian==JENISPENCARIAN.MENGANDUNG) {
		builder.and(pemesananWeb.pelangganWeb.nama.containsIgnoreCase(nilai));
	}
  }
  return pemesananRepository.findAll(builder, pageable);
}

Kode program di pencarian PemesananKunjungan tidak akan banyak berbeda.  Pada kode proram di atas, sama sekali tidak ada JPQL atau SQL.  Keuntungan penggunakan Querydsl akan semakin terlihat bila saya memungkinkan lebih banyak variasi pencarian, misalnya pencarian pencarian berdasarkan “nama” dan/atau “kode” sekaligus.