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