LoginSignup
20
11

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-03-26

Scala 2019 の投票で Slick を抜いて1位になっていた、Typelevel プロジェクトの関数型 RDB/JDBC アクセスライブラリ、doobie について。

doobie の特徴

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

  • EDSL 系ではなく、文字列補完子でSQLをそのまま書く系
  • 記述1された DBアクセスは、Cats の Free Monad ベースの型で合成しやすい。動的な SQL も FP的に合成可。
  • Free Monad からの変換先となる effect wrapper は、Cats EffectAsync[F]となるF[_]なら何でもいい。典型的には、cats.effect.IOmonix.eval.Task が使われる。
  • FS2Stream2がネイティブにサポートされていて、http4s など FS2 ベースのプロダクトと相性がいい。
  • Select 結果から Scalaデータへのマッピング先に、プリミティブ型、タプル、case class に加えて、ShapelessHListRecord もサポートされる。3
  • REPL で使える YOLO というモードがあって4、試しに動かしてみたり、実DBのメタデータを突き合わせて不整合を見つけたりするのに便利。
  • 現時点(2019/03)での最新バージョン 0.6.0 から、Cats Effect の ContextShift を活用した非同期APIがサポートされている。5
  • JDBC を純粋関数型的に操作する low-level API と、プログラムを記述する high-level API があるが、通常はもっぱら後者を使う。
  • Slick のようなテーブル定義をクラスで表現する仕組みや、Quill のような convention ベースのマッピングなどは特になく、Scala 的にタイプセーフというわけではない。ただし実DBから得たメタデータとクエリを照合して不整合を検出する仕組みが手厚い(後述)。

以下、サンプルコード。
※ doobie のバージョンは 0.6.0。その他のライブラリはbuild.sbt参照。

簡単なサンプル

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

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
}

ここでは effectTask を採用した。また、簡単のため、DriverManager を使ってTransactor を得ているが、HikariCP との連携や7、既存 DataSource を利用する方式8もある。

ソース Gist

http4s との連携

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

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 に決めるとすると、実行するサーバは以下のように書ける。

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 にしたほうがいい9

ソース Gist

クエリの検証

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

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

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

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

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

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

ソース Gist

補足

Free Monad の扱いについて

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

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

type ConnectionIO[A] = Free[ConnectionOp, A]` 

として定義される。他の JDBC オブジェクトについても同様。

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

DDL、insert、update 等について

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

logging について

クエリ定義コードで、query メソッドの代わりに、queryWithLogHandlerLogHandler を渡すと、実際に発行されたクエリと実行時間が標準出力(デフォルト)にログ出力される。13

所感

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

参考サイト


  1. 純粋関数型なので、もちろん「記述」と「実行」はきれいに分離される。 

  2. book of doobie: Selecting Data: Internal Streaming 

  3. doobie book: Selecting Data: Multi-Column Queries 

  4. book of doobie: Selecting Data: YOLO Mode 

  5. book of doobie: Managing Connections: About Threading 

  6. 動かす場合、この手順 を参考にして、適当に world データベースをセットアップ。自分は ローカルの Postgres Docker 上に作成した。 

  7. Using a HikariCP Connection Pool 

  8. Using an existing DataSource 

  9. Gitter: Luis Miguel Mejía Suárez@BalmungSan (Aug 18 2018 01:39) 

  10. 当然、UnitTest とは別のレベルのテストとして扱う必要がある。 

  11. Specs2 と ScalaTest 用の仕組みが提供される。参照 

  12. https://tpolecat.github.io/doobie/docs/07-Updating.html 

  13. book of doobie: Logging 

  14. 当然ながら永続化層のモデルとして、そもそも NoSQL ではなく本当に関係モデル/RDB が必要なのかという根本的な設計判断は不可欠。 

20
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
11