LoginSignup
14
7

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-04-12

始めに

ここ数ヶ月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

成果物

設計

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

Untitled.png

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

Untitled(1) (1).png

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

環境構築

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

1.jpg

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

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

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

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ベースの設定が必須です(Slickも使うけど)。今回はテスト側しか使わないので「src/test/resources」配下に「application.conf」を作成します。詳細な説明はドキュメントを参照してください。

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

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

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

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のルートパッケージに以下を作成します。

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のルートパッケージに以下を作成します。

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のルートパッケージに以下を作成します。

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にある環境変数についてはドキュメントを参照してください。

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句で指定したデータベースの初期化スクリプトを作成します。

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

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

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のルートパッケージに以下を作成します。

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です。

終わりに

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

14
7
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
14
7