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

slick3を使ってみた

scala slick

scalaで使われて使われているdbフレームワークであるslickを使ってみた。
slickも3系になってReactive Slickeと呼ばれるような機能変更があったようで
非同期周りの処理が豊富になったのだとは思いますが、初心者に取ってはミスの原因になりそう
なので、使うならしっかりと調べたいところです。
色々試したことのメモを残しておきたいと思います。

事前準備
slickを使用できるようbuild.sbのlibraryDependenciesに以下を追加

  "com.typesafe.slick" %% "slick" % "3.1.0-RC2",

DBにpostgresqlを使用したので、以下も追加しておきます。

  "postgresql" % "postgresql" % "9.1-901.jdbc4",

dbの接続先情報をapplication.confに記述します。

DB_CONFIG_NAME = {
  url = "jdbc:postgresql://localhost:5432/db_name?user=db_user_name&password=db_user_pass”
  driver = org.postgresql.Driver
  connectionPool = disabled
  keepAliveConnection = true
}

接続先設定を記述することによりソースコード中では以下のようにしてDBにクエリを投げれるようになります。

val db = Database.forConfig(“DB_CONFIG_NAME”)

テーブル準備
テーブル定義用のmodelクラスを準備します。
postgresqlのテーブル定義が以下のようになっている場合

CREATE TABLE event
(
  id integer NOT NULL DEFAULT nextval('event_id_seq'::regclass),
  event_id character varying(100) NOT NULL,
  event_nm character varying(100) NOT NULL,
  CONSTRAINT event_pkey PRIMARY KEY (id)
)

scala上でのmodelクラスは以下のようになります。

import slick.driver.PostgresDriver.api._
import slick.lifted.{ProvenShape, ForeignKeyQuery}

//idはフレームワークがわでauto incrementさせたく、初期化時はid=NoneにするのでOptionにしています
case class MyEvent(id: Option[Int], event_id: String, event_nm : String)

class MyEvents(tag: Tag)
  extends Table[MyEvent](tag, "event"){

  def id: Rep[Int] = column[Int]("id", O.PrimaryKey, O.AutoInc)
  def event_id: Rep[String] = column[String]("event_id")
  def event_nm: Rep[String] = column[String]("event_nm")

  def * = (id.?, event_id, event_nm) <> (MyEvent.tupled, MyEvent.unapply)
}

それではslickを使用してDBにアクセスします。

1.直接SQLを実行する場合、

import slick.driver.PostgresDriver.api._

import scala.concurrent.{Future, Await}
import scala.concurrent.duration.Duration

object MyHello extends App {

  val db = Database.forConfig(“TABLE_CONFIG_NAME”)
  //sqlを直接実行する場合
  try {
    //val myEvent = TableQuery[MyEvent]

    //インサート
    val event_id = "abc"
    val event_nm = "def"
    val queryi = sqlu"INSERT INTO event ( event_id, event_nm) VALUES ( ${event_id}, ${event_nm})"
    var g = db.run(queryi)
    Await.result(g, Duration.Inf) //Duration.Infよりsqlの実行終了まで待つ

    //セレクト
    println("sample sql select")
    val query = sql"SELECT id, event_id, event_nm FROM event where id < 10".as[(Int, String, String)]
    val f = db.run(query)
    Await.result(f, Duration.Inf) foreach println

  } finally db.close
}

slickではこのようにSQLのクエリを直接書いて投げることができます。insert文とselect文でクエリの生成方法がsqlu”インサート文”,
sql”セレクト文 “で異なっているので注意が必要です。
また、slickとは別になりますが、postgresqlでserial型を使わずにauto incrementするのでしたら、事前にsequenceを用意して
おいてカラムのデフォルト値をそれにしておきます。
id integer NOT NULL DEFAULT nextval('event_id_seq'::regclass)


2.テーブル定義クラス、modelクラスを使ってDBにアクセス

val db2 = Database.forConfig(“DB_CONFIG_NAME”)
  //モデルを使ってDBにアクセスする
  try{

    /** TableQuery */
    object MyEvents extends TableQuery(new MyEvents(_))
    object UserEvents extends TableQuery(new UserEvents(_))


    //通常のインサート
    var ins_test1 = MyEvent(None, "abc", "def")
    val insert1 = db2.run(MyEvents += ins_test1)
    val intertResult1 = Await.result(insert1, Duration.Inf)
    println(intertResult1)

    //インサート時にインクリメントした値の使用
    val ins_item = MyEvent(None, "seq_id", "seq_nm")
    val seq_ins = db2.run(MyEvents returning MyEvents.map(_.id) += ins_item)
    val ins_res = Await.result(seq_ins, Duration("10s"))
    //ins_resにインクリメントした値が入るのでこれを使って別のテーブルにもインサートします。
    //トランザクションが同じになっていないはずなので対策が必要
    val user_ev_ins = db2.run(UserEvents += UserEvent(1, ins_res))
    Await.result(user_ev_ins, Duration("10s"))


    //複数行インサート
    var ins_test2 = MyEvent(None, "modelInsert", "abzd")
    val insert2 = db2.run(MyEvents ++= Seq(MyEvent(None,"aha", "name2"), MyEvent(None,"aaa", "name3"), ins_test2))
    val intertResult2 = Await.result(insert2, Duration("10s"))
    println(intertResult2)


    // 結果確認(id=1だけ)
    val select1 = db2.run(MyEvents.filter { _.id === 1 }.result.head)
    val selectResult1 = Await.result(select1, Duration("10s"))
    println("where id=1")
    println(selectResult1)

    // 結果確認(id in (1, 2, 3))
    val select2 = db2.run(MyEvents.filter { _.id.inSet(List(1, 2, 3)) }.result)
    val selectResult2 = Await.result(select2, Duration("10s"))
    println("filter sample2")
    println(selectResult2)

    // ソートする場合のサンプル結果確認
    println("sort sample")
    val select3:Future[Seq[MyEvent]] = db2.run(MyEvents.filter{_.id < 5 }.sortBy(_.id.desc).result)
    Await.result(select3, Duration("10s")) foreach println

    // groupbyする場合のサンプル
    println("group by sample")
    val select4 = db2.run(MyEvents.groupBy(_.event_nm).map{case (event_nm, group) => (event_nm, group.map(_.id).max)}.result)
    Await.result(select4, Duration("10s")) foreach println

    // joinする場合のサンプル
    println("join test")
    val joinq = (for(e <- MyEvents;
                     u <- UserEvents if e.id === u.event_id && u.user_id === 1L
    ) yield (u.user_id,e.event_nm)).result
    val select = db2.run(joinq)
    Await.result(select, Duration("10s")) foreach println

  } finally db2.close()

テーブル定期クラスを使用してDBにアクセスする場合、以下のようにテーブルクエリのオブジェクトを準備します。
object MyEvents extends TableQuery(new MyEvents(_))

インサートは以下のようにして行います。インサート用のインスタンスを生成したあと db.runでインサートのアクションを実行します(この時点ではまだコミットは
していません。)それから、Await.result(insert1, Duration.Inf)の部分で実行結果を受け取っています。Duration.Infは実行結果の待ち時間を表しており
Duration.Infは処理が終わるまで待つ動きになります。

var ins_test1 = MyEvent(None, "abc", "def")
val insert1 = db2.run(MyEvents += ins_test1)
val intertResult1 = Await.result(insert1, Duration.Inf)

インサート時にid等のauto incrementを行う場合、まず事前準備としてモデルクラスとテーブルクラスのidカラムの設定を以下のようにします。

case class MyEvent(id: Option[Int], event_id: String, event_nm : String)

class MyEvents(tag: Tag)
  extends Table[MyEvent](tag, "event"){
  def id: Rep[Int] = column[Int]("id", O.PrimaryKey, O.AutoInc)
 /*
  *略
  */
  def * = (id.?, event_id, event_nm) <> (MyEvent.tupled, MyEvent.unapply)
}

モデルクラスではidをオプションにし、テーブル定義切らすではO.AutoIncを指定します。
こうすることでMyEvent(None, "abc", "def”)のようにidを指定していなくても自動で採番してくれるようになります。

auto incrementした値を利用したい場合は、以下のようにdb.runするときの戻り値をMyEvents.map(_.id)でauto incrementした
idを返すようにしています。

val ins_item = MyEvent(None, "seq_id", "seq_nm")
val seq_ins = db2.run(MyEvents returning MyEvents.map(_.id) += ins_item)

複数行のインサートは ++Seqで行います。

val insert2 = db2.run(MyEvents ++= Seq(MyEvent(None,"aha", "name2"), MyEvent(None,"aaa", "name3"), ins_test2))

select文の実行は以下のようになります。whereする場合はfilterで行うことができるようです。

val select1 = db2.run(MyEvents.filter { _.id === 1 }.result.head)
val selectResult1 = Await.result(select1, Duration("10s")) foreach println

ソートやgroup byも勿論できるので、方法については公式を確認するのが一番かと思います。
以下自分が確認したものになりまうす。

// ソートする場合のサンプル結果確認
println("sort sample")
val select3:Future[Seq[MyEvent]] = db2.run(MyEvents.filter{_.id < 5 }.sortBy(_.id.desc).result)
Await.result(select3, Duration("10s")) foreach println

// groupbyする場合のサンプル
println("group by sample")
val select4 = db2.run(MyEvents.groupBy(_.event_nm).map{case (event_nm, group) => (event_nm, group.map(_.id).max)}.result)
Await.result(select4, Duration("10s")) foreach println


// joinする場合のサンプル
println("join test")
val joinq = (for(e <- MyEvents;
                u <- UserEvents if e.id === u.event_id && u.user_id === 1L
) yield (u.user_id,e.event_nm)).result
val select = db2.run(joinq)
Await.result(select, Duration("10s")) foreach println

それからまとまったトランザクションないでまとまったSQLを投げたい場合は、以下のように
DBIO.seqを使えば良いようです。auto incrementの値はどうやって使うか調査が必要そう

val db3 = Database.forConfig(“DB_CONFIG_NAME”)
  try{
    object MyEvents extends TableQuery(new MyEvents(_))
    object UserEvents extends TableQuery(new UserEvents(_))

    val insertAction: DBIO[Unit] = DBIO.seq(
      MyEvents += MyEvent(None, "kkkk", "llll"),
      MyEvents += MyEvent(None, "mmmm", "nnnn")
    )
    val setupFuture: Future[Unit] = db.run(insertAction)
}finally db3.close()