# Spring Data Projection 整理
Projection 是指 select 時只回傳指定欄位,在 Spring Data 中有幾種情境。
假設有個 Entity 為:
```java=
@MappedSuperclass
public class BaseEntity {
@Column(name = "CreateTime", columnDefinition = "datetime2")
private Date createTime;
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
```
```java=
@Entity
@Table(name = "Test")
public class Test extends BaseEntity {
@Id
@Column(name = "Name", columnDefinition = "varchar(10)")
private String name;
@Column(name = "TestDate", columnDefinition = "datetime2")
private Date testDate;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getTestDate() {
return testDate;
}
public void setTestDate(Date testDate) {
this.testDate = testDate;
}
}
```
---
#### 當使用 Spring Data 提供的 findByXXX
###### Projection Class
使用 `JPARepository` 提供的 `findByXXX` method,這時我們可以用 projection class 並把我們想指定的回傳欄位寫在 constructor 中,如:
```java=
public class TestDTO {
private String name;
private Date createTime;
public TestDTO(String name, Date createTime) {
this.name = name;
this.createTime = createTime;
}
public String getName() {
return name;
}
public Date getCreateTime() {
return createTime;
}
/**
* 實作 equals 和 hashCode 是為了當回傳 Set<TestDTO> 時, 可以避免重複的 TestDTO
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestDTO testDTO = (TestDTO) o;
return Objects.equals(name, testDTO.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
```
這時我們的 Repository 為:
```java=
@Repository
public interface TestRepository extends JpaRepository<Test, String> {
// 這裡回傳的是 TestDTO
TestDTO findByName(String name);
}
```
執行的 SQL 為:
```sql=
select test0_.Name as col_0_0_, test0_.CreateTime as col_1_0_ from Test test0_ where test0_.Name=?
```
可以看到產生的 SQL 只有 select Name 和 CreateTime 兩個欄位。
###### Projection Interface
另一個方式是提供 projection interface 其中有我們想要的欄位的 getter,比方我們的 `TestDTO` 可以改成:
```java=
public interface TestDTO {
// 我們只想查詢 name 和 createTime
String getName();
Date getCreateTime();
}
```
Repository 可以不用修改,這時執行的 SQL 一樣只會查詢 Name 和 CreateTime:
```sql=
select test0_.Name as col_0_0_, test0_.CreateTime as col_1_0_ from Test test0_ where test0_.Name=?
```
projection interface 還可以動態地組合欄位, 比方我們的 `TestDTO` 想要把 `name` 和 `createTime` 合成 `createInfo`,我們可以用 `@Value` 來達成:
```java=
public interface TestDTO {
// getCreateInfo() 回傳的值會是 name + ' ' + createTime
@Value("#{target.name + ' ' + target.createTime}")
String getCreateInfo();
}
```
這樣查詢出來的 `TestDTO` 呼叫 `getCreateInfo` 時的值會是:
`test 2021-01-13 14:03:37.07`
---
#### 當使用 @Query
另一種情況是有用 `@Query` 提供執行的 SQL,若 `@Query` 中是 HQL 這時我們可以有下面幾種做法:
1. 使用上面提到的 projection class 加上 constructor 的用法,然後在 `@Query` 中 `new` 該 projection class,例如:
```java=
@Repository
public interface TestRepository extends JpaRepository<Test, String> {
// 這裡直接 new TestDTO 並把 t.name 傳入 constructor 中
@Query(value = "select new com.example.demo.TestDTO(t.name, t.createTime) from Test t where t.name = ?1")
TestDTO findByName(String name);
}
```
執行的 SQL:
```sql=
select test0_.Name as col_0_0_, test0_.CreateTime as col_1_0_ from Test test0_ where test0_.Name=?
```
2. 使用 projection interface,`@Query` 中的欄位要給予別名,而 projection interface 的 getter 名稱則要符合別名,注意這邊別名時是使用駝峰式命名,比方我們的 Repository 為:
```java=
@Repository
public interface TestRepository extends JpaRepository<Test, String> {
// 這裡給予別名 customerName 和 createTime
@Query(value = "select t.name as customerName, t.createTime as createTime from Test t where t.name = ?1")
TestDTO findByName(String name);
}
```
所以我們的 projection interface 為:
```java=
public interface TestDTO {
// 因為 HQL 中欄位別名為 customerName, 所以它的 getter 為 getCustomerName
String getCustomerName();
// 因為 HQL 中欄位別名為 createTime, 所以它的 getter 為 getCreateTime
Date getCreateTime();
}
```
這時執行的 SQL 為:
```sql=
select test0_.Name as col_0_0_ from Test test0_ where test0_.Name=?
```
4. 將回傳類別改成 `Map<String, Object>`,同時要給予欄位別名,這個別名變成回傳 `Map` 的 key 比方:
```java=
@Repository
public interface TestRepository extends JpaRepository<Test, String> {
// 這裡用 as Name 做為別名, 大小寫不重要
@Query(value = "select t.name as Name, t.createTime as createTime")
Map<String, Object> findByName(String name);
}
```
這時執行的 SQL 為:
```sql=
select test0_.Name as col_0_0_, test0_.CreateTime as col_1_0_ from Test test0_ where test0_.Name=?
```
但我們就必須用別名當 key 來取值
```java=
String name = (String) map.get("Name");
Date createTime = (Date) map.get("createTime");
```
4. 將回傳類別改成 `Object[][]` 比方:
```java=
@Repository
public interface TestRepository extends JpaRepository<Test, String> {
@Query(value = "select t.name, t.createTime from Test t where t.Name = ?1")
Object[][] findByName(String name);
}
```
這時執行的 SQL 為:
```sql=
select test0_.Name as col_0_0_, test0_.CreateTime as col_1_0_ from Test test0_ where test0_.Name=?
```
但我們就必須用陣列 index 順序來取值取值
```java=
String name = (String) array[0][0];
Date createTime = (Date) array[0][1];
```
如果 `@Query` 中是原生 SQL,也就是有加上 `native=true` 比方:
```java=
@Repository
public interface TestRepository extends JpaRepository<Test, String> {
@Query(value = "select t.Name, t.CreateTime from Test t where t.Name = ?1", native=true)
TestDTO findByName(String name);
}
```
這時就無法用 projection class 在 SQL 中 `new`,但下面幾種做法依然可以使用:
1. 直接使用上面提到的 projection interface
2. 將回傳類別改成 `Map<String, Object>` 的做法
3. 將回傳類別改成 `Object[][]` 的做法
參考來源:
- [Spring Data JPA Projections](https://www.baeldung.com/spring-data-jpa-projections)
- [詳解Spring Data JPA系列之投影(Projection)的用法](https://www.itread01.com/article/1500000915.html)