blog.petitviolet.net

MonadicでReactiveなSlick3.xを使う

2016-03-30

QiitaScala

Monadic で Reactive だと言われている Slick3.x を使ってみる。 本記事時点での最新はは 3.1.1 となっている。 公式のサンプルだと h2 を使っていたが、今回はローカルに立てた MySQL を使用する。

Getting Started — Slick 3.1.1 documentation

準備

事前にuserテーブルを作っておく

Create Table: CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `email` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

insert into user values (1, "alice", "alice@example.com");
insert into user values (2, "bob", "bob@example.com");

導入

build.sbt に slick を追加する。 MySQL を使用するので、その jdbc の依存関係も追加。

libraryDependencies ++= Seq(
  "com.typesafe.slick" %% "slick" % "3.1.1",
  "org.slf4j" % "slf4j-nop" % "1.7.20",
  "mysql" % "mysql-connector-java" % "5.1.35"
)

次に、resources/application.conf に MySQL の設定を記述する。 DB 名はsample-dbとしている。

mysql-local = {
  url = "jdbc:mysql://localhost/sample-db"
  driver = com.mysql.jdbc.Driver
  user = "user"
  password = "password"
  connectionPool = disabled
  keepAliveConnection = true
}

DB に接続して SELECT を実行

MySQL に接続して SQL を実行してみる。

object Main extends App {
  val db: JdbcBackend.DatabaseDef = Database.forConfig("mysql-local")
  import slick.driver.MySQLDriver.api._

  val sql = sql"select * from user".as[(Int, String, String)]
  val f = db.run(sql)

  Await.result(f, Duration.Inf) foreach println
}

これを./activator runすると

(1,alice,alice@example.com)

となって正しく select 出来た。 クエリの宣言と実行が分離された API となっている。

独自クラスに mapping する

上の例だと(Int, String, String)に select した結果を mapping したが、独自クラスに mapping してみる。 userテーブルに対応するクラスを用意する。

case class User(id: Int, name: String, email: String)

これに対応させるにはこのように書けばよい。

implicit val getUserResult = GetResult { r => User(r.<<, r.<<, r.<<) }
val f: Future[Seq[User]] = db.run(sql"select * from user".as[User])

これでUserクラスのインスタンスとして結果が得られる

User(1,alice,alice@example.com)

ここで気になるのがこの行。

implicit val getUserResult = GetResult { r => User(r.<<, r.<<, r.<<) }

このrPositionedResultという型で、カラムが定義されている順番に依存してしまっている。 カラム名で値を取ってくるにはこのようにすれば良い。

implicit val getUserResult = GetResult { r =>
  User(r.rs.getInt("id"), r.rs.getString("name"), r.rs.getString("email"))
}

テーブルにアクセスするクラスを実装する

SQL を生で書いてGetResultを使ってインスタンス化するのもいいが、 データアクセスするクラスに抽象化しておきたい。 その場合はTableTableQueryを使用してuserテーブルの mapping を実装する。

import slick.driver.MySQLDriver.api._

case class User(id: Int, name: String, email: String)

class Users(tag: Tag) extends Table[User](tag, "user") {
  def id = column[Int]("id", O.PrimaryKey)
  def name = column[String]("name")
  def email = column[String]("email")

  def * = (id, name, email) <> (User.tupled, User.unapply)
}

object Users extends TableQuery(new Users(_))

このように実装しておくとUsersに対してコレクション操作のような API を使用して DB から select が出来るようになる。 注意点として、filter==を使用する時は===を使う必要があるらしい。 同様に、!=のかわりに=!=が必要となる。

// select * from user;
db.run(Users.result)

// select * from user where name = 'alice';
db.run(Users.filter{ _.name === "alice" }.result)

// select email from user where name like 'alice%';
db.run(
  Users
    .filter { _.name.startsWith("alice") }
    .map { _.email }
    .result
)

ちなみに、実行された SQL を見るには以下のようにすれば見れる。

val query = Users.filter { _.id =!= 2 }.map { _.name }
println(query.result.statements)

これでこのように出力される。

List(select `name` from `user` where not (`id` = 2))

insert/update/delete

それぞれも直感的に使えるようになっている。 コメントに載せてある SQL はstatementsで得られる文字列ではなく、実際に実行されるもの。

  • insert

    • +=を使う(Users.insertOrUpdateとかもある)
    // insert into user values (3, 'charles', 'charles@example.com')
    db.run(
    Users += User(3, "charles", "charles@example.com")
    )
  • update

    • updateを使う
    // update user set name = 'alice-updated' where name like 'alice%';
    db.run(
    Users
      .filter { _.name.startsWith("alice") }
        .map(_.name)
        .update("alice-updated")
    )
  • delete

    • deleteを使う
    // delete from user where name = 'alice';
    val f = db.run(
    Users.filter{ _.name === "alice" }.delete
    )

Monadic Slick

Scala で Monadic、つまり for 式を使って select を実行してみる。 そのためにもう 1 つテーブルを用意する。

Create Table: CREATE TABLE `hobby` (
  `id` int(11) DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `content` text,
  KEY `user_id` (`user_id`),
  CONSTRAINT `hobby_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

insert into hobby values (1, 1, 'tennis');
insert into hobby values (2, 2, 'soccer');
insert into hobby values (3, 2, 'music');

対応するモデルは以下。Usersとほぼ同じだが、今回はforeignKeyが入っている。

case class Hobby(id: Int, userId: Int, content: String)

class Hobbies(tag: Tag) extends Table[Hobby](tag, "hobby") {
  def id = column[Int]("id", O.PrimaryKey)
  def userId = column[Int]("user_id")
  def content = column[String]("content")

  def user = foreignKey("user_FK", userId, Users) (
    _.id,
    onUpdate = ForeignKeyAction.Cascade,
    onDelete = ForeignKeyAction.Cascade
  )

  def * = (id, userId, content) <> (Hobby.tupled, Hobby.unapply)
}

object Hobbies extends TableQuery(new Hobbies(_))

// Usersと同様に使用できる
// db.run(Hobbies.result) foreach println

この Hobby テーブルと前述の User テーブルを使って Monadic な select を行う。 かなり直感的に使用できる。

val q = for {
  user <- Users
  hobby <- Hobbies if hobby.userId === user.id
} yield { (user.name, hobby.content) }
db.run(q.result) foreach println

実行すると以下の結果が得られ、想定通りに実行されていることがわかる。

Vector((alice,tennis), (bob,soccer), (bob,music))

実行された SQL はこのようになっている。

List(select x2.`name`, x3.`content` from `user` x2, `hobby` x3 where x3.`user_id` = x2.`id`)

Reactive Slick

Slick3 からは Reactive であると言われている 公式

具体的には DB からの SELECT 結果を ReactiveStream として受け取ることが出来るようになっている。

val query = Users.filter { _.id =!= 2 }.result
val stream: DatabasePublisher[User] = db.stream(query)

stream
  .mapResult { user => user.name }
  .foreach { println }
  .andThen { case _ => system.terminate() }

このDatabasePublisherorg.reactivestreams.Publisherを実装している。 また、Akka-Stream と組み合わせることも可能。

val source = Source.fromPublisher(stream)
val flow = Flow[String].map(s => s"mapped: $s")
val future = source via flow runForeach println

Await.result(future andThen { case _ => system.terminate() }, Duration.Inf)

所感

DB への問い合わせ自体を IO モナドのように宣言と実行を切り離した上で、実行結果をFutureか Reactive に扱えるようになっている。 単純に使うならFutureで良さそうだが、Reactive なアプリケーションを実装しているなら相性も良さそうに感じる。 DAO を実装するには簡単に出来る上に、スキーマからコードを自動生成する機能もあるようなので非常に便利。 Schema Code Generation — Slick 3.1.1 documentation

from: https://qiita.com/petitviolet/items/688cbf917df240407077