Scala
Akka
slick

Akka+AkkaHttp+SlickでTODOアプリAPI(その2)

始めに

前回に引き続き、今回はレポジトリ部分を作成します。

構成

  • IntelliJ 2018
  • Scala 2.12.5
  • sbt 1.1.4
  • Akka 2.5.11
  • AkkaHttp 10.1.0
  • Slick 3.2.3
  • Scalaz 7.2.19

成果物

https://github.com/lightstaff/scala-todo-api

レポジトリ

早速、書いていきます。まずはsrc/main/resources/application.confを作成します。Slickに読ませるデータベース設定です。

application.conf
todo-slick-db {
  dataSourceClass = "slick.jdbc.DriverDataSource"
  connectionPool = disabled
  properties = {
    driver = "org.h2.Driver"
    url = "jdbc:h2:mem:todo-api-todo;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;MODE=MySQL;INIT=runscript from 'src/main/resources/create-todo.sql'"
  }
}

合わせて、テーブル定義。

create-todo.sql
CREATE TABLE IF NOT EXISTS todo(
  id INT NOT NULL AUTO_INCREMENT
  , body VARCHAR(255)
  , PRIMARY KEY (id));

ルートパッケージ配下にrepositoryパッケージを追加します。テーブルは前回のHelloSlickとほとんど一緒です。

Model.scala
package todo.api.repository

import slick.jdbc.H2Profile.api._

object Model {

  final case class Todo(id: Int, body: String)

  class TodoTable(tag: Tag) extends Table[Todo](tag, "todo") {

    def id = column[Int]("id", O.PrimaryKey, O.AutoInc)

    def body = column[String]("body")

    override def * = (id, body) <> (Todo.tupled, Todo.unapply)

  }

}

続いてレポジトリ自体を定義しますが、簡易のDIを構成するのでインターフェースと実装は分離します。
また、レポジトリの各メソッドは単純なFutureを返さず、FutureにScalazのEitherを包んで返すことにします。

まずはインターフェースから。

TodoRepository.scala
package todo.api.repository

import scala.concurrent.Future

import scalaz.\/

trait TodoRepository {

  import Model._

  def findAll(): Future[\/[Throwable, Seq[Todo]]]

  def findById(id: Int): Future[\/[Throwable, Option[Todo]]]

  def create(data: Todo): Future[\/[Throwable, Int]]

  def update(data: Todo): Future[\/[Throwable, Int]]

  def delete(id: Int): Future[\/[Throwable, Int]]

}

続けて実装です。今回はslickのfor構文も使ってみます。

TodoRepositoryImpl.scala
package todo.api.repository

import scala.concurrent.Future
import scala.util.control.NonFatal

import scalaz.\/
import scalaz.syntax.ToEitherOps
import slick.dbio.DBIOAction
import slick.jdbc.H2Profile.api._

// Databaseインスタンスをコンストラクタインジェクション
class TodoRepositoryImpl(db: Database) extends TodoRepository with ToEitherOps {

  import Model._

  lazy val todos = TableQuery[TodoTable]

  // Future[R]ではなくFuture[\/[Throwable, R]]を返すようにしたラッパー
  private def run[R](a: DBIOAction[R, NoStream, Nothing]): Future[\/[Throwable, R]] =
    db.run(a).map(_.right[Throwable]).recover {
      case NonFatal(ex) =>
        ex.left[R]
    }

  override def findAll(): Future[\/[Throwable, Seq[Todo]]] = run(todos.result)

  override def findById(id: Int): Future[\/[Throwable, Option[Todo]]] = {
    val q = for {
      r <- todos if r.id === id
    } yield r

    run(q.result.headOption)
  }

  override def create(data: Todo): Future[\/[Throwable, Int]] =
    run(todos returning todos.map(_.id) += data)

  override def update(data: Todo): Future[\/[Throwable, Int]] = {
    val q = for {
      r <- todos if r.id === data.id
    } yield r.body

    run(q.update(data.body))
  }

  override def delete(id: Int): Future[\/[Throwable, Int]] = {
    val q = for {
      r <- todos if r.id === id
    } yield r

    run(q.delete)
  }

}

以上で、レポジトリは完了です。

終わりに

次回は今回作成したレポジトリを扱うアクターを定義します。レポジトリの戻り値をScalazのEitherにした意味がきっと出てくるはず・・・。