doobie による関数型 JDBC アクセス

[Scala 2019 の投票](https://blog.softwaremill.com/scalar-2019-whiteboard-voting-40b31e4f7f7)で Slick を抜いて1位になっていた、Typelevel プロジェクトの関数型 RDB/JDBC アクセスライブラリ、**doobie** について。

## doobie の特徴
Scala の RDB アクセスのための類似プロダクトには、Slick、Quill、ScalikeJDBC などがあるが、*doobie* には以下のような特徴がある。

- EDSL 系ではなく、文字列補完子で**SQLをそのまま書く系**。
- 記述[^description_of_db_access]された DBアクセスは、Cats の **Free Monad** ベースの型で合成しやすい。動的な SQL も FP的に合成可。
- Free Monad からの変換先となる *effect wrapper* は、**[Cats Effect](https://typelevel.org/cats-effect/)** の`Async[F]`となる`F[_]`なら何でもいい。典型的には、`cats.effect.IO` や `monix.eval.Task` が使われる。
- **[FS2](https://fs2.io/index.html)** の `Stream`[^streaming]がネイティブにサポートされていて、**[http4s](https://http4s.org/)** など FS2 ベースのプロダクトと相性がいい。
- Select 結果から Scalaデータへのマッピング先に、プリミティブ型、タプル、case class に加えて、**Shapeless** の `HList` や `Record` もサポートされる。[^multi_column_queries]
- REPL で使える **YOLO** というモードがあって[^yolo]、試しに動かしてみたり、実DBのメタデータを突き合わせて不整合を見つけたりするのに便利。
- 現時点(2019/03)での最新バージョン 0.6.0 から、Cats Effect の `ContextShift` を活用した**非同期API**がサポートされている。[^async]
- JDBC を純粋関数型的に操作する low-level API と、プログラムを記述する **high-level API** があるが、通常はもっぱら後者を使う。
- Slick のようなテーブル定義をクラスで表現する仕組みや、Quill のような convention ベースのマッピングなどは特になく、Scala 的に**タイプセーフというわけではない**。ただし実DBから得たメタデータとクエリを照合して不整合を検出する仕組みが手厚い（[後述](https://qiita.com/drafts/83bd11124042c58b850d#%E3%82%AF%E3%82%A8%E3%83%AA%E3%81%AE%E6%A4%9C%E8%A8%BC)）。

以下、サンプルコード。
※ doobie のバージョンは 0.6.0。その他のライブラリは[build.sbt](https://gist.github.com/yasuabe/ef813ad5b2ca5b6a377b1a2b11869d4a)参照。

[^description_of_db_access]: 純粋関数型なので、もちろん「記述」と「実行」はきれいに分離される。
[^multi_column_queries]: [doobie book: Selecting Data: Multi-Column Queries](https://tpolecat.github.io/doobie/docs/04-Selecting.html#multi-column-queries)

## 簡単なサンプル
簡単なサンプルは下のようなものになる。動かすには "world" データベースの事前セットアップ[^to_run]が必要。

```scala
case class Country(code: String, name: String, population: Long)

object SimpleSampleMain extends TaskApp {
  def xa[F[_]: Async: ContextShift]: Transactor[F] = Transactor.fromDriverManager[F](
    "org.postgresql.Driver", "jdbc:postgresql:world", "postgres", "pass"
  )
  def find(n: String): ConnectionIO[Option[Country]] =
    sql"select code, name, population from country where name = $n".query[Country].option

  def run(args: List[String]): Task[ExitCode] = for {
    line <- find("France").transact(xa[Task]) // ここで ConnectionIO[_] から Task[_] に変換
    _    <- Task { println(line) }
  } yield ExitCode.Success
}
```
ここでは *effect* に `Task` を採用した。また、簡単のため、DriverManager を使って`Transactor` を得ているが、HikariCP との連携や[^hikari_cp]、既存 DataSource を利用する方式[^existing_datasource]もある。

[ソース Gist](https://gist.github.com/yasuabe/b9382687a9ea70b407431833e95d0de8)

## http4s との連携
上述の通り、doobie の中では FS2 の `Stream` を使っているので、http4s などとも相性がいい。

```scala
case class City(id: Int, name: String, district: String)

class MyService[F[_]: Async: ContextShift] extends Http4sDsl[F] {
  def xa: Transactor[F] = Transactor.fromDriverManager[F](
    "org.postgresql.Driver", "jdbc:postgresql:world", "postgres", "pass"
  )
  def findCitiesByCountry(c: String): doobie.Query0[City] =
    sql"select id, name3, district from city where countrycode = $c".query[City]

  object CountryCodeMatcher extends QueryParamDecoderMatcher[String]("countryCode")

  def app: HttpApp[F] = HttpRoutes.of[F] {
    case GET -> Root / "cities" :? CountryCodeMatcher(code) =>
      Ok(findCitiesByCountry(code).stream.map(_.asJson).transact(xa))
  }.orNotFound
}
```
`MyService`では、まだ特定の *effect* に束縛していない。たとえば `IO` に決めるとすると、実行するサーバは以下のように書ける。

```scala
object ServerMain extends IOApp {
  def run(args: List[String]): IO[ExitCode] =
    BlazeServerBuilder[IO]
      .bindHttp(8080, "localhost")
      .withHttpApp(new MyService[IO].app)
      .serve
      .compile.drain
      .as(ExitCode.Success)
}
```
下記コマンドを実行すると、world データベース内の 248 の日本の街が返される。

```
$ curl 'http://localhost:8080/cities?countryCode=JPN'
```
※ 簡単のため、Json 化された `City` が個別に連続で流れてくる感じにしたが、本当は JsonArray にしたほうがいい[^json_array]。

[ソース Gist](https://gist.github.com/yasuabe/f84cf8e5637bc2df79326299a921b2b7)

## クエリの検証
アーキテクチャの外縁層で JDBC にアクセスするコードは、Scala プログラムとしてのコンパイル時の型安全もさることながら、実DBのスキーマと合ってるかがより重要になる。

特に、スキーママイグレーションの頻度が多い状況では、新しいマイグレーションDDLが既存のクエリを壊していないことや、新規のクエリが最新スキーマに同調していることを、Scala コード上だけではなくDBと実際に突き合わせて、簡単かつ継続的に検証したい[^other_level]。例えば以下のような観点がある。

- コード上のクエリ文字列とDBスキーマのテーブル名、カラム名などが合っているか
- コード上の型とスキーマ上の型が合っているか（NULL可否などを含む）
- クエリの結果データと変換先の Scala データの型、構造が合っているか

doobie では DBメタデータを使って、これらをチェックする仕組みが提供される。場合によっては、修正案まで含めてかなり親切に指摘してくれる。

例えば、上の `MyService` のクエリ `findCitiesByCountry` は以下のようにテストできる。[^test]

```scala
object MyServiceQuerySpec extends Specification with IOChecker {
  import scala.concurrent.ExecutionContext.Implicits.global
  implicit val cs: ContextShift[IO] = IO.contextShift(global)

  val sut = new MyService[IO]
  def transactor: doobie.Transactor[IO] = sut.xa //実DBを見に行くので必要

  check(sut.findCitiesByCountry("DUMMY"))
  // 他にもクエリがあれば、以下、同様に並べて列挙すればいい
}
```
ここで、もし例えば `district` の型を誤って `Double` にしていると、テストが失敗して以下のようなメッセージが出る。

```
VARCHAR (varchar) is ostensibly coercible to Double according to the
JDBC specification but is not a recommended target type. Expected
schema type was FLOAT or DOUBLE.
```
ちなみに REPL 上の YOLO モードで確認できるので、新規開発中にも活用できると思う。

![Screenshot from 2019-03-27 07-07-40.png](https://qiita-image-store.s3.amazonaws.com/0/9633/67f5dadb-aa66-eb32-1e94-7391298b40b8.png)

[ソース Gist](https://gist.github.com/yasuabe/e3f176f4329fee46d43984e5a2c84915)

## 補足
#### Free Monad の扱いについて
JDBCプログラミング経験者にはおなじみの `Connection`、`Statement` や `Record` といった各種オブジェクトについて、それぞれのメソッドに対応する Free Monad 代数が、サフィックス `〜Op` をつけた命名で定義される。例えば Connection なら、代数 `ConnectionOp` とその要素、`Commit`、`Rollback`、`Close` などが定義される。

また、各々の代数を `cats.free.Free` にしたものが、サフィックス`〜IO`をつけた形で提供される。例えば、Connection に対しては、

```scala
type ConnectionIO[A] = Free[ConnectionOp, A]` 
```
として定義される。他の JDBC オブジェクトについても同様。

インタープリターも doobie で提供されるので、特に詳しく知らなくても普通に使えるようにはなっているが、観察すると Free Monad の活用例として参考になる。

#### DDL、insert、update 等について
SELECT系と同様に、文字列補完子を使って`ConnectionIO[_]`型の DBアクセスが記述できる。これもシンプルに書ける[^updates]。

#### logging について
クエリ定義コードで、`query` メソッドの代わりに、`queryWithLogHandler` に `LogHandler` を渡すと、実際に発行されたクエリと実行時間が標準出力（デフォルト）にログ出力される。[^logging]

## 所感
関数型な Scala 開発での RDBアクセス[^relational_model]の選択肢としてかなり有力ではなかろうか。特に Cats Effect と FS2 に慣れていたり、すでに導入していたりする開発現場なら、これが一番シンプルかつ自然な気がする。

## 参考サイト
- [doobie official: A functional JDBC layer for Scala](https://tpolecat.github.io/doobie/)
  - [book of doobie](https://tpolecat.github.io/doobie/docs/01-Introduction.html)
- [Typelevel: Libraries](https://typelevel.org/projects/)
- [SoftwareMill Blog: Comparing Scala relational database access libraries](https://softwaremill.com/comparing-scala-relational-database-access-libraries/)

[^json_array]: [Gitter: Luis Miguel Mejía Suárez@BalmungSan (Aug 18 2018 01:39)](https://gitter.im/http4s/http4s?at=5b76fa2ca158fd0ea370755b)
[^hikari_cp]: [Using a HikariCP Connection Pool](https://tpolecat.github.io/doobie/docs/14-Managing-Connections.html#using-a-hikaricp-connection-pool)
[^existing_datasource]: [Using an existing DataSource](https://tpolecat.github.io/doobie/docs/14-Managing-Connections.html#using-an-existing-datasource)
[^to_run]: 動かす場合、[この手順](https://tpolecat.github.io/doobie/docs/01-Introduction.html#sample-database-setup) を参考にして、適当に world データベースをセットアップ。自分は ローカルの Postgres Docker 上に作成した。
[^async]: [book of doobie: Managing Connections: About Threading](https://tpolecat.github.io/doobie/docs/14-Managing-Connections.html#about-threading)
[^streaming]: [book of doobie: Selecting Data: Internal Streaming](https://tpolecat.github.io/doobie/docs/04-Selecting.html#internal-streaming)
[^updates]: https://tpolecat.github.io/doobie/docs/07-Updating.html
[^yolo]: [book of doobie: Selecting Data: YOLO Mode](https://tpolecat.github.io/doobie/docs/04-Selecting.html#yolo-mode)
[^logging]: [book of doobie: Logging](https://tpolecat.github.io/doobie/docs/10-Logging.html)
[^test]: Specs2 と ScalaTest 用の仕組みが提供される。[参照](https://tpolecat.github.io/doobie/docs/13-Unit-Testing.html)
[^other_level]: 当然、UnitTest とは別のレベルのテストとして扱う必要がある。
[^relational_model]: 当然ながら永続化層のモデルとして、そもそも NoSQL ではなく本当に関係モデル／RDB が必要なのかという根本的な設計判断は不可欠。