MonadicでReactiveなSlick3.xを使う


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

[Getting Started — Slick 3.1.1 documentation](http://slick.typesafe.com/doc/3.1.1/gettingstarted.html#quick-introduction)

# 準備

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

```sql
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の依存関係も追加。

```scala
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`としている。

```conf
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を実行してみる。

```scala
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`テーブルに対応するクラスを用意する。

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

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

```scala
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)

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

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

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

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

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

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

```scala
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`で`==`を使用する時は`===`を使う必要があるらしい。
同様に、`!=`のかわりに`=!=`が必要となる。

```scala
// 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を見るには以下のようにすれば見れる。

```scala
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`とかもある)

  ```scala
  // insert into user values (3, 'charles', 'charles@example.com')
  db.run(
    Users += User(3, "charles", "charles@example.com")
  )
  ```

- update
  - `update`を使う

  ```scala
  // update user set name = 'alice-updated' where name like 'alice%';
  db.run(
    Users
      .filter { _.name.startsWith("alice") }
        .map(_.name)
        .update("alice-updated")
  )
  ```

- delete
  - `delete`を使う

  ```scala
  // delete from user where name = 'alice';
  val f = db.run(
    Users.filter{ _.name === "alice" }.delete
  )
  ```

# Monadic Slick

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

```sql
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`が入っている。

```scala
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を行う。
かなり直感的に使用できる。

```scala
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であると言われている [公式](http://slick.typesafe.com/news/2015/04/29/slick-3.0.0-released.html)

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

```scala
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() }
```

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

```scala
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](http://slick.typesafe.com/doc/3.1.1/code-generation.html)