本次練功場學習重點:
教你怎麽用Spring Boot實作出 Basic-3-Layer架構的服務。
什麼是Basic-3-Layer?
(可能說法不同跟名詞表示不太一樣,希望我有把概念傳達正確,寫不對還請各位指教…)
Controller Layer:
Web端最上層,負責接受使用者輸入,然後將執行的結果回應給使用者,算是程式的進入點了。
Service Layer:
負責Bussines Login商業邏輯,把一些你想要定義跟實現的規則寫在這裡。
Data Access Layer:
主要是資料的存取,跟database資料庫溝通。
範例程式的需求
做出一個建立並可查詢使用者的服務功能。
從上面這一句話,我們就能試著用Basic-3-Layer將它拆解。
(1)做出一個可建立新使用者,並且可查詢使用者的資料的功能 ← 這應該就是在說Service Layer的部分
(2)產出建立跟查詢使用者http request API ←這邊就是Controller Layer,傳入參數呼叫API,並得到伺服器給我們的結果
(3)一個資料庫來存放我們建立的使用者←這不用多說就是Data Access Layer,存取資料的地方了
IntelliJ產生Spring Boot專案(需要Ultimate版本)
在初始專案的部分,一樣用Spring Intializer產生,還沒用過的人可以先去看第一篇的教學: https://t.co/Jrng6wnant?amp=1
這次的範例選用了MySQL當資料庫,要在Dependencies要多勾選SQL → Spring Data JPA 以及 SQL →MySQL Driver,而Web一樣勾選 → Spring Web,之後的步驟都是下一步下一步就能完成囉。
資料庫建置:
MySQL的建置可以有分多方法,但我們算是在學習測試階段,習慣用MAC/Linux系統的人,安裝Docker會是比較快的方式,而用Windows系統的夥伴除非你的版本支援,否則可能裝個VM裡頭再裝Docker,或是直接就在你的本機端安裝MySQL就好了。(需要Docker MySQL教學的就給我狂拍手好了…因為安裝MySQL不是本次練功重點)
與MySQL資料連接
當專案順利建立完後,先試著跟你在Docker或是本機建立好的MySQL資料庫連線,這樣在寫程式後,就能無縫接軌地跟資料庫連線了。
步驟一:在Database找到+號,選擇DataSource → MySQL
步驟二:輸入你的MySQL Host, 登入的User帳密,然後按一下Test Connection,打勾表示連線成功,可以按下OK!
步驟三:連線後就能看到Database區塊出現你的localhost的資料庫了,接著我們繼續下一步,滑鼠右鍵點選schemas →New →Schema
步驟四:跳出建立視窗,將schema_name取名為test,因為我們的資料庫要取名為test
步驟五: 請在專案資料夾找到resources/application.properties
步驟六: 填入以下設定值到application.properties,而特別注意spring.jpa.hibernate.ddl-auto=update表示在你之後的enity程式變更後,你的資料庫的table也會隨之一起更新,若不要這樣做,可以設定成none
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=你自己的資料庫密碼
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
欲知更詳細Access Data with MySQL,可去官網有介紹
https://spring.io/guides/gs/accessing-data-mysql/
專案的架構
處理完資料庫的設定後,可以開始進入coding部分了! 我們在專案資料夾下,先建立底下幾個package,而我會先從資料層開始從下往上講起。
data
定義User的資料結構(dto),及對應到資料庫的資料表(entity)
/data/dto/UserDto.kt
定義使用者的id, 名字跟年齡
data class UserDto(
val id: Long, //使用者的id
val name: String, //使用者的名字
val age: Int //使用者的年齡
)
/data/entity/User.kt
(1)在User data class外層加上@Entity跟@Table的annotation
(2)將id加上@Id, @GeneratedValue(strategy = GenerationType.SEQUENCE) 表示告訴資料庫是這個Table的id且數值往上遞增(就是多新增一個User,id就會加1)
(3)其餘的name跟age上加上@Column,代表了資料表的欄位
以上的動作將會在程式執行後在MySQL資料庫產生對應的Table
@Entity
@Table
data class User (
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
val id: Long?,
@Column
val name: String,
@Column
val age: Int
)
repository
定義User Repository的介面跟實作
repository/UserRepository.kt
class外層加上@Repository表示標記了該類別是處理資料層的部分,然後繼承了JpaRepository跟JapSpeficificationExecutor,裡頭都已經實作跟資料庫溝通的CRUD動作了,所以就能直接使用像是save跟findById的method,之後將會在Service層那邊使用到。
@Repository
interface UserRepository: JpaRepository<User,Long>, JpaSpecificationExecutor<User>{
}
service
定義User Service的介面跟實作
/service/UserService.kt
終於要來實現我們一開始的需求,所以這邊定義了fun addUser新增使用者跟findById查詢使用者的介面
interface UserService {
fun addUser(userDto: UserDto): UserDto
fun findById(id:Long): UserDto
}
/service/impl/RealUserService.kt
在class外層加上@Service,表示這個類別是標記專門處理Service 層
(1)fun addUser 的動作:將呼叫傳入的使用者資料userDto:UserDto,藉此產生新的User(id = null, name = userDto.name, age = userDto.age)資料庫所需資料,接著透過UserRepository的save將它存進資料庫裡頭。
(2)fun findById 的動作:將想要查詢的使用者id,透過UserRepository的findById方法,將從資料庫查訊到的User物件,最後再把值塞到UserDto(it.id!!, it.name, it.age)回傳給上層
@Service
class RealUserServiceImpl(val userRepository: UserRepository) : UserService {
override fun addUser(userDto: UserDto) =
User(id = null,
name = userDto.name,
age = userDto.age
).run {
val newUser = userRepository.save(this)
UserDto(newUser.id!!, newUser.name, newUser.age)
}
override fun findById(id: Long) =
userRepository.findById(id).map {
UserDto(it.id!!, it.name, it.age)
}.orElseThrow {
RuntimeException()
}
}
controller
定義呼叫User API的介面 (interface)跟實作(implement)
/controller/UserApi.kt
(1) 新增使用者 fun addUser
@PostMapping(“/user”) 表示http request用Post方式進行
(@RequestBody userDto:UserDto ) 表示將以UserDto的資料格式傳入
(2)查詢使用者fun findById
@GetMapping(“/user”) 表示http request用Get方式進行
(@RequestParm id:Long):UserDto 表示用id傳入參數,而回傳值使用UserDto的資料格式
interface UserApi{
@PostMapping("/user")
fun addUser(@RequestBody userDto:UserDto): UserDto
@GetMapping("/user")
fun findById(@RequestParam id:Long): UserDto
}
三種傳值方式,依需求可以換成
@RequestBody @RequestParam @PathVariable
/controller/impl/UserController.kt
(1)fun addUser 的動作:將傳入的使用者資料UserDto,傳遞給UserService,接著再從UserService傳遞給UserRepository達到新增使用者的功能。
(2)fun findById 的動作:將傳入的查詢使用者id,接著再從UserService傳遞給UserRepository達到查詢使用者的功能
@RestController
class UserController(val userService: UserService) :UserApi {
override fun addUser(userDto: UserDto) = userService.addUser(userDto)
override fun findById(id: Long): UserDto = userService.findById(id)
}
測試執行
程式終於千辛萬苦寫完了,那就要開始編譯執行並測試我們的API功能是不是正確。
按下工具列的綠色執行箭頭,你可以在Run的console mode的log觀察一下,在前面寫的enity的部分就在這默默在MySQL資料庫被建立起來了!
然後再去Database看一下有沒有使用者Table顯示出來,這裡你可能要先在schemas按下右鍵的refresh,因為似乎不會即時更新,更新完就能看到user table了!
新建一個使用者
我們透過IntelliJ Ultimate版本內建的功能Http Client來做呼叫API的動作,只要在專案夾右鍵按New →Http Request,跳出視窗取一個名稱就能開始使用。
接著就會產生一個UserRequest.http檔案,開啟後可以看到Add Request的選項,給他點下去,選擇Post Text Body,它會先幫我們產生一個樣板,把他修改成我們要的。
POST http://localhost:8080/user
Content-Type: application/json
{
"name": "demo",
"age": 666
}
將request路徑跟要建立使用者的資料填入後,就可以按POST左邊的綠色執行箭頭呼叫API了!
如果程式都沒錯誤,順利的話就能看見回傳200 OK了!
跑去database並點擊user table兩下,IntelliJ會幫你產生一個查詢表,看到資料寫進去就是成功囉!!
查詢使用者
要做GET User API一樣也是透過內建的Http Request工具,這次選GET Request,產生的樣板再改成我們要呼叫的樣子
GET http://localhost:8080/user?id=1
Accept: application/json
最後因為我們前一步已經先建立好使用者,所以資料庫理當會找得到id=1的使用者回傳!
當然如果你問如果資料庫沒有資料的話,當然會需要例外處理了!這裡就交給大家自行去練習,或是不想用id去找資料,也能嘗試去改寫用name尋找關鍵字來找到你想找到的使用者。
補充
有人一定覺得很奇怪,(是我覺得很奇怪拉…都沒這樣寫過),為什麼UserController.kt裡頭的val userService: UserService沒有new物件給它,就能直接使用?! 還有RealUserService.kt裡頭的val userRepository: UserRepository也一樣沒有new物件啊!!
這就是Spring神奇的地方,已經在背後幫你Component Scan,把package實作類別加到Spring的生命週期裡。但是過往做法都還會在物件前面還會加個@Autowired ,Spring才真的會去幫忙自動配置,可能目前版本環境已經默認有這些功能了嗎?(待高手解釋…)。
但是如果實作的類別超過一個後,就必須真的要加入@Qualifier來指定,因為Spring不知道你指向的是哪一個,標示錯誤給你看。
/service/impl/DummyUserServiceImpl.kt
直接加一個測試用的DummyUserService,然後在UserController就會直接噴錯給你看
所以要在UserController.kt多加上@Qualifier(“RealUserService”)
然後RealUserServiceImpl的@Service(“RealUserService”)也要加上同樣的value,這樣就能解決錯誤了。
總結
這次的練功場又是收穫滿滿,知識量在短短一小時半已經爆炸,所以筆記上有錯誤歡迎指正出來,一起互相學習。
練功場還有很多我沒寫進來的,像是有講一些Kotlin的Scope function怎麼運用在RealUserService那邊,讓程式比起以前用Java寫起來更簡潔易懂,這些我想在後續的Kotlin讀書會應該都會再學到,也不特別贅述囉。
當天的練功場即時筆記有滿多補充的資料,大家也能去官網找到連結去參考
以上就是練功場的第二堂體驗,教你用Spring Boot來實現Basic-3-Layer!
歡迎各位也一起來練功場學習Spring囉!
2020–08–2~2020–09–27相約在線上! 隔週一起來練功!