読者です 読者をやめる 読者になる 読者になる

ScalaのDBアクセスライブラリまとめ

Slick 3.0.0がリリースされました。

SlickはこれまでScalaで利用可能な代表的なDBアクセスライブラリの1つとして利用されてきましたが、3.0.0では別名Reactive Slickと呼ばれ、モナドを駆使したFutureベースのプログラミングを前提としたものに変貌してしまいました。Reactive Slickの狙いについてはメイン開発者であるStefan Zeiger氏によるこのスライドが参考になります。僕は聴講できなかったのですが、Scala Days 2015 in San Franciscoでのセッションで使用したものでしょうか。

これまでSlickは同期的・手続き的な実行しかできなかったため、PlayやAkkaなどのノンブロッキングベースのフレームワークと組み合わせた場合に処理全体をブロックする必要があったのですが、Slick 3.0.0ではDBアクセスを含む場合でも処理全体をFutureにすることができるようになります。

というわけで、Slick 3.0.0はクエリの記述方法こそ2.xと互換性がありますが、実行モデルが根本的に異なるためこれまでSlick 2.xを使っていたアプリケーションをSlick 3.0.0に移行するには大幅な見直しが必要になると思われます。また、そもそもSlickを単純にDBにアクセスするためのライブラリとして使用していた場合、Futureベースに移行するメリットを見出せないというケースもあるのではないでしょうか。

少々前置きが長くなってしまいましたが、そんなSlick 2.xユーザの今後の移行先の候補としてScalaで利用可能なDBアクセスライブラリをまとめてみました。

ScalikeJDBC

@seratch_jaさんを中心に開発されているORMです。依存関係が少なくコンパクトで、タイプセーフなクエリビルダと、String Interpolationを活用したネイティブSQLの両方をサポートしています。インターフェースもわかりやすく、ドキュメントの比較的充実しているため、Scalaのライブラリにありがちな「どう使えばいいのかわからない」ということはまずないと思います。

以下はタイプセーフなクエリビルダ(QueryDSL)の例です。

val id = 123
val (m, g) = (GroupMember.syntax("m"), Group.syntax("g"))
val groupMember = withSQL {
  select.from(GroupMember as m).leftJoin(Group as g).on(m.groupId, g.id)
    .where.eq(m.id, id)
}.map(GroupMember(m, g)).single.apply()

以下はネイティブSQLの例です。パラメータの埋め込みだけでなく、SQLを簡単・安全に記述するための機能が提供されています。

val id = 123

val (m, g) = (GroupMember.syntax("m"), Group.syntax("g"))
val groupMember: Option[GroupMember] = sql"""
  select
    ${m.result.*}, ${g.result.*}
  from
    ${GroupMember.as(m)} left join ${Group.as(g)} on ${m.groupId} = ${g.id}
  where
    ${m.id} = ${id}
  """
  .map(GroupMember(m.resultName, g.resultName)).single.apply()

また、ScalikeJDBCをベースにしたSkinnyORMというORマッピングフレームワークや、まだアルファステージとのことですが、ScalikeJDBC-Asyncというノンブロッキング版も開発されています。

国内での採用事例も多く、プロダクションレベルで利用可能な本格的なDBアクセスライブラリとしては有力な候補と言ってよいと思います。将来的には海外での普及にも期待したいところです。

Doobie

比較的新参のライブラリで、同期的ではありますがMonadicなAPIを持ちます。

def find(n: String): ConnectionIO[Option[Country]] = 
  sql"select code, name, population from country where name = $n".query[Country].option

// And then
scala> find("France").transact(xa).unsafePerformIO

Reactive Slickと比べれば扱いやすそうに見えますが、関数型プログラミングに慣れていない場合はやや慣れが必要そうです。設計思想については開発者の方によるスライドが参考になります。

Squeryl

ScalaのDBアクセスライブラリの中では比較的古参のもので、タイプセーフなクエリビルダが特徴です。多少慣れが必要かもしれませんが、さほど難しいものではありません。

val q =
  join(artists, songs.leftOuter)((a,s)=>
    groupBy(a.id, a.firstName)
    compute(countDistinct(s.map(_.id)))
    on(a.id === s.map(_.authorId))
  )

ネイティブSQLがサポートされていないようなので、複雑なSQLが必要になることが最初から想定されるケースでは厳しそうです。また、最近は開発もそれほど活発ではなく、最後のリリースは2014年1月となっています。将来性という意味ではちょっと厳しいかもしれません。

SORM

こちらもScalaのDBアクセスライブラリとしては古参のものです。

ケースクラスのネストでリレーションを表現できるなど一般的なORマッピングフレームワークに近い印象です。なるべく簡潔なコードでDBアクセスを行うことに主眼が置かれており、タイプセーフであることをそこまで重視しているわけではないようです。Scalaの特徴を活かすという意味ではやや物足りなさがあります。

val artists
  = Db.query[Artist]
      .whereNotContains("genre", pop)
      .limit(3)
      .orderBy("name")
      .whereNotContains("genre", rock)
      .fetch()

一応ネイティブSQLもサポートされていますが、以下のようにString Interpolationにも対応していないおまけ程度のものなので、ネイティブSQLを書かなければならないケースが多い場合は避けたほうがよいかもしれません。

Db.fetchWithSql[Artist]("SELECT id FROM artist WHERE name=? || name=?", "Beatles", "The Beatles")

scala-activerecord

名前の通り、Scala版のActiveRecordといった位置付けのライブラリです。もともとはSquerylをフォークしてラップしたものだそうですが、これも比較的歴史の長いものです。

Person.findBy("name", "person1")
Person.findBy("age", 55)
Person.findAllBy("age", 18).toList
Person.where(_.age.~ >= 20).orderBy(_.age desc).toList

こんか感じで簡単なクエリであればとてもシンプルに記述することができます。エンティティのプロパティにアノテーションをつけておくとバリデーションもできるようです。このあたりの機能はActiveRecordっぽいですね。

case class User(@Required name: String) extends ActiveRecord

反面、複雑なSQLが必要になるケースは別のライブラリを使用することが推奨されているなど割り切ったライブラリになっています。現在も開発が継続されており、今年3月にリリースされた最新版ではJSONシリアライズ・デシリアライズ、groupByやhavingのサポートなどが追加されているようです。

MyBatis Scala

Javaで有名なMyBatisにScala用のインターフェースを被せたもの。MyBatis自体がマッピングSQLなどをXMLで記述するというフレームワークなのですが、MyBatis ScalaではXMLリテラルを使用できたり、マッピングScalaコードで定義するためのDSLが用意されていたりします。

val findAll = new SelectListBy[String,Person] {
    def xsql =
      <xsql>
        <bind name="pattern" value="'%' + _parameter + '%'" />
        SELECT
          id_ as id,
          first_name_ as firstName,
          last_name_ as lastName
        FROM
          person
        WHERE
          first_name_ LIKE #{{pattern}}
      </xsql>
  }

開発もさほど活発とは言えませんし、ややScalaのカルチャーには馴染まない部分が目立つのも事実ではありますが、MyBatisはJavaでの実績がありますし、

  • JavaでMyBatisを使っていた開発者が多い
  • MyBatisを使っている既存のJavaシステムをScalaに移行する

といったケースでは強力なツールになるかもしれません。

Anorm

Play2で標準のDBアクセスライブラリとして採用されているものですが、単独でも使用できるようになっています。SQLを直接書き、値の取り出しも手動で行う必要があります。

val lang = "French"
val population = 10000000
val margin = 500000

val code: String = SQL"""
  select * from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where l.Language = $lang and c.Population >= ${population - margin}
    order by c.Population desc limit 1"""
  .as(SqlParser.str("Country.code").single)

値の取り出しに関してはParser APIというAPIを駆使する必要があります。ここが少しわかりにくい部分かもしれません。

case class SpokenLanguages(country:String, languages:Seq[String])

def spokenLanguages(countryCode: String): Option[SpokenLanguages] = {
  val languages: List[(String, String)] = SQL(
    """
      select c.name, l.language from Country c 
      join CountryLanguage l on l.CountryCode = c.Code 
      where c.code = {code};
    """
  )
  .on("code" -> countryCode)
  .as(str("name") ~ str("language") map(flatten) *)

  languages.headOption.map { f =>
    SpokenLanguages(f._1, languages.map(_._2))
  }
}

全部手で書くので柔軟性が高いとも言えますが、大規模なアプリケーションをすべてこれで作るのは少々骨が折れそうな気がします。簡単なツールをさくっと作りたいというケースであればありかもしれません。

また、上記以外に新しめのライブラリとしてRelateというものがあります。SQLを書いて値の取り出しも手動で行うというものですが、難しいことを何も考えなくても使えるので、簡単な用途であれば便利そうです。Anormを使うくらいならこちらのほうがいいかも…。

val ids = Seq(1, 2, 3)
sql"SELECT email FROM users WHERE id in ($ids)".asMap { row =>
  row.long("id") -> row.string("email")
}

val id = 4
val email = "github@lucidchart.com"
sql"INSERT INTO users VALUES ($id, $email)".execute()

Slickが異世界に旅立ってしまったので、ScalaのDBアクセス業界に再び戦国時代がやってくるもしれません。