Try   HackMD

Spring Data Projection 整理

Projection 是指 select 時只回傳指定欄位,在 Spring Data 中有幾種情境。
假設有個 Entity 為:

@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; } }
@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 中,如:

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 為:

@Repository public interface TestRepository extends JpaRepository<Test, String> { // 這裡回傳的是 TestDTO TestDTO findByName(String name); }

執行的 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 可以改成:

public interface TestDTO { // 我們只想查詢 name 和 createTime String getName(); Date getCreateTime(); }

Repository 可以不用修改,這時執行的 SQL 一樣只會查詢 Name 和 CreateTime:

select test0_.Name as col_0_0_, test0_.CreateTime as col_1_0_ from Test test0_ where test0_.Name=?

projection interface 還可以動態地組合欄位, 比方我們的 TestDTO 想要把 namecreateTime 合成 createInfo,我們可以用 @Value 來達成:

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 的用法,然後在 @Querynew 該 projection class,例如:
    ​​​​@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:
    ​​​​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 為:
    ​​​​@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 為:
    ​​​​public interface TestDTO { ​​​​ // 因為 HQL 中欄位別名為 customerName, 所以它的 getter 為 getCustomerName ​​​​ String getCustomerName(); ​​​​ // 因為 HQL 中欄位別名為 createTime, 所以它的 getter 為 getCreateTime ​​​​ Date getCreateTime(); ​​​​}
    這時執行的 SQL 為:
    ​​​​select test0_.Name as col_0_0_ from Test test0_ where test0_.Name=?
  3. 將回傳類別改成 Map<String, Object>,同時要給予欄位別名,這個別名變成回傳 Map 的 key 比方:
    ​​​​@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 為:
    ​​​​select test0_.Name as col_0_0_, test0_.CreateTime as col_1_0_ from Test test0_ where test0_.Name=?
    但我們就必須用別名當 key 來取值
    ​​​​String name = (String) map.get("Name"); ​​​​Date createTime = (Date) map.get("createTime");
  4. 將回傳類別改成 Object[][] 比方:
    ​​​​@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 為:
    ​​​​select test0_.Name as col_0_0_, test0_.CreateTime as col_1_0_ from Test test0_ where test0_.Name=?
    但我們就必須用陣列 index 順序來取值取值
    ​​​​String name = (String) array[0][0]; ​​​​Date createTime = (Date) array[0][1];

如果 @Query 中是原生 SQL,也就是有加上 native=true 比方:

@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[][] 的做法

參考來源: