petitviolet blog

    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