19
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

MonadicでReactiveなSlick3.xを使う

Last updated at Posted at 2016-03-30

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

19
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?