Akka+AkkaHttp+SlickでTODOアプリAPI（その1）

# 始めに
ここ数ヶ月Scalaを触ってみたので、その成果としてTODOアプリ用のAPIをAkka+AkkaHttp+Slickで組んでみる。
ついでと言うと失礼だけどScalazもかじってみる。

＊それぞれのライブラリについての詳細な解説はしんどいので極力省略します。

##構成##
- 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

# 設計
大まかに設計してみます。
まずは構成図です。それぞれのライブラリを用いて三層チックに構成します。クライアントは当然作りません。

![Untitled.png](https://qiita-image-store.s3.amazonaws.com/0/94252/ec2d9758-44c0-1901-9915-4890c7c77d73.png)

次に大まかなクラス図です。アクターには一応Supervisorをかませます。レポジトリについてはインターフェースと実装を分離する予定です。

![Untitled(1) (1).png](https://qiita-image-store.s3.amazonaws.com/0/94252/5f8d00de-4bcd-12a7-73a3-d05a16725cd2.png)


本来であればわざわざアクター使う必要がない気もしますが、それを言ったらお終いになってしまうので気にしません。

# 環境構築
IntelliJにScalaPlugin導入済みとして進めます。
とりあえずテンプレートを使ってもいいけど、今回は空のプロジェクトから作ります。「create new project」から「Scala」「sbt」で新規プロジェクトを作成。

![1.jpg](https://qiita-image-store.s3.amazonaws.com/0/94252/6a8ace57-6bf7-1500-f4e0-3c60f61d821b.jpeg)

早速、sbt周りを整備します。
まずはsbtプラグイン関係。唐突ですが最終的にDockerImage化したいので[sbt-native-packager](https://github.com/sbt/sbt-native-packager)を入れます。

```scala:project/plugin.sbt
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.4")
```

次にbuild.sbtを編集。使用するライブラリを追加し、DockerImageでビルドできるように定義します。scalacOptionsはお好みで。

```scala:build.sbt
lazy val akkaVersion = "2.5.9"
lazy val akkaHttpVersion = "10.1.0"
lazy val slickVersion = "3.2.3"
lazy val scalazVersion = "7.2.19"

scalaVersion := "2.12.5"

organization := "todo.api"

name := "todo-api"

version := "0.1-SNAPSHOT"

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-actor" % akkaVersion,
  "com.typesafe.akka" %% "akka-slf4j" % akkaVersion,
  "ch.qos.logback" % "logback-classic" % "1.2.3",
  "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test,
  "com.typesafe.akka" %% "akka-stream" % akkaVersion,
  "com.typesafe.akka" %% "akka-http" % akkaHttpVersion,
  "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % Test,
  "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion,
  "io.spray" %% "spray-json" % "1.3.4",
  "com.typesafe.slick" %% "slick" % slickVersion,
  "com.typesafe.slick" %% "slick-hikaricp" % slickVersion,
  "org.scalaz" %% "scalaz-core" % scalazVersion,
  "org.scalaz" %% "scalaz-concurrent" % scalazVersion,
  "org.scalaz" %% "scalaz-effect" % scalazVersion,
  "org.scalaz" %% "scalaz-iteratee" % scalazVersion,
  "org.scalatest" %% "scalatest" % "3.0.5" % Test,
  "com.h2database" % "h2" % "1.4.196"
)

mainClass in Compile := Some("todo.api.Main")

dockerBaseImage := "java:8-jdk-alpine"

dockerExposedPorts := Seq(8000)

enablePlugins(JavaAppPackaging, AshScriptPlugin, DockerPlugin)

scalacOptions ++= Seq(
  "-deprecation",
  "-feature",
  "-unchecked",
  "-language:_",
  "-Xlint",
  "-Xfatal-warnings",
  "-Ywarn-dead-code",
  "-Ywarn-numeric-widen",
  "-Ywarn-unused",
  "-Ywarn-unused-import",
  "-Ywarn-value-discard"
)
```

この段階でIntelliJのsbtシェルにエラーが出てなければ大丈夫なはず・・・。あと、reloadコマンドを打っておいた方が無難かも。
とりあえずAkka・AkkaHttp・Slickがそれぞれ動くか確かめます。

# Hello Akka
まずはAkkaのアクターを動作させてみます。
Akkaは[HOCON](https://github.com/lightbend/config)ベースの設定が必須です（Slickも使うけど）。今回はテスト側しか使わないので「src/test/resources」配下に「application.conf」を作成します。詳細な説明は[ドキュメント](https://doc.akka.io/docs/akka/current/general/configuration.html#configuration)を参照してください。

```applicaiton.conf
akka {
  loggers = ["akka.testkit.TestEventListener"]
  loglevel = "DEBUG"

  actor {
    provider = "akka.actor.LocalActorRefProvider"
  }
}
```


mainのルートパッケージに以下を作成します。

```scala:HelloActor.scala
package todo.api

import akka.actor.{Actor, ActorLogging}

object HelloActor {

  // 受信用メッセージ
  final case class HelloCommand(name: String)

  // 返信用メッセージ
  final case class HelloReply(message: String)

}

class HelloActor() extends Actor with ActorLogging {

  import HelloActor._

  override def preStart(): Unit = log.info("starting hello actor.")

  override def postStop(): Unit = log.info("stopping hello actor.")

  // HelloCommandを受け取ったらHelloReplyを返す
  override def receive: Receive = {
    case HelloCommand(name) =>
      sender() ! HelloReply(s"Hello $name!!")
  }

}
```

次にtestのルートパッケージに以下を作成します。

```scala:HelloActorSpec.scala
package todo.api

import akka.actor.{ActorSystem, Props}
import akka.testkit.{TestKit, TestProbe}
import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}

class HelloActorSpec
    extends TestKit(ActorSystem("hello-actor-spec"))
    with WordSpecLike
    with Matchers
    with BeforeAndAfterAll {

  import HelloActor._

  override def afterAll(): Unit = TestKit.shutdownActorSystem(system)

  "hello actor" should {

    "hello!!" in {
      val probe = TestProbe()
      val helloActor = system.actorOf(Props[HelloActor])

      helloActor.tell(HelloCommand("ME"), probe.ref)
      probe.expectMsg(HelloReply("Hello ME!!"))
    }
  }

}
```

sbtシェルからtestコマンドを呼んで、完了すればOKです。

# Hello AkkaHttp
続いてAkkaHttpでルーティングを動作させてみます。
まずmainのルートパッケージに以下を作成します。

```scala:HelloRoute.scala
package todo.api

import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route

object HelloRoute {

  // nameパラメータを受け付けテキストを返信する
  val helloRoute: Route = path("hello") {
    parameter('name) { name =>
      complete(StatusCodes.OK -> s"Hello $name!!")
    }
  }

}
```

次にtestのルートパッケージに以下を作成します。

```scala:HelloRouteSpec.scala
package todo.api

import akka.http.scaladsl.testkit.ScalatestRouteTest
import org.scalatest.{Matchers, WordSpecLike}

class HelloRouteSpec extends WordSpecLike with Matchers with ScalatestRouteTest {

  import HelloRoute._

  "hello route" should {

    "hello!!" in {

      Get("/hello?name=ME") ~> helloRoute ~> check {
        responseAs[String] shouldBe "Hello ME!!"
      }
    }
  }
}
```

sbtシェルからtestコマンドを呼んで、完了すればOKです。

# Hello Slick
最後にSlickで簡単なCRUDの動作を確認します。
Slickのデータベースインスタンスの生成方法はいくつかありますが、今回はHOCON（application.conf）から生成します。
Hello Akka時に作成した「application.conf」に以下を追記します。
urlにある環境変数については[ドキュメント](http://www.h2database.com/javadoc/org/h2/engine/DbSettings.html)を参照してください。

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

加えて、上記のINIT句で指定したデータベースの初期化スクリプトを作成します。

```sql:create-hello.sql
CREATE TABLE IF NOT EXISTS hello(
  id INT NOT NULL AUTO_INCREMENT
  , name VARCHAR(50)
  , PRIMARY KEY (id));
```

次にmainのルートパッケージに以下を作成します。

```scala:HelloSlick.scala
package todo.api

import scala.concurrent.{ExecutionContext, Future}

import slick.jdbc.H2Profile.api._

object HelloSlick {

  def apply(db: Database)(implicit ec: ExecutionContext): HelloSlick = new HelloSlick(db)

  // モデル定義
  final case class Hello(id: Int, name: String)

  // テーブル定義
  class HelloTable(tag: Tag) extends Table[Hello](tag, "hello") {

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

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

    override def * = (id, name) <> (Hello.tupled, Hello.unapply)

  }

  val helloTable = TableQuery[HelloTable]

}

class HelloSlick(db: Database)(implicit ec: ExecutionContext) {

  import HelloSlick._

  // 全件取得
  def findAll(): Future[Seq[Hello]] = db.run(helloTable.result)

  // Id指定取得
  def findById(id: Int): Future[Option[Hello]] =
    db.run(helloTable.filter(_.id === id).result.headOption)

  // 追加
  def create(hello: Hello): Future[Hello] =
    db.run(helloTable returning helloTable.map(_.id) += hello).map(id => hello.copy(id = id))

  // 更新
  def update(hello: Hello): Future[Int] =
    db.run(helloTable.filter(_.id === hello.id).map(_.name).update(hello.name))

  // 削除
  def delete(id: Int): Future[Int] = db.run(helloTable.filter(_.id === id).delete)

}
```

次にtestのルートパッケージに以下を作成します。

```scala:HelloSlickSpec.scala
package todo.api

import scala.concurrent.ExecutionContext

import org.scalatest.concurrent.ScalaFutures
import org.scalatest.time.{Millis, Seconds, Span}
import org.scalatest.{Matchers, WordSpecLike}
import slick.jdbc.H2Profile.api._

class HelloSlickSpec extends WordSpecLike with Matchers with ScalaFutures {

  import HelloSlick._

  implicit val executor: ExecutionContext = ExecutionContext.global

  override implicit val patienceConfig: PatienceConfig =
    PatienceConfig(timeout = Span(5, Seconds), interval = Span(200, Millis))

  val db = Database.forConfig("hello-slick-db")
  val helloSlick = HelloSlick(db)

  "hello slick" should {

    "crud!!" in {
      // c: create
      val created = helloSlick.create(Hello(0, "ME")).futureValue
      created.id shouldBe 1

      // r: read
      val findAll = helloSlick.findAll().futureValue
      findAll.size shouldBe 1
      val findById = helloSlick.findById(created.id).futureValue
      findById.nonEmpty shouldBe true
      findById.foreach(a => a.id shouldBe created.id)

      // u: update
      val updated = helloSlick.update(Hello(created.id, "ME2")).futureValue
      updated shouldBe 1

      // d: delete
      val deleted = helloSlick.delete(created.id).futureValue
      deleted shouldBe 1
    }

  }

}
```

sbtシェルからtestコマンドを呼んで、完了すればOKです。

# 終わりに
次回から実際に作成していきます。次回はレポジトリを作成します。