2020年5月8日 星期五

DISTINCT

本文為【Spring Boot情境式網站開發指南:使用Spring Data JPA、Spring Security、Spring Web Flow】一書的【 第4章 Criteria API入門】延續,完整範例程式碼可至出版社下載

SQL的DISTINCT用來得到成員不重複的集合物件,因此成員類別必須覆寫hashCode()與equals()方法。

使用DISTINCT取出Entity物件的字串成員

使用以下JPQL查詢時:
SELECT DISTINCT e.dept FROM Employee e
等價於使用以下Criteria API查詢。因為只查詢一個欄位dept,行7使用CriteriaQuery的distinct(true)方法只會針對該欄位過濾重複值:
@Test
public void Criteria_DistinctColumn() {
    EntityManager em = emf.createEntityManager();
    CriteriaQuery sql = em.getCriteriaBuilder().createQuery(String.class);
    Root emp = sql.from(Employee.class);
    sql.select(emp.get(Employee_.dept))
        .distinct(true);
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    assertThat(resultList, contains("IT", "Admin"));
}

使用DISTINCT查詢多欄位

使用以下JPQL查詢時:
SELECT DISTINCT e.dept, e.salary FROM Employee e
ORDER BY e.dept ASC, e.salary DESC
等價於使用以下Criteria API查詢。多欄位的結果使用Object[ ]呈現:
@Test
public void Criteria_DistinctMultiColumns() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Object[].class);
    Root emp = sql.from(Employee.class);
    sql.multiselect( emp.get(Employee_.dept), 
                  emp.get(Employee_.salary))
        .orderBy(cb.asc(emp.get(Employee_.dept)), 
                 cb.desc(emp.get(Employee_.salary)))
        .distinct(true);
    TypedQuery tq = em.createQuery(sql);
    List resultList = tq.getResultList();
    List stringList = resultList.stream()
            .map(arr -> Arrays.toString(arr)).collect(Collectors.toList());
    stringList.forEach(System.out::println);
    assertThat(stringList, contains("[Admin, 3500.0]", 
                                    "[IT, 2500.0]", 
                                    "[IT, 2000.0]"));
}

ORDER BY

本文為【Spring Boot情境式網站開發指南:使用Spring Data JPA、Spring Security、Spring Web Flow】一書的【 第4章 Criteria API入門】延續,完整範例程式碼可至出版社下載

Criteria API使用CriteriaBuilder的方法asc()做升冪(ascending)排序,使用方法desc()表示降冪(descending)排序。

使用asc()、desc()排序

以下範例行8使用CriteriaBuilder的方法asc()傳入salary欄位以建立升冪Order物件,行9則使用desc()建立降冪Order物件,可擇一使用;最終傳入CriteriaQuery的orderBy()方法中:
@Test
public void Criteria_OrderByAsc() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    sql.select(emp);
    Order salaryOrder = cb.asc(emp.get(Employee_.salary));
//  Order salaryOrder = cb.desc(emp.get(Employee_.salary));
    sql.orderBy(salaryOrder);
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    em.close();
    assertThat(resultList, contains(employee3, employee1, employee2));
}
前述範例等價於使用以下JPQL查詢:
SELECT e FROM Employee e ORDER BY e.salary
// SELECT e FROM Employee e ORDER BY e.salary ASC

多欄位排序

CriteriaQuery的orderBy()方法中可以接受多個Order物件,並使用不同的asc()或desc(),如以下範例程式行8、9,以完成多欄位排序:
@Test
public void Criteria_OrderByMultiColumns() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    sql.select(emp)
        .orderBy( cb.asc(emp.get(Employee_.dept)), 
                cb.desc(emp.get(Employee_.salary)) );
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    em.close();
    assertThat(resultList, contains(employee2, employee1, employee3));
}
前述範例等價於使用以下JPQL查詢:
SELECT e FROM Employee e ORDER BY e. dept ASC, e.salary DESC

2020年5月2日 星期六

WHERE條件值使用字面常量(Literal)

字面常量使用字串

使用以下JPQL查詢時:
SELECT e FROM Employee e WHERE e.name = 'Rose'
等價於使用以下Criteria API查詢,且行8顯示否定用法:
@Test 
public void Criteria_EqualString() {
  EntityManager em = emf.createEntityManager();
  CriteriaBuilder cb = em.getCriteriaBuilder();
  CriteriaQuery sql = cb.createQuery(Employee.class);
  Root emp = sql.from(Employee.class);
  Expression expr = cb.equal(emp.get(Employee_.name), "Rose");
// Expression expr = cb.equal(emp.get(Employee_.name), "Rose").not();
  sql.select(emp).where(expr);
  TypedQuery tq = em.createQuery(sql);
  List resultList = tq.getResultList();
  resultList.forEach(System.out::println);
  assertThat(resultList, containsInAnyOrder(employee2));
}

字面常量使用數字

使用以下JPQL查詢時:
SELECT e FROM Employee e WHERE e.salary > 2000.0
等價於使用以下Criteria API查詢。比較數字使用方法gt()表示greater-than:
@Test 
public void Criteria_GreaterNumber() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    Expression expr = cb.gt(emp.get(Employee_.salary), 2000.0);
    sql.select(emp).where(expr);
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    assertThat(resultList, containsInAnyOrder(employee1, employee2));
}

字面常量使用enum

使用以下JPQL查詢時:
SELECT e FROM Employee e WHERE e.role = entity.Role.IT
等價於使用以下Criteria API查詢。Entity欄位型態為enum可以直接使用equal()方法:
@Test 
public void Criteria_EqualEnum() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    Expression expr = cb.equal(emp.get(Employee_.role), Role.IT);
    sql.select(emp).where(expr);
    TypedQuery tq = em.createQuery(sql);
    List resultList = tq.getResultList();
    resultList.forEach(System.out::println);
    assertThat(resultList, containsInAnyOrder(employee1, employee3));
}

字面常量使用日期

使用以下JPQL查詢時:
SELECT e FROM Employee e WHERE e.joinDate > '2017-05-01'
等價於使用以下Criteria API查詢。比較日期時,無法直接比較日期格式如Timestamp,必須借助CriteriaBuilder的literal()方法轉換為Expression<Timestamp>後再套用到greaterThanOrEqualTo(),如範例程式碼行7~10:
@Test 
public void Criteria_GreaterDate() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    Expression timestampExpr = 
       cb.literal( localToTimeStamp(LocalDate.of(2017, 05, 01)) );
    Expression expr = 
       cb.greaterThanOrEqualTo(emp.get(Employee_.joinDate), timestampExpr);
    sql.select(emp).where(expr);
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    assertThat(resultList, containsInAnyOrder(employee4, employee2));
}

字面常量使用boolean

使用以下JPQL查詢時:
SELECT e FROM Employee e WHERE e.fullTime = true
等價於使用以下Criteria API查詢。若Entity的Boolean欄位為true,可以使用範例程式碼行9的方法isTrue()或行10直接省略,兩者效果相同;若欄位值要求為false則使用行11的方法isFalse():
@Test  
public void Criteria_BooleanField() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Employee.class);
    Root emp = sql.from(Employee.class);
    Expression adultExpression = emp.get(Employee_.fullTime);
    sql.select(emp)
        .where(cb.isTrue(adultExpression));
    //  .where(adultExpression);
    //  .where(cb.isFalse(adultExpression));
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    assertThat(resultList, hasItems(employee1));
    assertThat(resultList, hasItems(employee3));
}

使用Criteria API基礎查詢


本文為【Spring Boot情境式網站開發指南:使用Spring Data JPA、Spring Security、Spring Web Flow】一書的【 第4章 Criteria API入門】延續,完整範例程式碼可至出版社下載

JPA對資料庫的存取除了使用JPQL之外,還可以使用Criteria API。Criteria API的大致使用方式已經在前一章節說明,本章重點在資料庫存取情境的因應和與JPQL的比較,原則上將與第3章的各範例程式合併,因此內容編排不再贅述Entity物件的保存(persist)與建構(construct)方式。
以下範例介紹SELECT子句內為單一欄位或複數欄位的作法。
查詢單一欄位
以Entity類別Employee為例,JPQL查詢其單一欄位name時使用指令為:
SELECT e.name FROM Employee e
以下為使用Criteria API的作法:
@Test
public void criteria_SelectSingle() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(String.class);
    Root emp = sql.from(Employee.class);
    Path name = emp.get("name");
    sql.select(name);
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(System.out::println);
    em.close();
    assertThat(resultList, hasSize(3));
}
【說明】
5
CriteriaQuery指定要回傳的資料型態,欄位name的型態為String。
7-8
CriteriaQuery的select()方法接受Expression型態的物件,Path為其子型態,也可以使用Expression<String>宣告變數。
查詢複數欄位
以Entity類別Employee為例,JPQL查詢欄位id與name時使用指令為:
SELECT e.id, e.name FROM Employee e
以下範例行7與行8為使用Criteria API的作法。因為CriteriaBuilder的方法array()接受「可變動個數參數(varargs)」,且介面Selection是介面Expression的父型態,因此方法參數可接受個數變動的Expression物件:
CompoundSelection array(Selection... elections);
@Test 
public void criteria_SelectArray() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Object[].class);
    Root emp = sql.from(Employee.class);
    sql.select(cb.array(emp.get("id"), 
                         emp.get(Employee_.name)));
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(arr -> System.out.println(Arrays.toString(arr)));
    em.close();
    assertThat(resultList, hasSize(3));
}

【說明】
5
CriteriaQuery指定查詢後得到的資料型態,可使用Object[]同時將欄位id, name的資料封裝。
7-8
CriteriaQuery的select()方法接受多個Expression型態的物件。
7
欄位名稱使用String描述。
8
欄位名稱使用metamodel類別Employee1_的靜態欄位name描述。
另外一個查詢複數欄位的作法為使用CriteriaQuery的multiselect()方法,如以下範例行7。因為方法multiselect()接受可變動個數參數,且介面Selection是介面Expression的父型態,因此方法參數可接受個數變動的Expression物件:
CriteriaQuery multiselect(Selection... elections);
此時不需要使用CriteriaBuilder的array()方法結合欄位,直接列舉即可:
@Test 
public void criteria_MultiSelect() {
    EntityManager em = emf.createEntityManager();
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery sql = cb.createQuery(Object[].class);
    Root emp = sql.from(Employee.class);
    sql.multiselect(emp.get(Employee_.id), emp.get(Employee_.name));
    TypedQuery typedQuery = em.createQuery(sql);
    List resultList = typedQuery.getResultList();
    resultList.forEach(arr -> System.out.println(Arrays.toString(arr)));
    em.close();
    assertThat(resultList, hasSize(3));
}

2020年4月26日 星期日

使用交易的傳播機制

@Transactional的屬性propagation定義了交易的邊界(boundary)或稱為傳播的方式。因為@Transactional標註的方法可能再呼叫其他方法,Spring會呼叫TransactionManager的getTransaction()方法來取得或建立與資料庫的交易,並根據屬性propagation的設定決定要啟動和暫停交易;但並非TransactionManager的所有實作都直接支援所有交易傳播方式。後續介紹幾種常用的傳播方式以及它們如何作用。
以REQUIRED的機制傳播交易
「Propagation.REQUIRED」是屬性propagation預設值,使用方式如下:
@Transactional (propagation = Propagation.REQUIRED)
public void requiredCase() { 
    // ... 
}
因為是預設,也可以省略:
@Transactional
public void requiredCase() { 
    // ... 
}
若方法上標註@Transactional (propagation = Propagation.REQUIRED),Spring將:
1.     檢查是否有使用中的交易,如果不存在就「建立一個新的交易」。
2.     若存在交易但驗證為無效,則拋出例外。
3.     若存在交易且驗證為有效,則使用該交易。
這樣的概念可以使用以下的偽代碼(pseudo-code)來示意:
if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
return createNewTransaction();
以SUPPORTS的機制傳播交易
若屬性propagation值使用「Propagation.SUPPORTS」:
@Transactional (propagation = Propagation.SUPPORTS)
public void supportsCase() { 
    // ... 
}
則Spring將:
1.     檢查是否有使用中的交易,如果不存在就「不使用交易」。
2.     若存在交易但驗證為無效,則拋出例外。
3.     若存在交易且驗證為有效,則使用該交易。
這樣的概念可以使用以下的偽代碼(pseudo-code)來示意:
if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
return emptyTransaction();
以MANDATORY的機制傳播交易
若屬性propagation值使用「Propagation.MANDATORY」:
@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryCase() { 
    // ... 
}
則Spring將:
1.     檢查是否有使用中的交易,如果不存在就「拋出例外」。
2.     若存在交易但驗證為無效,則拋出例外。
3.     若存在交易且驗證為有效,則使用該交易。
這樣的概念可以使用以下的偽代碼(pseudo-code)來示意:
if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
throw IllegalTransactionStateException;
以NEVER的機制傳播交易
若屬性propagation值使用「Propagation.NEVER」:
@Transactional(propagation = Propagation.NEVER)
public void neverCase() { 
    // ... 
}
則Spring預期不存在交易;執行時期將檢查是否有使用中的交易,若存在就拋出例外。
這樣的概念可以使用以下的偽代碼(pseudo-code)來示意:
if (isExistingTransaction()) {
    throw IllegalTransactionStateException;
}
return emptyTransaction;
以NOT_SUPPORTED的機制傳播交易
若屬性propagation值使用「Propagation.NOT_SUPPORTED」:
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupportedCase() { 
    // ... 
}
則Spring將檢查是否有使用中的交易,如果存在就「暫停」;因此商業邏輯將在交易暫停的情況下被執行。
使用JTATransactionManager可以直接支援這類需求,其他情況則可能是以模擬的方式進行。
以REQUIRES_NEW的機制傳播交易
若屬性propagation值使用「Propagation.NEVER」:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNewCase() { 
    // ... 
}
則Spring將:
1.     檢查是否有使用中的交易,如果不存在就「建立新交易」。
2.     若存在交易則暫停原交易,並建立新交易。
這樣的概念可以使用以下的偽代碼(pseudo-code)來示意:
if (isExistingTransaction()) {
    suspend(existing);
    try {
        return createNewTransaction();
    } catch (exception) {
        resumeAfterBeginException();
        throw exception;
    }
}
return createNewTransaction();
與NOT_SUPPORTED相似,使用JTATransactionManager可以直接支援這類需求,其他情況則可能是以模擬的方式進行。

使用標註類別@Transactional進行交易

標註類別@org.springframework.transaction.annotation.Transactional用來將方法內的資料庫存取包含於資料庫交易中,可以設定交易的傳播(propagation),隔離(isolation),超時(timeout),唯讀(read-only)和回復(rollback)條件等,也可以設定交易管理器(transaction manager)。
@Transactional的運行方式
假設我們以@Transactional標註callMethod()方法,Spring會在方法周圍(around)以AOP機制處理交易需求:
createTransactionIfNecessary();  //若必要將產生交易
try {
    callMethod();
    commitTransactionAfterReturning();  //若正常結束則commit
} catch (exception) {
    rollbackTransactionAfterThrowing();  //若異常結束則rollback
    throw exception;
}
如何使用@Transactional
我們可以將@Transactional標註在介面(interfaces)、類別(classes)的宣告上,或是直接標註在方法(method)上。若彼此間衝突或不一致時,將以覆寫(override)的概念決定有效順位,原則是:
1.            標註在「類別方法」時高於「父類別方法」,又高於「介面方法」。
2.            標註在「類別宣告」時高於「父類別宣告」,又高於「介面宣告」。
3.            標註在「方法」時高於「類別宣告」或「介面宣告」。
當@Transactional標註在類別的宣告上時,效果將自動擴充到類別內每一個沒有使用@Transactional標註的public方法;若private或protected的方法上被開發者以@Transactional標註,也將自動被Spring框架忽略。
以下示範標註在介面宣告上:
@Transactional
public interface TransferService {
    void transfer(String user1, String user2, double val);
}
一般而言,不會建議在介面上直接使用類似@Transactional的功能性標註類別,因為介面的設計要考慮一般性,不見得每個實作都有需要使用資料庫交易;但如@Repository這種分類型標註類別就比較沒有這樣的限制,因為每個實作都還會是Repository類別。
一旦有介面或父類別被標註@Transactional,就可以在子類別上標註@Transactional以覆寫設定:
@Service
@Transactional
public class TransferServiceImpl implements TransferService {
    @Override
    public void transfer(String user1, String user2, double val) {
        // ...
    }
}
所以也可以在方法上直接標註@Transactional來覆寫類別上的標註:
@Transactional
public void transfer(String user1, String user2, double val) {
    // ...
}

2020年4月12日 星期日

SpringBoot的日誌(logging)機制


Java常見的幾種日誌(logging) API關係如下:




因為日誌API的實作不止一種,常見程式手法為依賴抽象層「slf4j」,再以設定的方式抽換實作。Spring Boot專案預設實作為「logback」,將在執行時期自動注入。
使用方式如:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Controller
public class HomeController {
    Logger logger = LoggerFactory.getLogger(HomeController.class);
    @RequestMapping(value = { "/", "/homepage" }, method = GET)
    public String home() {
        testLogLevel();
        return "home";
    }
    private void testLogLevel() {
        logger.trace("A TRACE Message");
        logger.debug("A DEBUG Message");
        logger.info("An INFO Message");
        logger.warn("A WARN Message");
        logger.error("An ERROR Message");
    }
}
相關日誌紀錄的設定(application.properties)為:
# Core properties(logging)
logging.level.com.rwd=debug
logging.file=D:/springBoot4War.log
#logging.pattern.console= %d{yyyy-MM-dd HH:mm:ss} - %logger{36} - %msg%n
#logging.pattern.file= %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%