# 六角鼠年鐵人賽 Week 27 - Spring Boot - Spring Data & JPA JPQL
==大家好,我是 "為了拿到金角獎盃而努力著" 的文毅青年 - Kai==
## 諸葛孔明
:::info
非淡泊無以明志,非寧靜無以致遠。
:::
## 主題
有鑑於 @Query Annotation 的部分可能還是有些模糊的地方,因此特別抽出這一塊來說明。
希望讓讀者可以更了解究竟這些寫在 Repository 中的 Query 可以如何變化,讓開發人員能夠更得心應手地寫出符合需求的 SQL commands。
Kai 在上週提及了 Spring Data & JPA 是透過一種 JPQL 的方式處理 SQL 指令。
其實不全然是 JPQL,還有 Native SQL 的部分也包含在 JPA 當中。
JPA 提供了兩種方式讓開發者做彈性化的處理~
## JPQL 和 Native SQL
JPQL 的全名是 **Java Persistence Query Language**,是一種與使用 DB 無關的的物件導向 SQL,實作的方法都已被封裝在 JPA 中,使用者必須遵從 JPQL 規定的格式進行值 (SQL 指令) 的設定,以及帶入參數、條件、集合、排序處理等方法的使用,方能讓 JPA 組成正確的 SQL 指令並執行。
而 JPA 則會根據使用 DB 的類型,決定組成 SQL 指令時候的保留字或 SQL 組成規範,這讓開發人員不需要針對不同的 DB 做太大幅度的調整,盡量讓 JPA 幫忙處理瑣碎的事情即可,因此大大增加了開發的彈性。
兩者最大的差異在於,JPQL 使用的是 Entity 的物件替代了原本的 Table,而 Native SQL 則是使用了原本的 Table。
因此在執行時,JPQL 針對的部分是 Entity 的部分,而 Native SQL 則會將參數、條件帶入後就直接送到 SQL 執行。
:::danger
許多人在寫 JPQL 的指令時常會遇到以下錯誤訊息˙
> Validation failed for query for method public abstract java.util.List
這是因為在寫入的 @Query 中,並沒有正確的使用到 Entity 的表名稱與欄位名稱,因此雖然可以 Compile 過,但在執行時卻因為 Bean 開始帶入建置的關係無法找到對應的 Class 而出錯。
以上週的範例舉實際例子:
```java=
// 錯誤
@Query("SELECT * FROM EMPLOYEE as e WHERE e.EMP_ID <> ?1")
List<Employee> findByPriorityExclude(Long number1);
// 正確
@Query("SELECT * FROM Employee as e WHERE e.emp_id <> ?1")
List<Employee> findByPriorityExclude(Long number1);
```
這兩段 SQL 指令基本上無論在任何 DB 來看都是沒有任何問題的 (除了 ClickHouse 這種非常重視大小寫名稱的之外)
但因為 Kai 的 Entity 中,資料表名稱為 Empolyee 以及 欄位名稱為 emp_id,後續便會因為 **大小寫** 的關係促使 JPA 無法找到對應的 Entity 而報錯。
:::
最後與 Native SQL 比較來看,雖然 JPQL 避免了針對不同 DB 的修改,相反的,在基於 Entity 的規範下,JPQL 僅支援標準的子查詢指令,因此無法像 Native SQL 一次性的使用多階層、多 Table 的子查詢 SQL 指令,意即無法在 JPQL 中做太多複雜的變化。
## 應用方式
JPQL 和 Native SQL 的使用法非常簡單,用上述的例子繼續舉例,以下分別是 JPQL 和 Native SQL 對於同一段 SQL 指令的寫法。
```java=
// JPQL
@Query("SELECT * FROM Employee as e WHERE e.emp_id <> ?1")
List<Employee> findByPriorityExclude(Long number1);
// Native SQL
@Query(nativeQuery = true, value = "SELECT * FROM Employee as e WHERE e.emp_id <> ?1")
List<Employee> findByPriorityExclude(Long number1);
```
關鍵字就是 nativeQuery 這個參數必須要被設置為 true,而且是每一個 Native SQL 的 @Query 都要這樣設置,並沒有支援一次設定全部使用 Native SQL 的方式,但可以透過另外一種方式實現。
那就是在 Entity 中直接使用 @NamedNativeQueries + @NamedNativeQuery 的方式
```java=
@NamedNativeQueries({
@NamedNativeQuery(
name = "Employee.findByPriorityExclude",
query = "SELECT * FROM Employee as e WHERE e.emp_id <> ?1",
resultClass=Employee.class
),
@NamedNativeQuery(
...
),
})
```
完整 Entity Class
```java=
package kai.com.jpa.entity;
import javax.persistence.*;
import java.sql.Date;
@Entity
@NamedNativeQueries({
@NamedNativeQuery(
name = "Employee.findByPriorityExclude",
query = "SELECT * FROM Employee as e WHERE e.emp_id <> ?1",
resultClass=Employee.class
)
})
public class Employee {
public Employee (){}
public Employee(String name, String team, Date birthDate){
this.emp_name = name;
this.emp_team = team;
this.emp_birthDate = birthDate;
}
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long emp_id;
private String emp_name;
private String emp_team;
private Date emp_birthDate;
public Long getEmp_id() {
return emp_id;
}
public void setEmp_id(Long emp_id) {
this.emp_id = emp_id;
}
public String getEmp_name() {
return emp_name;
}
public void setEmp_name(String emp_name) {
this.emp_name = emp_name;
}
public String getEmp_team() {
return emp_team;
}
public void setEmp_team(String emp_team) {
this.emp_team = emp_team;
}
public Date getEmp_birthDate() {
return emp_birthDate;
}
public void setEmp_birthDate(Date emp_birthdate) {
this.emp_birthDate = emp_birthdate;
}
}
```
而後我們在 Repository 的設定就不需要再使用 @Query 而可以直接是一個方法,這個方法名稱會直接對應到剛剛設置的 @NamedNativeQuery 的 name 參數去,從而達到相同的效果
Repository Class
```java=
//@Query("SELECT * FROM Employee as e WHERE e.emp_id <> ?1")
List<Employee> findByPriorityExclude(Long number1);
```
## 置入參數的方式
置入參數的方式有兩種,一種是常見的、類似 Stored Procedure 方式的 Named Parameters,另外一種是 JPQL 特有的 Positional Parameters
```java=
// JPQL Named Parameters 方式 1
@Query("FROM Employee as e WHERE e.emp_id <> :number1")
List<Employee> findByPriorityExclude(Long number1);
// JPQL Named Parameters 方式 2
@Query("FROM Employee as e WHERE e.emp_id <> :getEmpId")
List<Employee> findByPriorityExclude(@Param("getEmpId") Long number1);
// JPQL Positional Parameters 方式
@Query("FROM Employee as e WHERE e.emp_id <> ?1")
List<Employee> findByPriorityExclude(Long number1);
```
**Positional Parameters**
- 依照傳入的參數順序,將其置入 SQL 中
- 簡單易懂
**Named Parameters**
- 依照對應名稱的參數,將其置入 SQL 中
- 若沒有經過良好整理,容易錯亂
## 結語
:::danger
其他還有如 Sorting 和 Paging 等應用方式,基本上與上述的例子大同小異,就不多作介紹~
~~以上差不多就結束了基本的 Spring Data & JPA 的單元部分了~~
~~下一期可以考慮學習 Activity Flow 或是 進入 Spring Cloud 部分(這部分尷尬的是現在諸多被 K8S 取代掉了,學習價值不高... 但為了充實所有的基本知識,應該還是會放在主力目標!)~~
計畫趕不上變化... 才發現 Spring Data & JPA還漏了部分想要分享的內容,見諒,詳細會分享在第 28 篇
[六角鼠年鐵人賽 Week 28 - Spring Boot - Spring Data & JPA QueryDSL](/mC-2-op_Qe-tVLoJ2x-d1w)
:::
首頁 [Kai 個人技術 Hackmd](/2G-RoB0QTrKzkftH2uLueA)
###### tags: `Spring Boot`,`Spring Data`,`w3HexSchool`