หน้าเว็บ

วันอาทิตย์ที่ 16 กันยายน พ.ศ. 2555

เข้าใจ Dynamic Query Criteria JPA Eclipse Link + Spring Data : Java

อะไรคือ Dynamic Query ?
        จากที่ผ่านมา http://na5cent.blogspot.com/2013/01/queryjpql-service-java.html  การสร้างคำสั่ง Query ของเรา จะเป็นการ Fixed code ไว้  อยาก select อะไร select กี่ field เราก็เขียน select field นั้นลงไปเลย  แต่ถ้าเราเจอปัญหาในลักษณะที่ว่า Dynamic เช่น User ต้องการ select field อะไรก็ได้ แล้วแต่เขาจะเลือก  คำสั่ง SQL นั้นอาจประกอบไปด้วย 1 field, 2 field, 3 field  ... ถ้าเจอแบบนี้  เราจะทำยังไง  จะเขียน SQL ของทั้งหมดที่เป็นไปได้เหรอ  ใครทำแบบนั้น  ก็ทำไปเถอะครับ  ผมคนนึงล่ะที่ไม่เอาด้วย  คงไม่ต้องไปทำมาหากินอย่างอื่นแล้ว  แค่นั่งเขียน code ทุกความเป็นไปได้ทั้งหมด  ตัวอย่างเช่น ถ้ามีซัก 5 field มันก็คงจะประมาณนี้มั้งครับ  ที่ต้องเขียน
  1. EmpID
  2. EmpID + FirstName
  3. EmpID+LastName
  4. EmpID+DepID
  5. EmpID+Position
  6. EmpID+FirstName+LastName
  7. EmpID+FirstName+DepID
  8. EmpID+FirsttName+Position
  9. EmpID+LastName+DepID
  10. EmpID+LastName+Position
  11. EmpID+DepID+Position
  12. EmpID +FirstName+LastName+DepID+Position
  13. ...
        หรืออาจมีอีก อันนี้ผมก็ไม่ได้ไล่ทั้งหมดน่ะครับ  ขี้เกียจไล่  เอาเป็นว่ามันก็จะเป็นแบบนี้แหล่ะ  ต้องมาเขียนไล่เองทั้งหมด  แล้วถ้าเขามีการแก้ไข database เป็น 6 7 หรือ 8 field ล่ะ  ตายเลย!!!

        เรามาเขียน dynamic query กันดีกว่า  ช่วยให้งานเราสบายขึ้นครับ  แค่อาจต้องเรียนรู้เพิ่มเติมอีกนิดหน่อย
เรามาเริ่มกันเลยดีกว่าครับ

1. ใน Repositorie ให้ทำการ extends JpaSpecificationExecutor เพื่อให้สามารถใส่  Specification ลงไปใน Method ต่างๆ ของ Repositorie ได้ เช่น .findAll(Specification<User> spac) เป็นต้น

package com.blogspot.na5cent.repositories;

import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import com.blogspot.na5cent.model.User;

/**
 *
 * @author redcrow
 */
public interface UserRepositorie extends JpaRepository<User, Integer>, JpaSpecificationExecutor {
    ...
    ...
}

2. Create Specification สำหรับสร้างเงื่อนไขในการ Query
       ผมขอ แยก package ไว้น่ะครับเพราะไม่ได้มีแค่ UserSpec เท่านั้น  ยังมีอีกเยอะ  แต่ไม่ได้เอามาให้ดูครับ
        ในที่นี้  ผมสร้าง Class UserSpecification ที่มี Method likeBy เพื่อรับค่า Parameter by และ word เข้ามา   Parameter by เป็นตัวแปรชนิด SingularAttribute และ word เป็นตัวแปรชนิด String

ตัวแปรชนิด SingularAttribute คืออะไร ?
        ตัวแปรชนิด SingularAttribute คือตัวแปรที่ใช้สำหรับอ้างถึงตัวแปรที่มีอยู่ในคลาสที่กำหนด  เช่น   SingularAttribute<User, String> by  หมายถึง  Parameter by สามารถเป็นตัวแปรอะไรก็ได้ ที่อยู่ใน Class User และมีชนิดข้อมูลเป็น String  เป็นต้น  ดูตัวอย่างใน Services ข้อ 3 ครับ
package com.blogspot.na5cent.specifications;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.SingularAttribute;
import org.springframework.data.jpa.domain.Specification;
import com.blogspot.na5cent.model.User;
import com.blogspot.na5cent.model.User_;

/**
 *
 * @author redcrow
 */
public class UserSpecification {

    public static Specification<User> likeBy(final SingularAttribute<User, String> by, final String word) {
        return new Specification<User>() {

            //Predicate เปรียบได้กับเงื่อนไข WHERE ใน SQL ครับ ขึ้นอยู่กับว่าเราจะ WHERE อะไร
            @Override
            public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) {

                //ในที่นี้ผมต้องการสร้างเงื่อนไขคือ 
                //SELECT u FROM User u WHERE u.[ตรงนี้คือ parameter by ครับ] LIKE ?1
                //ที่จริงตรง root.get(by): by เราสามารถเป็นตัวแปร String ก็ได้  แต่ต้องเป็น attribute class น่ะครับ  แต่ที่ผมไม่ใส่ String ลงไป ผมมีเหตุผลดังนี้ครับ  คือ String มันไม่ type safe คือ ถ้าเรามีการเปลี่ยนแปลงชื่อตัวแปรใหม่  มันจะไม่แจ้งข้อผิดพลาดให้เราเห็นครับ  เวลาเราใช้งานไป  มันก็จะเกิดข้อผิดพลาดขึ้น  ซึ่งตัวแปร SingularAttribute สามารถช่วยเราในเรื่องนี้ได้  ซึ่งถ้าหากมีการแก้ไขชื่อ attribute class ขึ้นมา  มันจะแจ้งเตือนให้โดยอัตโนมัติครับ
                return cb.like(root.get(by), word);
            }
        };
    }
}

Root<User> root
root ใช้สำหรับอ้างถึง Entity User

3. เรียกใช้, สมมติ ผมเรียกใช้ผ่าน Service
package com.blogspot.na5cent.services.implementations;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specifications;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.blogspot.na5cent.model.User;
import com.blogspot.na5cent.model.User_;
import com.blogspot.na5cent.repositories.UserRepositories;
import com.blogspot.na5cent.services.UserService;
import com.blogspot.na5cent.specifications.UserSpecification;
import static org.springframework.data.jpa.domain.Specifications.*; //***********

/**
 *
 * @author redcrow
 */
@Service
@Transactional(propagation = Propagation.REQUIRED)
public class UserServiceImplementation implements UserService {

    @Autowired
    private UserRepositorie userRepo;

    ...
    ...

    @Override
    public Page findByLike(String word, Pageable page) {
        return userRepo.findAll(createLikeSpecification(word), page);
    }

    private Specifications<User> createLikeSpecification(String word) {
        word = "%" + word.trim() + "%";
          
        //สร้าง Specification เพื่อกำหนดเงื่อนไข WHERE
        //ซึ่งผมต้องการ OR ในทุกๆ field ของ Entity Class
        Specifications<User> spec;
        spec = where(UserSpecification.likeBy(User_.name, word))//***************
                .or(UserSpecification.likeBy(User_.userName, word))
                .or(UserSpecification.likeBy(User_.userType, word))
                .or(UserSpecification.likeBy(User_.organizationId, word))
                .or(UserSpecification.likeBy(User_.organizationName, word))
                .or(UserSpecification.likeBy(User_.province, word))
                .or(UserSpecification.likeBy(User_.branch, word));
        return spec;
    }
}

        จากที่ได้อธิบายไว้ในข้อ 2 ของตัวแปร SingularAttribute  เมื่อเราเรียกใช้  จะเป็นดังนี้

UserSpecification.likeBy(User_.userName, word)  

        User_   คือ Meta Class ที่อ้างถึง User Class ซึ่ง Eclipse Link จะสร้างให้เราเองโดยอัตโนมัติ  เมื่อจะเรียกใช้ จะต้อง
import static org.springframework.data.jpa.domain.Specifications.*;


นี่เป็น code User_ ที่ Eclipse Link ทำการ Auto Generate ให้ครับ
package com.blogspot.na5cent.model;

import javax.annotation.Generated;
import javax.persistence.metamodel.ListAttribute;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;

@Generated(value="EclipseLink-2.3.2.v20111125-r10461", date="2555-09-14T10:36:17")
@StaticMetamodel(User.class)
public class User_ { 

    public static volatile SingularAttribute<User, String> organizationName;
    public static volatile SingularAttribute<User, String> organizationId;
    public static volatile SingularAttribute<User, String> password;
    public static volatile SingularAttribute<User, Integer> version;
    public static volatile SingularAttribute<User, String> userType;
    public static volatile SingularAttribute<User, Integer> id;
    public static volatile SingularAttribute<User, String> name;
    public static volatile SingularAttribute<User, String> province;
    public static volatile SingularAttribute<User, String> branch;
    public static volatile SingularAttribute<User, String> userName;

}
        เราสามารถใช้ Meta Class นี้ในการอ้างถึงตัวแปรทุกตัวที่มีอยู่ใน Class นั้นได้  เช่น User_.userName  ก็คือการอ้างถึงตัวแปร userName ใน Class User หรือ Entity User นั่นเอง

อันนีเป็น Entity User น่ะครับ
package com.blogspot.na5cent.model;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;

/**
 *
 * @author redcrow
 */
@Entity
@Table(name = "USER")
public class User implements Serializable {

    @Id
    @Column(name = "USER_ID")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    @Version
    private Integer version;
    private String name;
    private String userName;
    private String password;
    private String userType;
    private String organizationId;
    private String organizationName;
    private String province;
    private String branch;

    public User() {
    }

    public User(Integer id) {
        this.id = id;
    }

    //getter and setter

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 59 * hash + (this.id != null ? this.id.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final User other = (User) obj;
        if (this.id != other.id && (this.id == null || !this.id.equals(other.id))) {
            return false;
        }
        return true;
    }
}

ตัวอย่างของ Criteria ที่ผมเขียนไว้ครับ
...
...
...
/**
 *
 * @author Redcrow
 */
public class DeficientSpec {

    //WHERE deficient.status = ?1
    public static Specification<Deficient> eqaulByDeficientStatus(final DeficientStatus status) {
        Specification<Deficient> specification = new Specification<Deficient>() {
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
                return cb.equal(root.get(Deficient_.status), status);
            }
        };
        return specification;
    }

    //WHERE LOWER(deficient.[by]) LIKE ?1
    public static Specification<Deficient> likeBy(final SingularAttribute<Deficient, String> by, final String keyWord) {
        Specification<Deficient> specification = new Specification<Deficient>() {
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
                return cb.like(cb.lower(root.get(by)), keyWord.toLowerCase().trim());
            }
        };
        return specification;
    }

    //WHERE deficient.[by] BETWEEN ?1 AND ?2
    public static Specification<Deficient> betweenDateBy(final SingularAttribute<Deficient, Date> by, final Date startDate, final Date endDate) {
        Specification<Deficient> specification = new Specification<Deficient>() {
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
                Expression exps = root.get(by);
                return cb.between(exps, startDate, endDate);
            }
        };

        return specification;
    }

    //WHERE deficient.notifier.organization.id = ?1
    public static Specification<Deficient> equalByOrganization(final String keyWord) {
        Specification<Deficient> specification = new Specification<Deficient>() {
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
                Expression exps = root.get(Deficient_.notifier)
                        .get(WSUserDetails_.organization)
                        .get(Organization_.orgId);
                return cb.equal(exps, keyWord);
            }
        };

        return specification;
    }

    //WHERE deficient.notifier.organization.province.id = ?1
    public static Specification<Deficient> equalByProvince(final String keyWord) {
        Specification<Deficient> specification = new Specification<Deficient>() {
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
                Expression exps = root.get(Deficient_.notifier)
                        .get(WSUserDetails_.organization)
                        .get(Organization_.province)
                        .get(Province_.id);
                return cb.equal(exps, keyWord);
            }
        };

        return specification;
    }

    //WHERE deficient.notifier.organization.province.zone.id = ?1
    public static Specification<Deficient> equalByZone(final String keyWord) {
        Specification<Deficient> specification = new Specification<Deficient>() {
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
                Expression exps = root.get(Deficient_.notifier)
                        .get(WSUserDetails_.organization)
                        .get(Organization_.province)
                        .get(Province_.zone)
                        .get(Zone_.id);
                return cb.equal(exps, keyWord);
            }
        };

        return specification;
    }
}
เวลาเรียกใช้
    
...
...
...
    private Specification<Deficient> createSpecification() {
        Specifications<Deficient> spec;
        spec = where(DeficientSpec.likeBy(Deficient_.genericName, "%" + name + "%"));
        if (null != zone && !"".equals(zone) && !"all".equals(zone)) {
            spec = spec.and(DeficientSpec.equalByZone(zone));
        }

        if (null != province && !"".equals(province) && !"all".equals(province)) {
            spec = spec.and(DeficientSpec.equalByProvince(province));
        }

        if (null != hospital && !"".equals(hospital)) {
            spec = spec.and(DeficientSpec.equalByOrganization(hospital));
        }

        if (null != startDate && null != endDate) {
            spec = spec.and(DeficientSpec.betweenDateBy(Deficient_.notifyDate, startDate, endDate));
        }

        if (null != resolveStartDate && null != resolveEndDate) {
            spec = spec.and(DeficientSpec.betweenDateBy(Deficient_.updateStatusDate, resolveStartDate, resolveEndDate));
        }

        spec = spec.and(DeficientSpec.eqaulByDeficientStatus(status));
        return spec;
    }
...
...
...


...
...
...
    //SELECT s 
    //FROM Staff s 
    //WHERE s.staffId NOT IN(
    //    SELECT u.staffId 
    //    FROM User u 
    //    WHERE u.disabled = false
    //) 
    public static Specification<Staff> notInUser() {
        return new Specification<Staff>() {
            @Override
            public Predicate toPredicate(Root<Staff> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Subquery<String> sq = query.subquery(String.class);
                Root<User> user = sq.from(User.class);

                sq.select(user.get(User_.staffId));
                Predicate and = cb.and((cb.isNotNull(user.get(User_.staffId))), cb.equal(user.get(User_.disabled), false));
                sq.where(and);


                return cb.not(cb.in(root.get(Staff_.staffId)).value(sq));
            }
        };
    }

//----------------------------------------------------------------------------
    //WHERE staff.userType <> ?1 AND staff.userType <> ?2
    public static Specification<Staff> staffTypeNotEqualZero() {
        return new Specification<Staff>() {
            @Override
            public Predicate toPredicate(Root<Staff> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return cb.and(cb.notEqual(root.get(Staff_.userType).get(UserType_.userType), "0"), cb.notEqual(root.get(Staff_.userType).get(UserType_.userType), "00"));
            }
        };
    }

//----------------------------------------------------------------------------
    //WHERE data_report.dateReport BETWEEN ?1 AND ?2
    public static Specification<DataReport> betweenDateBy(final SingularAttribute<DataReport, Calendar> by, final Calendar startDate, final Calendar endDate) {
        return new Specification<DataReport>() {
            @Override
            public Predicate toPredicate(Root<DataReport> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Expression<Calendar> dateReport = root.get(by);
                return cb.between(dateReport, startDate, endDate);
            }
        };
    }
...
..
..
หวังว่าคงพอได้ concept กันน่ะครับ  วันนี้เอาแค่เบสิกๆ ก่อนน่ะครับ  แล้ววันหลังจะมาเขียนใหม่  ^_____________^


ไม่มีความคิดเห็น:

แสดงความคิดเห็น