Scala用のタイプセーフなSQLビルダtranquil 0.0.3をリリースしました

github.com

以前から少しずつ進めていたtranquilですが、APIもそれなりに落ち着いてきたので久しぶりにバージョンアップしてみました。ちなみに以前のものはこんな感じでした。

takezoe.hatenablog.com

テーブル定義は以下のような感じで随分シンプルに定義できるようになりました。

import com.github.takezoe.tranquil._

case class User(userId: String, userName: String, companyId: Option[Int])

class Users extends TableDef[User]("USERS") {
  val userId    = new Column[String](this, "USER_ID"),
  val userName  = new Column[String](this, "USER_NAME"),
  val companyId = new OptionalColumn[Int](this, "COMPANY_ID")

  override def toModel(rs: ResultSet): User =
    User(userId.get(rs), userName.get(rs), companyId.get(rs))
}

object Users {
  def apply() = new SingleTableAction(new Users())
}

case class Company(companyId: Int, companyName: String)

class Companies extends TableDef[Company]("COMPANIES") {
  val companyId   = new Column[Int](this, "COMPANY_ID"),
  val companyName = new Column[String](this, "COMPANY_NAME")

  override def toModel(rs: ResultSet): Company =
    Company(companyId.get(rs), companyName.get(rs))
}

object Companies {
  def apply() = new SingleTableAction(new Companies())
}

マクロで自動生成する手法も試してみたのですが、IntelliJで補完が効かなかったりなど厳しい感じだったので現状ではソースコードの自動生成で対応する感じになるかなと思います。ただ、自動生成するとはいってもシンプルにできるのであればそのほうがよいので、そういう意味ではResultSetからケースクラスを生成するメソッドや、コンパニオンオブジェクトでファクトリメソッドを定義しないといけないのがまだちょっと微妙ですね。

クエリはこんな感じです。

import com.github.takezoe.tranquil._
import com.github.takezoe.tranquil.Dialect.generic

val conn: java.sql.Connection = ...

// SELECT
val users: Seq[(User, Option[Company])] =
  Users()
    .leftJoin(Companies()){ case u ~ c => u.companyId eq c.companyId }
    .filter { case u ~ c => (u.userId eq "takezoe") || (u.userId eq "takezoen") }
    .sortBy { case u ~ c => u.userId asc }
    .list(conn)

テーブルのエイリアスを手動で指定する必要がなくなりました。が、既存のモデルを壊さずにエイリアスを自動生成できるようにしたので実装が割と微妙な感じになってしまいました。やはりDSLではASTを組み立てるだけにして、SQLに変換するフェーズとは分けるようにしたほうが良さそうです(なるべくシンプルな作りにしたかったのですが、こうやって複雑になっていくのか…)。

この他にも以前ブログに書いた時点ではまだサポートしていなかった以下のような機能が実装済みです。

  • 特定のカラムだけSELECTする
  • GROUP BY、HAVING
  • ページング(LIMIT、OFFSET的なやつ)
  • DBごとの違いを吸収するためのDialect

ただ、最初にあまり深く考えずに作り始めてしまったこともあり、無理やり辻褄をあわせるためにvarを使っていたり、ランタイムリフレクションを使っていたりと作り的に結構微妙な感じになってしまいました。また、見た目に違和感のないDSLを作ろうとするとどうしてもimplicit conversionを多用することになってしまい、コードがわかりにくくなってしまいます。

一度作ってみて感覚が掴めたので、もう一度ちゃんと設計して作り直したほうがいいかも…。