Slick(旧Scala Query)を使ったScalaによるDBアクセス

2012.10.24

Scala QueryとSlick

ScalaQueryとは、ScalaでRDBを扱うための低レベルなライブラリです。 RDBのデータをコレクションのように扱うことができ、シンプルで自然な記述でDBへのアクセスができるので人気があります。

今回使用するSlickとは、このScalaQueryの後継として開発されているライブラリです。 ScalaQueryを汎用化し、RDB以外にもさまざまなデータソースを扱えるようにするのことを目標に開発されています。

環境構築

今回使用した動作環境は以下のとおりです。

  • OS : MacOS X 10.7.4
  • sbt : 0.12
  • Scala : 2.10.0 M7
  • Slick : 0.11.1

なお、プロジェクトのテンプレート作成のためgiter8を使用するので、Homebrewでインストールしておいてください。 ※別にインストールしなくてもsbtのプロジェクトを手動でつくれば問題ないです

% brew install giter8

プロジェクトのセットアップ

ではまずsbtプロジェクトを作成しましょう。giter8を使用してsbtプロジェクトを作成します。 コンソールでいくつか質問されますが、とりあえず全部エンターキーを押します。

% g8 typesafehub/scala-sbt

scala-projectという名前でディレクトリが作成されたはずです。

次にsbtで依存ライブラリのインストールをしましょう。 scala-project/project/ScalaProjectBuild.scalaを下記のように編集します。 今回はDBにMySQLを使用するので依存ライブラリとしてslickとmysqlのドライバを追加します。 また、slickはScala 2.10でないと動作しないので、scalaVersionに2.10.0-M7を指定します。

import sbt._
import sbt.Keys._

object ScalaProjectBuild extends Build {

  lazy val scalaProject = Project(
    id = "scala-project",
    base = file("."),
    settings = Project.defaultSettings ++ Seq(
      name := "Scala Project",
      organization := "org.example",
      version := "0.1-SNAPSHOT",
      scalaVersion := "2.10.0-M7", //Scalaは2.10を使用
      // add other settings here
      libraryDependencies += "com.typesafe" % "slick_2.10.0-M7" % "0.11.1", //slick
      libraryDependencies += "mysql" % "mysql-connector-java" % "5.1.18" % "runtime" //mysql      
    )
  )
}

sbtコンソール上で依存ライブラリをダウンロードし、リロードしましょう。

% cd scala-project
% sbt
> update
> reload

最後にデータベースを作成しておきます。MySQLでデータベースを「mytest」という名前で作成しておきます。 これで準備ができました。

Slickを使ってみる

ではソースファイルを編集してslickを使ってみましょう。 すでにsrc/main/scala/org/example/ScalaProject.scalaファイルがあるので、このファイルを編集します。

必要なクラスのimport

まずは必要なクラスをimportしましょう。

import scala.slick.session.Database
import Database.threadLocalSession
import scala.slick.jdbc.{GetResult, StaticQuery => Q}
import Q.interpolation
import scala.slick.driver.MySQLDriver.simple._

Tableオブジェクトの作成

Appトレイトを継承して実行できるようにします。 次にTableを継承し、UserテーブルとCompanyテーブル用オブジェクトを作成します。 これらは実際のMySQLのテーブルと対応しています。(テーブルは後で作成)

object Main extends App {

  object User extends Table[(Int, Int,String, String, String)]("User") {
    def id = column[Int]("id", O.PrimaryKey) // This is the primary key column
    def companyId = column[Int]("companyId")
    def name = column[String]("name")
    def email = column[String]("email")
    def password = column[String]("password")
    // Every table needs a * projection with the same type as the table's type parameter
    def * = id ~ companyId ~ name ~ email ~ password
    def company = foreignKey("COMPANY_FK", companyId, Company)(_.id)
  }


  object Company extends Table[(Int,String)]("Company") {
    def id = column[Int]("id", O.PrimaryKey)
    def name = column[String]("name")
    def * = id ~ name
  }

プライマリキーや外部キーも定義可能です。 また、「」を定義しておくことで、任意のカラムを指定した状態を「<Tableオブジェクト>.」のように使うことができます。 (上記Companyの場合、idとnameのTupleとして扱える)

DBへの接続とテーブルの作成

次にDatabase.forURLで接続先を指定し、withSessionを呼びます。このブロック内でDBへの操作を行います。 このあたりはScalaQueryと同じです。

Database.forURL("jdbc:mysql://localhost/mytest", driver = "com.mysql.jdbc.Driver",
    user="<ユーザー名>",password="<パスワード>") withSession {
    try {
      (Company.ddl ++ User.ddl).drop
    } catch {
      case _ => println("table not found")
    }
    (Company.ddl ++ User.ddl).create

また、Tableオブジェクトの持つddlトレイトに対してcreateやdropをすることで、DBのテーブルを作成したり削除することができます。 ここではCompanyテーブルとUserテーブルがなければ削除し、テーブルを作成しています。

テストデータの登録

データの登録はinsert関数かinsertAllでおこなうことができます。

Company.insert(100,"cm")
    Company.insert(200,"annotation")

    User.insert(1, 100,"taro","taro@cm.com", "pass1")
    User.insert(2, 100,"mike","mike@cm.com", "pass2")
    User.insert(3, 100,"hanako","hanako@cm.com", "pass3")

    User.insertAll(
      (4,200,"takeshi","takeshi@cm.com","pass4"),
      (5,200,"jonny","jonny@cm.com","pass5")
    )

データの検索

ではUserを検索してみましょう。QueryにTableオブジェクトを渡してUserを全件取得しています。 また、for文とifガードを用いてSQLでテーブル同士を連結するように扱うことも可能です。

println("Users:")
    Query(User) foreach { case (id,companyId,name,email,pass) =>
      println("  " + id + ":" + companyId + ":" + name + ":" + email + ":" + pass)
    }

    println("Manual join:")
    val q2 = for {
      u <- User if u.id > 1
      c <- Company if c.id === u.companyId
    } yield (u.name, c.name)
    for(t <- q2) println("  " + t._1 + " working in  " + t._2)

データの更新と削除

Companyの名前が「cm」のものを検索し、そのレコードの名前を「new-cm」に変更しています。 for文で返しているのがc.nameだけなので、指定するのもnameだけになっています。 その後Userテーブルでidが2のデータを削除しています。whereでidを指定してdeleteメソッドを呼んでいるだけです。

println("update:")    
    val qu = for(c <- Company if c.name ===  "cm") yield c.name
    val updated1 = qu.update("new-cm")

    val q3 = for {
      u <- User if u.id > 1
      c <- Company if c.id === u.companyId
    } yield (u.name, c.name)
    for(t <- q3) println("  " + t._1 + " working in  " + t._2)

    println("delete:")
    User.where(_.id === 2).delete

    val q4 = for {
      u <- User if u.id > 1
      c <- Company if c.id === u.companyId
    } yield (u.name, c.name)
    for(t <- q4) println("  " + t._1 + " working in  " + t._2)

  }
}

プログラムの実行

ではプログラムを実行してみましょう。 sbtのコンソール上でrunコマンドを実行するとプログラムが実行されます。

> run
[info] Running org.example.Main 
・・・
table not found
Users:
  1:100:taro:taro@cm.com:pass1
  2:100:mike:mike@cm.com:pass2
  3:100:hanako:hanako@cm.com:pass3
  4:200:takeshi:takeshi@cm.com:pass4
  5:200:jonny:jonny@cm.com:pass4
Manual join:
  mike working in  cm
  hanako working in  cm
  takeshi working in  annotation
  jonny working in  annotation
update:
  mike working in  new-cm
  hanako working in  new-cm
  takeshi working in  annotation
  jonny working in  annotation
delete:
  hanako working in  new-cm
  takeshi working in  annotation
  jonny working in  annotation

なお、SQLを直接実行したい場合は下記のようにStaticQueryを使用して直接実行することもできます。

StaticQuery.queryNA[User]("select * from User") foreach { u =>
   println("  " + u.id + "\t" + u.name + "\t")
 }

まとめ

いかがでしたか?コレクションを扱うように自然にRDBのアクセスができたかと思います。 最初は少しとっつきずらいところがあるかもしれませんが、関数型言語と同じく、慣れればとても使いやすいライブラリになるとおもいます。(たぶん) また、Playframework 2.2では現在使用されているAnormとSlickを統合する予定もあるようで、ますます楽しみですね。