關連式資料庫 (RDB) 也是一種常見的 backing serivce, 在 Java 的世界中常以 JDBC 作為關連式資料庫存取的方式, 或是也會在其上作一層 ORM。所以 Quarkus 有相關的 JDBC 的操作, 也有 HIBERNATE ORM 的整合。
ORM 很像是 Java 開發者說,我的世界只能有物件,什麼資料庫的表、主鍵、外鍵我不想管而弄出來的複雜框架,有一定的學習曲線。但就大多數的狀況 ORM 只能符合很特定的情境。太複雜的查詢還是用 SQL 比較直接,太簡單又覺得何必引入。回頭想想是不是利用了 ORM 反增加了開發或是除錯的複雜度
總而言之,這篇就是要講用 JDBC,關於是否需要 ORM 有不少文章在討論就先打住
https://ionutbalosin.com/2021/12/do-we-really-need-an-orm/
Quarkus 有自帶 reactive-sql-clients, 所以可以用這個配合 awaitSuspending(),轉成 coroutines 的操作。
這裡會用前幾天的 Film 範例,把資料庫改接 PostgreSQL, 用 reactive-sql-clients
把 Mongo dependency 換成
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
先在 FilmRepository 修改 from function , 改成 資料庫的 (Row) -> Film ,有一個重點是欄位名要用小寫才會取得到。這點要注意一下。
import io.vertx.mutiny.sqlclient.Row
import tw.brandy.ironman.entity.Film
private fun from(row: Row) = Either.catch {
Film(
row.getString("title"),
row.getLong("episodeid").toInt(),
row.getString("director"),
row.getLocalDate("releasedate"),
row.getString("updater")
)
}.mapLeft { AppError.DatabaseProblem(it) }
suspend fun findAll(): Either<AppError, List<Film>> = Either.catch {
client.preparedQuery("SELECT episodeId,title,director,releaseDate,updater FROM films")
.execute().awaitSuspending() //(1)
}.mapLeft { e -> AppError.DatabaseProblem(e) }
.flatMap { list -> list.traverse(::from) //(2) }
private fun toFilmTuple(film: Film): Tuple = //(1)
Tuple.of(film.episodeID, film.title, film.director, film.releaseDate, film.updater)
suspend fun add(film: Film): Either<AppError, Film> = Either.catch {
client.preparedQuery(
""" INSERT INTO films (episodeId,title,director,releaseDate,updater)
VALUES ($1,$2,$3,$4,$5)"""
).execute(toFilmTuple(film)).awaitSuspending()
}.mapLeft { AppError.DatabaseProblem(it) }.map { film }
Tuple 為有限個元素所組成的序列,在 FP 中很常出現,這裡帶一下 Kotlin 自身只有 Pair, Triple, 再往上是建議用直接用 data class, 這個是用 Mutiny 的 tuple 是可以到6個,再往上要用 Tuple.wrap(listOf(....))
所以剩下的 delete 也可以如法炮製
計算個數的 sql 要用 count ,這裡會給一個 cnt 的別名方便抓取
suspend fun count(): Either<AppError, Long> = Either.catch {
client.preparedQuery("SELECT count(*) cnt FROM films")
.execute().awaitSuspending()
}.mapLeft { AppError.DatabaseProblem(it) }.map { it.first().getLong("cnt") }
在新增或修改後。以 id 取得資料才算成功
suspend fun save(film: Film) = filmRepository.add(film)
.flatMap { filmRepository.findByEpisodeId(film.episodeID) }
suspend fun update(film: Film) = filmRepository.update(film)
.flatMap { filmRepository.findByEpisodeId(film.episodeID) }
不同的 database reactive 要引入不同的資料庫實作,還有佔位符有些是 ? 就要照出現的順序使用
Database | Extension name | Pool class name | Placeholders |
---|---|---|---|
IBM Db2 | quarkus-reactive-db2-client |
io.vertx.mutiny.db2client.DB2Pool |
? |
MariaDB/MySQL | quarkus-reactive-mysql-client |
io.vertx.mutiny.mysqlclient.MySQLPool |
? |
Microsoft SQL Server | quarkus-reactive-mssql-client |
io.vertx.mutiny.mssqlclient.MSSQLPool |
@p1 , @p2 , etc. |
Oracle | quarkus-reactive-oracle-client |
io.vertx.mutiny.oracleclient.OraclePool |
? |
PostgreSQL | quarkus-reactive-pg-client |
io.vertx.mutiny.pgclient.PgPool |
$1 , $2 , etc. |
https://github.com/hmchangm/getting-start-QK/tree/ironman2022-d26-jdb