---
# System prepended metadata

title: Spring Data Projection 整理

---

# 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)