หน้าเว็บ

วันอังคารที่ 8 มกราคม พ.ศ. 2556

การสร้างคำสั่ง Query(JPQL) และการเรียกใช้งานผ่าน Service : java

JPQL คืออะไร?
        JPQL ย่อมาจาก Java Persistence Query Language  มันก็เหมือนกับการเขียนคำสั่ง Query ข้อมูล แบบ SQL ทั่วๆไปนั่นเองครับ  เพียงแต่ว่า  มันจะเป็นการเขียน Query ในลักษณะของการ Query ข้อมูลจาก Entity Class (http://na5cent.blogspot.com/2013/01/entity-class-map-entity-relationship.html) แทน  ค่าที่ได้จะเป็น Object เสมอ ตัวอย่างประมาณนี้ครับ

"SELECT std FROM Student std WHERE std.id = ?1"

        จะสังเกตเห็นว่า มันก็เหมือนคำสั่ง SQL ธรรมดาๆ นั่นเอง เพียงแต่มีข้อสังเกตอย่างนึงครับ  มองเห็นอะไรมั้ยครับว่า  Student ทำไมผมไม่เขียนเป็นตัวอักษรตัวพิพม์ใหญ่?
        นั่นก็เพราะว่า  นี่ไม่ใช่การอ้างไปที่ table ของ database น่ะสิครับ  แต่เป็นการอ้างไปที่ Entity Class ที่เป็น java class แทน  ดังนั้นสิ่งที่ SELECT ออกมาได้จึงเป็น Object หรือ List ของ Object นั่นเองครับ  หวังว่าคงพอเข้าใจน่ะครับ

        ก่อนที่เราจะเริ่มลงมือเขียนกันจริงๆ เรามาทำความเข้าใจกับโครงสร้างกันก่อนครับ  ในที่นี้ผมจะเขียนคำสั่ง JPQL เก็บไว้ใน package repositories ครับ

        package repositories คืออะไร?
repository ถ้าแปลตรงๆ มันก็คือคลังหรือพื้นที่เก็บ  ซึ่งคลังนี้คือคลังที่เอาไว้เก็บคำสั่ง JPQL ครับ  โดยที่เราต้องการเขียนรวมไว้เป็น package เพื่อให้ framework มาอ่านไปสร้างเป็นคำสั่ง SQL ในภายหลัง

        ทีนี้มาถึงการเรียกใช้งานครับ  เราจะเรียกใช้งาน JPQL ที่เราได้เขียนไว้  ผ่าน service  ซึ่ง service นั้นเราจะใช้ spring framework  มาช่วยจัดการในส่วนนี้

มาลงมือเขียนกันจริงๆ ดีกว่าครับ  บรรยายแต่ทฤษฎีไป  แต่ทำไม่เป็ํนมันก็เท่านั้น

มีวิธีการดังต่อไปนี้
1. ใน package repositories (ถ้าไม่มีให้สร้างขึ้นมาน่ะครับ) ให้สร้าง interface ที่เราต้องการเก็บคำสั่ง JPQL ของ ส่วนนั้นขึ้นมา เช่น  UserRepository  จากนั้น ให้ extends JpaRepository<Entity Class, Id Type ของ Entity Class> ดังนี้


UserRepository.java
package com.blogspot.na5cent.planapprovalsystem.repositories;

import com.blogspot.na5cent.planapprovalsystem.models.User;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 *
 * @author Redcrow
 */
public interface UserRepository extends JpaRepository<User, Integer> {

    //code ในส่วนนี้หมายถึง "SELECT us FROM User us WHERE us.userName = ?1"  ซึ่ง spring framework จะจัดการสร้างให้เราเองโดยอัตโนมัติจากชื่อ method ที่เราเขียน  ซึ่งมันจะมีหลักการเขียนอยู่ครับ  เดี๋ยวจะอธิบายในภายหลัง
    public List<user> findByUserName(String userName);
    
}

User.java
...
...
...
@Entity
@Table(name = "USER_APP")
public class User implements Serializable {

    @Id
    //
    @TableGenerator(name = "USER_APP_GEN",
    table = "NA5CENT_SEQ",
    pkColumnName = "NAME",
    valueColumnName = "VALUE",
    pkColumnValue = "USER_APP")
    //
    @GeneratedValue(generator = "USER_APP_GEN",
    strategy = GenerationType.TABLE)
    //
    @Column(name = "USER_ID")
    private Integer id;
    //--------------------------------------------------------------------------
    @Version
    private Integer version;
    //--------------------------------------------------------------------------
    //informations
    @Column(name = "USER_NAME")
    private String userName;
    private String password;
    @Column(name = "FULL_NAME")
    private String fullName;
    private Boolean enable;

    ...
    ...
    ...
}
        interface JpaRepository คืออะไร?
interface JpaRepository คือ interface ที่เก็บคำสั่ง JPQL เบื้องต้นไว้ เช่นคำสั่ง findAll(), findOne(), save(), count(), delete() เป็นต้น  ซึ่งเราสามารถเรียกใช้งานได้เลยโดยที่่เราไม่ต้องเขียนขึ้นมาเอง  และมันยังเป็น interface ที่เอาไว้ให้ spring framework มาอ่านเอาข้อมูลไปใช้ในการสร้างคำสั่ง SQL อีกด้วย  โดยวิธีการนั้นจะใช้เทคนิคที่เรียกว่า Java Reflection (ภาษาไทยเขาจะเรียกว่าเงาสะท้อนของจาวา ครับ) ซึ่งถ้าใครสนใจ ก็ลองไปอ่านเอาเองน่ะครับ

2. ต่อมาคือการสร้าง interface service ขึ้นมาครับ ซึ่งผมจะเก็บไว้ใน package services


UserService.java
package com.blogspot.na5cent.planapprovalsystem.services;

import com.blogspot.na5cent.planapprovalsystem.models.User;
import java.util.List;

/**
 *
 * @author Redcrow
 */
public interface UserService {
    public List<User> findByUserName(String userName);

    //ผลลัพธ์เป็น paging ครับ
    public Page<User> findAllByPaging(Pageable page);

    public List<User> saveAll(List<User> users);

    public User save(User plan);

    public User findById(Integer id);

    public void deleteAll();

    public void deleteById(Integer id);
    
    public void delete(User item);
}

3. สร้าง class ที่ implements interface Service ที่เราเขียนไว้ไปใช้งานครับ โดยผมจะเก็บไว้ที่ package services.implementations ดังนี้


UserServiceImplementation.java
package com.blogspot.na5cent.planapprovalsystem.services.implementations;

import com.blogspot.na5cent.planapprovalsystem.models.User;
import com.blogspot.na5cent.planapprovalsystem.repositories.UserRepository;
import com.blogspot.na5cent.planapprovalsystem.services.UserService;
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.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 *
 * @author Redcrow
 */
@Service //บอกว่า class นี้คือ service  ซึ่งเราจะสามารถเรียกใช้งานผ่าน spring framework ได้

//@Transactional(propagation= Propagation.REQUIRED) เป็นการบอกว่า class นี้ Require transaction เสมอครับ  ซึ่ง spring framework จะใช้ pattern การเขียนแบบ AOP(Aspect Oriented Programing ซึ่งเป็นการแก้ปัญหาแนวตัดขวางของ code) ที่ใช้ Proxy method ในการ begin หรือ commit transaction ให้เราเอง  เราจะไม่เห็น code ในส่วนนั้น  ซึ่งจะว่าไปมันก็คือการ transparent code ไว้  เพราะไม่งั้นทุก service เราก็ต้องมาเขียน db.getTransaction().begin() หรือ commit() เองทั้งหมด  ซึ่งเค้าเห็นว่ามันไม่จำเป็นต้องมี  เค้าเลยใช้แค่ annotation @Transactional มาช่วยแทน  จึงทำให้ code สั้นลง ดูสะอาดตา  อ่านเข้าใจง่าย และอีกอย่างมันสบายเราด้วยครับ
@Transactional(propagation= Propagation.REQUIRED) 
public class UserServiceImplementation implements UserService{
    
    //@Autowired เป็นการ connect ไปยัง database ซึ่งในที่นี้เราไม่ต้องเขียน connection database เอง  spring framework จะจัดการให้เราเองแบบเสร็จสับตับหมูเลยครับ เราแค่เรียกใช้ repository ที่เราต้องการก็พอ  ซึ่งในที่นี้ก็คือ UserRepository ที่เราได้เขียนไว้นั่นเอง
    @Autowired 
    private UserRepository userRepository;

    @Override
    public Page<User> findAllByPaging(Pageable page) {
        //เป็นการเรียกใช้งานคำสั่ง JPQL default ที่มีค่าเป็น "SELECT us FROM User us"
        return userRepository.findAll(page);
    }

    @Override
    public List<User> saveAll(List<User> plans) {
        //เป็นการเรียกใช้งานคำสั่ง JPQL default ที่มีค่าเป็น "INSERT INTO User ..." หรือ "UDATE User ..." แบบเป็นชุดข้อมูลครับ
        return userRepository.save(plans);
    }

    @Override
    public User save(User plan) {
        //เป็นการเรียกใช้งานคำสั่ง JPQL default ที่มีค่าเป็น "INSERT INTO User ..." หรือ "UDATE User ..." แบบเป็น Object เดียว
        return userRepository.save(plan);
    }

    @Override
    public User findById(Integer id) {
        //เป็นการเรียกใช้งานคำสั่ง JPQL default ที่มีค่าเป็น "SELECT us FROM User us WHERE us.id = ?1"
        return userRepository.findOne(id);
    }

    @Override
    public void deleteAll() {
        //เป็นการเรียกใช้งานคำสั่ง JPQL default ที่มีค่าเป็น "DELETE FROM User us"
        userRepository.deleteAll();
    }

    @Override
    public void deleteById(Integer id) {
        //เป็นการเรียกใช้งานคำสั่ง JPQL default ที่มีค่าเป็น "DELETE FROM User us WHERE us.id = ?1"
        userRepository.delete(id);
    }

    @Override
    public void delete(User item) {
        //เป็นการเรียกใช้งานคำสั่ง JPQL default ที่มีค่าเป็น "DELETE FROM User us WHERE us.id = ?1" อันเดียวกันกับข้างบน  เพียงแต่ parameter ที่เราส่งเข้ามาไม่ใช้ Integer แต่เป็น object User แทน
        userRepository.delete(item);
    }

    @Override
    public List<User> findByUserName(String userName) {
        //ตัวนี้ไม่ใช้ JPQL default  เราต้องสร้างเอง ซึ่งเราก็ได้สร้างไปแล้ว ใน UserRepository
        return userRepository.findByUserName(userName);
    }
    
}

4. การเรียกใช้งาน Service ที่ได้เขียนไว้
        ในที่นี้ผมจะเรียกผ่าน managed bean ของ JSF(JavaServer Faces)น่ะครับ ก่อนอื่นเรามาสร้าง Utilities Class  เอาไว้เรียกใช้งาน Service กันก่อน เพื่อเป็นการลด code ในการเรียกใช้งาน  ในภายหลัง ดังนี้
FacesUtils.java เอาไว้ get ServletContext ของ JSF ครับ
package com.blogspot.na5cent.planapprovalsystem.utilities;

import javax.faces.context.FacesContext;
import javax.servlet.ServletContext;
import org.springframework.web.jsf.FacesContextUtils;

/**
 *
 * @author Redcrow
 */
public class FacesUtils {

    public static ServletContext getServletContext() {
        return FacesContextUtils
                .getRequiredWebApplicationContext(FacesContext.getCurrentInstance())
                .getServletContext();
    }
}
JSFSpringUtils.java ที่เอาไว้ get Service ต่างๆ มาใช้งานครับ
package com.blogspot.na5cent.planapprovalsystem.utilities;

import javax.servlet.ServletContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

/**
 *
 * @author Redcrow
 */
public class JSFSpringUtils {

    public static <T> T getBean(Class<T> clazz) {
        return WebApplicationContextUtils.getWebApplicationContext(FacesUtils.getServletContext()).getBean(clazz);
    }

    public static Object getBean(String bean) {
        return WebApplicationContextUtils.getWebApplicationContext(FacesUtils.getServletContext()).getBean(bean);
    }

    public static <T> T getBean(ServletContext servletContext, Class<T> clazz) {
        return WebApplicationContextUtils.getWebApplicationContext(servletContext).getBean(clazz);
    }

    public static Object getBean(ServletContext servletContext, String bean) {
        return WebApplicationContextUtils.getWebApplicationContext(servletContext).getBean(bean);
    }
}
ที่ managed bean UserManagementMB.java
...
..
..

/**
 *
 * @author Recrow
 */
@ManagedBean
@SessionScoped
public class UserManagementMB implements Serializable {

    private static Logger LOG = LoggerFactory.getLogger(UserManagementMB.class);
    private List<User> users;
    private User user;
    //services
    private UserService userService;

    @PostConstruct
    public void postConstruct() {
        //get service มาใช้งานครับ ใช้ JSFSpringUtils ที่เราได้เขียนไว้  เราจะ get ที่ interface ของ service นั้นแทน โดย spring จะใช้หลักการ design ที่ว่า "Program to an interface, not an implementation" ซึ่งมันจะไป get implementation หรือ service มาให้เราเองครับ 
        userService = JSFSpringUtils.getBean(UserService.class);
        reset();
    }

    private void reset() {
        //ในตรงนี้  จะมีการเรียกใช้ db.getTransaction().begin();
        users = userService.findAll(new PageRequest(0, 25)); //เรียกใช้ service findAll()
        //ในตรงนี้  จะมีการเรียกใช้ db.getTransaction().commit(); ให้เราเองโดยอัตโนมัติ  แต่เราจะไม่เห็น เพราะมันทำผ่าน Proxy Method ที่เป็นการเขียนแบบ AOP
    }

   public void onSave() {
        //ในตรงนี้  จะมีการเรียกใช้ db.getTransaction().begin();
        user = userService.save(user); //เรียกใช้ service save()
        //ในตรงนี้  จะมีการเรียกใช้ db.getTransaction().commit(); ให้เราเองโดยอัตโนมัติ  แต่เราจะไม่เห็น เพราะมันทำผ่าน Proxy Method ที่เป็นการเขียนแบบ AOP
        Notifications.message(Notifications.INFO, "การเพิ่มบัญชีผู้ใช้งาน", "สำเร็จ : เพิ่มข้อมูลผู้ใช้ " + user.getFullName() + " เรียบร้อย");
        reset();
    }

    public void onDelete() {
        //ในตรงนี้  จะมีการเรียกใช้ db.getTransaction().begin();
        userService.delete(user); //เรียกใช้ service delete()
        //ในตรงนี้  จะมีการเรียกใช้ db.getTransaction().commit(); ให้เราเองโดยอัตโนมัติ  แต่เราจะไม่เห็น เพราะมันทำผ่าน Proxy Method ที่เป็นการเขียนแบบ AOP
        Notifications.message(Notifications.INFO, "การลบบัญชีผู้ใช้งาน", "สำเร็จ : ลบข้อมูลผู้ใช้ " + user.getFullName() + " เรียบร้อย");
        reset();
    }

    ...
    ...
    ...
}
แค่นี้ก็เรียบร้อยแล้วครับ select insert update delete ง่ายๆ ไร้ SQL

        ต่อมา คือการเขียนคำสั่ง JPQL เพิ่มเติมลงไปใน interface repository ที่เราสร้างขึ้นมา  ดังนี้ครับ
ตัวอย่างต่อไปนี้  ผมขอใช้ repository อื่น  เพื่อให้เห็นภาพกันมากขึ้น
NewsRepository.java
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.Modifying;
import org.springframework.data.jpa.repository.Query;
import com.blogspot.na5cent.model.Language;
import com.blogspot.na5cent.model.News;

/**
 *
 * @author Recrow
 */
public interface NewsRepository extends JpaRepository<News, Long> {
    //ถ้าต้องการเขียน JPQL เองต้องใช้ @Query 
    @Query("SELECT nws FROM News nws WHERE nws.lang = ?1 AND nws.importantUpdate = false")
    Page<News> findByLang(Language lang,Pageable pageable);

    @Query("SELECT nws FROM News nws WHERE nws.lang = ?1 AND nws.importantUpdate = true")
    public List<News> findImportantUpdate(Language lang, Pageable page);

    @Modifying //ถ้ามีการแก้ไขเปลี่ยนแปลงข้อมูลต้องมี @Modifying เสมอ เช่นการ update หรือ delete
    @Query("UPDATE News nws SET nws.importantUpdate = ?2 WHERE nws = ?1")
    public void setImportatnUpdate(News news, Boolean importantUpdate);
}
        สังเกตว่าการ Query จะเป็นการ Query จาก Entity Class แทนเพราะ เราส่ง Entity Class เข้าไป  ไม่ใช่ database table (News คือ Entity Class) nws.importantUpdate ตัวนี้ก็เป็น attribute ของ Entity Class ไม่ใช่ Column ของ table  ค่าต้องตรงกันตาม attribute class ตัวเล็กตัวใหญ่มีผลครับ (case sensitive)

เรียนรู้เพิ่มเติมได้ที่  http://www.objectdb.com/java/jpa/query/jpql/structure

        ต่อมาคือการเขียน Repository แบบไม่ต้องมี JPQL  ซึ่ง spring framework มันจะใช้ java reflection มาอ่านข้อมูลของ class หรือ method name ไปสร้างเป็น SQL ให้เองโดยอัตโนมัติ
มีหลักการดังต่อไปนี้ครับ

อันนี้ผม copy มาจาก document ของ spring  http://static.springsource.org/spring-data/data-jpa/docs/current/reference/html/

Keywordการตั้งชื่อ methodJPQL snippet
AndfindByLastnameAndFirstnamewhere x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstnamewhere x.lastname = ?1 or x.firstname = ?2
BetweenfindByStartDateBetweenwhere x.startDate between 1? and ?2
LessThanfindByAgeLessThanwhere x.age < ?1
GreaterThanfindByAgeGreaterThanwhere x.age > ?1
AfterfindByStartDateAfterwhere x.startDate > ?1
BeforefindByStartDateBeforewhere x.startDate < ?1
IsNullfindByAgeIsNullwhere x.age is null
IsNotNull,
NotNull
findByAge(Is)NotNullwhere x.age not null
LikefindByFirstnameLikewhere x.firstname like ?1
NotLikefindByFirstnameNotLikewhere x.firstname not like ?1
StartingWithfindByFirstnameStartingWithwhere x.firstname like ?1
(parameter bound with appended %)
EndingWithfindByFirstnameEndingWithwhere x.firstname like ?1
(parameter bound with prepended %)
ContainingfindByFirstnameContainingwhere x.firstname like ?1
(parameter bound wrapped in %)
OrderByfindByAgeOrderByLastnameDescwhere x.age = ?1 order by x.lastname desc
NotfindByLastnameNotwhere x.lastname <> ?1
InfindByAgeIn(Collection<Age> ages)where x.age in ?1
NotInfindByAgeNotIn(Collection<Age> age)where x.age not in ?1
TruefindByActiveTrue()where x.active = true
FalsefindByActiveFalse()where x.active = false

หวังว่าคงพอเข้าใจ concept การสร้าง และการเขียน JPQL น่ะครับ  ^____^

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

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