(20) Controller、Service、Repository的建立 - 1 _ JpaRepository 的使用

Albert Hg
learning-from-jhipster
13 min readAug 24, 2021

本篇將會介紹Controller、Service、Repository的差異,套用這樣的觀念來完成一隻API從接收Request、資料處理與儲存、回傳Response的整個流程來做解析與展示。

在上一篇文章中,我們已經導入Spring-Data-JPA,同時也建立與綁定與DTO的關係。那麼我們如何透過Spring-Data-JPA來進行資料的操作也就是本篇最主要的目的。但是在這之前,我們必須先認識幾個Annotation之間的關係。

因此本篇文章將會依照下方的主題依序進行介紹:

  • @RestController、@Service、@Repository 的差異
  • JpaRepository 的介紹
  • 使用 @Repository 以及 JpaRepository
  • 實踐 @Service 並注入 Repository
    - 新增與更新資料
    - 刪除資料
    - 搜尋資料

@RestController、@Service、@Repository 的差異

相信你們一定都有聽過 MVC 設計模式(Model、View、Controller)。在比較早期的開發模式中,MVC 是最熱門的一種模式,開發者們會把 API 接口以及相關的控制邏輯都寫在 Controller 內,在資料層面或商業邏輯則是由 Model 來進行實作,並將最後的結果回傳至 View 頁面中。

但這樣長久下來,程式與程式之間的耦合性就會非常高,容易寫出難以維護的義大利麵程式。為了要將程式好好的模組化,以方便簡單管理與重複利用,因此後續衍伸出了現在這樣的模式,也就是:

View ⇆ Controller ⇆ Service ⇆ Repository ⇆ Model
  • View 專注於資料的呈現頁面
  • Controller 負責 API 的接口
  • Service 處理背後的商業邏輯
  • Repository 專門與資料庫進行操作
  • Model 作為資料與 DAO 的 Mapping

如此一來,在分工的部分就會更為明確。在 Controller 的部分,如果是開發 SpringBoot,我們常常會使用 @RestController 來標記 Class 為 Controller,這個部份我們曾經有在之前的文章「(6) Web Starter開發API — Controller」探討過。

而 Service 的部分會使用 @Service 來標記 Class。這個部分主要是作為商業邏輯的開發,通常在寫 Service 的部分會特別注意「單一原則」,以確保每個 function 內只專注做好一件事情,同時也方便後續的單元測試的進行。

最後是我們今天的重點,@Repository。 Repository 是一種介面,我們參考下方的說明,可以知道Repository 非常複雜,因為他同時管理了資料面的封裝與映射,並且用 Collection 的 Interface 來存取物件。Collection 介面在使用上進行了統一,同時也集合了不同的數據結構,例如 Set、List、Queue,因此在開發上除了彈性以外,更是節省了一些開發時間。

repository is a mechanism for encapsulating storage, retrieval, and search behavior which emulates a collection of objects.# from https://zetcode.com/springboot/repository/

JpaRepository 的介紹

JpaRepository 是作為 Repository 應用的一種繼承的「抽象介面」,他允許我們可以透過介面的使用,就直接與資料庫進行映射與溝通。

繼續往下深入探討 JpaRepository,從原始碼中可以看到他繼承了兩種介面:PagingAndSortingRepository 與 QueryByExampleExecutor,他們的介面與繼承關係可以參考下圖:

所以只要透過 JpaRepository,就可以簡單的使用那些「新增、修改、刪除」等基本功能,而不須自己再重新造輪這些常常會使用到的功能。除此之外還可以使用使用那些基本的查找,像是「排序、分頁、搜尋ID」。

使用 @Repository 以及 JpaRepository

在先前的幾篇文章中我們已經建立好了 MemberDTO.java,並且也將 MemberDTO 透過 @Entity 進行與 Table 的綁定。

接著如果想要建立可以透過 JPA 對資料庫進行操作的 Repository,可以這樣做:

https://gist.github.com/albert-hg/437a02be148365043bba65c52d41263b

沒錯,甚麼都不用加入,因為繼承 JpaRepository,所以不需要自己實作任何方法,就可以簡單的使用「新增、修改、刪除」等基本功能。

但必須注意的是,因為 JpaRepository的泛型為 <T,ID>,所以在使用繼承時,必須定義好 T 與 ID 的型別,也就是 <MemberDTO, Long>。

實踐 Service 並注入 Repository

接著我們要來實作透過Service來向Repository進行資料操作的方法了。首先我們會先建立一個跟 MemberDTO 相關的 Service,並用 @Service 的標註來標註這個 Class:

https://gist.github.com/albert-hg/dbf92c06c5f8412a8637a68292a32488

因為 Spring 的特性,所以只要當我們用 @Controller、@Service、@Repository 等標記,就可以在服務啟動的時候先進行 Bean 的初始化與建立,同時也可以注入特定的物件,也就是 Dependency Injection(DI)。

DI 除了方便載入所需要使用的物件外,其優點還有「降低耦合性」、「明確的讓開發者知道該物件被注入了那些相依物件」、「提高可維護性」、「提高可測試性」。

新增與更新資料

稍微了解一下 DI 的優點後,我們就可以對 MemberService.java 來注入所需的 Repository 在 Constructor 中,並加上一個「save()」的功能:

https://gist.github.com/albert-hg/79cf481a2ab8c9430582f94c95039bb5

如此一來就可以直接將傳進來的 DTO 存在資料庫內了!值得注意的是,Repository 的 save() 除了儲存新資料外,也包含更新資料的方法,而更新的依據會依照將要儲存的資料的主鍵(PK)是否存在,如果存在則為更新,不存在則為新增。

例如:

資料庫內已經存在一筆 MemberDTO {id: 1001, name: 'albert'},其中 id 為 PK1) 若save的資料為 {id: 1001, name: 'robert'},則會將資料庫內 id 為 1001 且 name 為 albert 的值改為 robert2) 若save的資料為 {name: 'robert'},則會在資料庫內新建立一筆 name 為 robert 的資料,id 的部分會透過 @GeneratedValue 的 strategy 決定

而實際在操作時,Hibernate 會下的指令像這樣:

// create
Hibernate: insert into member (name, id) values (?, ?)
// update
Hibernate: select * from member where member.id=?
Hibernate: update member set name=? where id=?

刪除資料

在整個 JpaRepository 可以使用的刪除方法總共有六種:

  1. void deleteById(ID arg0);
    直接透過 ID 來刪除資料,但當沒刪除的 ID 不存在時,會丟出 EmptyResultDataAccessException 的錯誤訊息。
  2. void delete(T arg0);
    傳入一個 Entity 來刪除資料該 Entity 的 ID 的資料,其實內部是使用 deleteById 的方法,差別在於單純使用 delete 時,如果 ID 不存在也不會丟出錯誤訊息。
  3. void deleteAll(Iterable<? extends T> arg0);
    傳入一組 Entity Array,內部透過迴圈「一一使用 delete 刪除 Entity」。
  4. void deleteAll();
    內部會先取得所有Entity,再透過迴圈「一一使用 delete 刪除 Entity」。
  5. void deleteAllInBatch();
    相比 deleteAll 先取得所有 Entity 再一個一個刪除,deleteAllInBatch 則是直接向資料庫傳送一整張表的命令。
  6. void deleteInBatch(Iterable<T> arg0);
    相比 deleteAll 一個一個送出刪除的命令,deleteInBatch 會將傳入要刪除的 Entity 的 ID,使原本需要透過多個刪除的命令使用 「or」的方式,將多個命令組合成一個,以減少多次對資料庫的造訪,通常這種刪除方法的效率會比較快一些。

這六個 delete 的方法,在 Hibernate 的部分會像這樣下命令:

// deleteById(ID arg0)
Hibernate: select * from member where member.id=?
Hibernate: delete from member where id=? // 都會執行,因此可能會報錯
// delete(T arg0)
Hibernate: select * from member where member.id=?
Hibernate: delete from member where id=? // 依select的結果決定執行與否
// deleteAll(Iterable<? extends T> arg0)
Hibernate: select * from member where member.id=?
Hibernate: delete from member where id=? // 依select的結果決定執行與否
// deleteAll()
Hibernate: select * from member
Hibernate: delete from member where id=? // 執行多次select的結果
// deleteAllInBatch()
Hibernate: delete from member // 直接刪掉整張表
// deleteInBatch(Iterable<T> arg0)
Hibernate: delete from member where id=? or id=? or id=? or id=?

搜尋資料

取得資料是最常使用到的功能,但是當我們查看 JpaRepository 的那些關聯圖,會發現光是 findAll 就有好幾種不同的形式可以使用,所以現在要來討論這些 findAll 之間的區別。

  1. List<T> findAll();
    取得全部資料。
  2. List<S> findAll(Example<S> arg0);
    傳入一個物件,並搜尋與這些資料相符合的所有結果。
  3. Page<T> findAll(Pageable arg0);
    使用 Pageable 的物件定義回傳的分頁內容。
  4. List<T> findAll(Sort arg0);
    使用 Sort 的物件定義回傳的分頁內容。
  5. Page<S> findAll(Example<S> arg0, Pageable arg1);
    傳入一個物件,並搜尋與這些資料相符合的特定分頁的結果。
  6. List<S> findAll(Example<S> arg0, Sort arg1);
    傳入一個物件,並搜尋與這些資料相符合的排序過後的結果。

上面所提到跟分頁與排序的相關使用,都會在下一篇一起做說明。另外在 Example 的部分,Example 作為一個被尋找物件,但該物件個每個欄位不一定需要都有設定搜尋條件,例如:

https://gist.github.com/albert-hg/2b70db1a012823ba7439c1f91715328f

而除了 findAll 以外,還有剩下一些搜尋是針對某些特別的條件,像是:

  1. List<T> findAllById(Iterable<ID> arg0);
    傳入一組 ID 的 Array 或 List,以搜尋出對應的結果。
  2. Optional<T> findById(ID arg0);
    傳入一個 ID,並找出該 ID 的結果。此回傳會用 Optional 封裝,可以透過 Optional 的 isPresent() 方法來檢查回傳的結果是否有值。
  3. Optional<S> findOne(Example<S> arg0);
    傳入一個「完整的」物件,並回傳搜尋結果。

到目前為止,我們將 JpaRepository 提供的搜尋資料的方法全部介紹完了。現在我們來簡單的挑幾個搜尋的方法實作在 Service 中,比較常見的會是 findAllByPageable 以及 findById

https://gist.github.com/albert-hg/f917869c05d3b4a4b23cc527f427886e

文末

在 Controller、Service、Repository 的部分我打算分兩篇文章來介紹。這一篇主要介紹 Repository 怎麼使用,並且簡單的實作了 Service 的內容。

下一篇文章將會專注在 Controller,以及 Pageable 與 Sort 作為 API 的使用。

--

--

Albert Hg
learning-from-jhipster

I am a programmer but love other things. I am a nobody but keep myself going. I am a person who wishes to reach the heaven but lost the wings.