Monday, December 15, 2014

Spring Data Dynamic Query

saya mau berbagi sedikit tentang dynamic query menggunakan spring data. dynamic query yang saya maksud adalah melakukan query dengan parameter yang dinamis menggunakan satu repository method.

saya punya domain class Customer sebagai berikut:
 @Entity  
 @Table(name = "POST")  
 public class Customer extends Base {  
   
   @Column(name = "name")  
   private String name;  
   
   @Column(name = "phone")  
   private String phone;  
   
   @Column(name = "address")  
   private String address;  
   
   @Column(name = "email")  
   private String email;  
   
   @Column(name = "visit_count")  
   private Integer visitCount;  
   
   @Column(name = "gender")  
   private String gender;  
   
   //SETTER GETTER  
 }  

misalnya ada kebutuhan untuk:
- query Customer by name
- query Customer by name AND phone
- query Customer by gender AND visitCount.

ada beberapa cara melakukan query di spring data, ada namedquery, ada method query, ada criteria query. sebagai referensi, saya rasa blog ini http://www.petrikainulainen.net/spring-data-jpa-tutorial/ bagus untuk memulai mempelajari spring data. untuk bisa menggunakan parameter yang dinamis, criteria query bisa kita gunakan.

spring data menyediakan interface org.springframework.data.jpa.repository.JpaSpecificationExecutor<T> untuk memfasilitasi criteria query. criteria akan dibangun dan dibungkus dalam org.springframework.data.jpa.domain.Specification<T>. maka yang harus kita lakukan adalah meng-extends interface JpaSpecificationExecutor di interface CustomerRepository kita, kemudian membungkus parameter ke dalam sebuah interface Specification.

ini CustomerRepository saya:
 @Repository  
 public interface CustomerRepository extends JpaRepository<Customer, Integer>, JpaSpecificationExecutor<Customer>{  

 }  

lalu seperti biasa, untuk mengakses level DAO, saya buat service interface:
 public interface CustomerService {  

      public void save(List<CustomerVO> custVos);  

      public List<CustomerVO> search(Map<String, Object> parameters);  

 }  

method save akan kita gunakan untuk menginput data Customer, dan method search akan kita gunakan sebagai pintu untuk melakukan berbagai query sesuai kebutuhan di atas. untuk menjaga fleksibilitas, saya gunakan argumen Map<String, Object> untuk search tersebut.

kira-kira seperti ini penggambaran cara kerjanya:

misalkan ada sebuah query "select * from customer where name like '%{name}%' and phone like '%{phone}%' and visit_count={count}"

maka yang dimaksud dengan predicate adalah:
- predicate1: name like '%{name}%'
- predicate2: phone like '%{phone}%'
- predicate3: visit_count={count}

CriteriaBuilder yang menyusun tiap Predicate tersebut dengan operator 'and', sehingga Specificationnya menjadi "{predicate1} and {predicate2} and {predicate3}"

kemudian spring data repository (JpaSpecificationExecutor) yang akan melengkapi "select * from customer where {specification}"

ini implementasi saya:
 @Service  
 public class CustomerServiceImpl implements CustomerService {  

      @Autowired  
      private CustomerRepository customerRepository;  

      @Autowired  
      private CustomerVoConverter customerVoConverter;  

      @Override  
      public void save(List<CustomerVO> custVos) {  
           List<Customer> customers = new ArrayList<Customer>();  
           for(CustomerVO custVo : custVos){  
                customers.add(customerVoConverter.transferVOToModel(custVo, new Customer()));  
           }  
           customerRepository.save(customers);  
      }  

      @Override  
      public List<CustomerVO> search(Map<String, Object> parameters) {  
           List<CustomerVO> vos = null;  
           List<Customer> customers = customerRepository.findAll(dynamicSearchParams(parameters));  
           if(!customers.isEmpty()){  
                vos = customerVoConverter.transferListOfModelToListOfVO(customers, new ArrayList<CustomerVO>());  
           }  
           return vos;  
      }  

      private Specification<Customer> dynamicSearchParams(final Map<String, Object> parameters) {  
     return new Specification<Customer>() {  

                @Override  
                public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {  
                     Predicate[] predicates = getPredicates(root, cb, parameters);  
                     return cb.and(predicates);  
                }  

                private Predicate[] getPredicates(Root<Customer> root, CriteriaBuilder cb, Map<String, Object> parameters){  
                     Predicate[] predicates = new Predicate[parameters.size()];  
                     Iterator<String> paramKeyIterator = parameters.keySet().iterator();  
                     int i = 0;  
                     while(paramKeyIterator.hasNext()){  
                          String key = paramKeyIterator.next();  
                          Object value = parameters.get(key);  
                          Predicate predicate = null;  
                          if(root.get(key).getJavaType() == String.class){  
                               predicate = cb.like(cb.upper(root.get(key).as(String.class)), "%" + ((String)value).toUpperCase() + "%");  
                          }else{  
                               predicate = cb.equal(root.get(key), value);  
                          }  
                          predicates[i] = predicate;  
                          i++;  
                     }  
                     return predicates;  
                }  
     };  
   }  
 }  

saya memisahkan model untuk entity dan model untuk business process, di business process saya gunakan model CustomerVO, dan CustomerVoConverter adalah class yang saya gunakan untuk meng-convert entity ke VO dan sebaliknya.

interface JpaSpecificationExecutor<T> memiliki method List<T> findAll(Specification<T> spec). method ini yang saya gunakan untuk mengimplementasi search function di atas.

 List<Customer> customers = customerRepository.findAll(dynamicSearchParams(parameters));  

kita lihat di dalam method dynamicSearchParams, yang direturn adalah implementasi Specification<Customer> yang dibangun dengan menggunakan CriteriaBuilder yang disusun dari beberapa Predicate (disimpan di dalam array), dengan operator 'and'. ini disusun di dalam method toPredicate bawaan dari interface Specification.

kemudian kita lihat method getPredicates, method ini yang membongkar isi Map parameters dan membangun Predicate untuk tiap map entry.

perhatikan bagian root.get(key), root adalah proyeksi dari object Customer dan get(key) adalah fungsi untuk memanggil field key yang ada di object Customer.

misalnya key = "name" maka root.get(key) akan memanggil value dari field 'name' yang ada di object Customer. ini artinya, tiap key di Map parameters harus sesuai dengan nama field entity Customer. jika salah, misalnya key yang didaftarkan adalah 'nama', maka ini exception yang akan dithrow
 org.springframework.dao.InvalidDataAccessApiUsageException: Unable to resolve attribute [nama] against path; nested exception is java.lang.IllegalArgumentException: Unable to resolve attribute [nama] against path  

di sini saya melakukan pemisahan untuk field yang typenya String, saya gunakan clause 'like' dan yang lain menggunakan 'equal' untuk predicatenya:
 if(root.get(key).getJavaType() == String.class){  
      predicate = cb.like(cb.upper(root.get(key).as(String.class)), "%" + ((String)value).toUpperCase() + "%");  
 }else{  
      predicate = cb.equal(root.get(key), value);  
 }  

sudah, begitu saja. sekarang kita coba dengan menggunakan unit test, saya pakai junit:

 @RunWith(SpringJUnit4ClassRunner.class)  
 @ContextConfiguration(locations = { "/spring/applicationContext.xml"})  
 public class CustomerServiceTest {  

      @Autowired  
      private CustomerService customerService;  

      @Autowired  
      private CustomerRepository customerRepository;  

      @Before  
      public void insertCustomers(){  
           List<CustomerVO> customers = new ArrayList<CustomerVO>();  
           for(int i=0; i<10; i++){  
                CustomerVO custVo = new CustomerVO();  
                custVo.setAddress("alamat " + i);  
                custVo.setEmail("cust" + i + getMail(i));  
                custVo.setGender(i%2 == 0 ? "M" : "F");  
                custVo.setName(getName(i));  
                custVo.setPhone(getPhone(i));  
                custVo.setVisitCount(i % 3);  
                customers.add(custVo);  
           }  
           customerService.save(customers);  
           List<Customer> custs = customerRepository.findAll();  
           Assert.assertTrue(custs.size() == 10);  
      }  

      private String getMail(int i){  
           String mail = null;  
           if(i%3 == 0){  
                mail = "@mail.com";  
           }else if(i%5 == 0){  
                mail = "@google.com";  
           }else if(i%7 == 0){  
                mail = "@yahoo.com";  
           }else{  
                mail = "@test.com";  
           }  
           return mail;  
      }  

      private String getName(int i){  
           String name = null;  
           if(i%2 == 0){  
                name = "eko " + i;  
           }else if(i%3 == 0){  
                name = "budi " + i;  
           }else if(i%7 == 0){  
                name = "nama " + i;  
           }else{  
                name = "customer " + i;  
           }  
           return name;  
      }  

      private String getPhone(int i){  
           String phone = "9999";  
           if(i%3 == 0){  
                phone = "0811" + i + phone;  
           }else if(i%4 == 0){  
                phone = "0856" + i + phone;  
           }else{  
                phone = "0878" + i + phone;  
           }  
           return phone;  
      }  

      @Test  
      public void findByName(){  
           Map<String, Object> parameters = new HashMap<String, Object>();  
           parameters.put("name", "budi");  
           System.out.println("--FIND BY NAME 'budi'--");  
           List<CustomerVO> custVos = customerService.search(parameters);  
           for(CustomerVO custVo : custVos){  
                System.out.println(custVo);  
           }  

           System.out.println("\n");  
           Map<String, Object> parameters2 = new HashMap<String, Object>();  
           parameters2.put("name", "eko");  
           parameters2.put("phone", "0856");  
           System.out.println("--FIND BY NAME 'eko' AND PHONE '0856'--");  
           List<CustomerVO> custVos2 = customerService.search(parameters2);  
           for(CustomerVO custVo : custVos2){  
                System.out.println(custVo);  
           }  

           System.out.println("\n");  
           Map<String, Object> parameters3 = new HashMap<String, Object>();  
           parameters3.put("gender", "f");  
           parameters3.put("visitCount", "1");  
           System.out.println("--FIND BY GENDER 'f' AND VISITCOUNT 1--");  
           List<CustomerVO> custVos3 = customerService.search(parameters3);  
           for(CustomerVO custVo : custVos3){  
                System.out.println(custVo);  
           }  
           System.out.println("\n");  
      }  
 }  

method getName, getMail, getPhone hanya untuk me-'random'-kan value saja.

dan outputnya adalah:
 --FIND BY NAME 'budi'--  
 CustomerVO [name=budi 3, phone=081139999, address=alamat 3, email=cust3@mail.com, visitCount=0, gender=F]  
 CustomerVO [name=budi 9, phone=081199999, address=alamat 9, email=cust9@mail.com, visitCount=0, gender=F]  

 --FIND BY NAME 'eko' AND PHONE '0856'--  
 CustomerVO [name=eko 4, phone=085649999, address=alamat 4, email=cust4@test.com, visitCount=1, gender=M]  
 CustomerVO [name=eko 8, phone=085689999, address=alamat 8, email=cust8@test.com, visitCount=2, gender=M]  

 --FIND BY GENDER 'f' AND VISITCOUNT 1--  
 CustomerVO [name=customer 1, phone=087819999, address=alamat 1, email=cust1@test.com, visitCount=1, gender=F]  
 CustomerVO [name=nama 7, phone=087879999, address=alamat 7, email=cust7@yahoo.com, visitCount=1, gender=F]  

berhasil, 1 method untuk berbagai parameter query.

kita tidak pernah tau kapan field di Customer akan bertambah, kita tidak pernah tau kapan dan apa saja kombinasi search parameter untuk Customer. semua itu ada kemungkinan berkembang seiring perjalanan business processnya. karena itu akan lebih baik jika dari awal kita sudah menyiapkan kemungkinan tersebut dengan menyediakan fungsi-fungsi yang dinamis.

thanks, semoga bermanfaat.

No comments:

Post a Comment