読者です 読者をやめる 読者になる 読者になる

AkkaHTTP+Slick(PostgreSQL)+Herokuで簡単なRESTfulAPIを公開してみる。

Scala Akka-HTTP Slick Heroku

概要

最近、Akka-HTTP + Slick3 + PostgreSQLでHerokuにDeployしました。
忘れそうなので、Herokuとの接続の作業メモを書きます。
下記で現在(12/10)公開しています。
http://kz-akkahttp.herokuapp.com/posts


開発環境

Scala: 2.11.8
SBT: 0.13.9
Akka-HTTP: 2.4.11
Slick: 3.1.1
PostgreSQL: 9.4.9


Create App

シンプルな REST APICRUD機能を作成する。

GET /posts => 記事一覧表示
POST /posts => 新規記事投稿
GET /posts/:id => 記事表示
PUT /posts/:id => 記事変更
DELETE /posts/:id => 記事削除

// build.sbt

enablePlugins(JavaServerAppPackaging)

name := "slick_akkahttp_heroku"

version := "1.0"

scalaVersion := "2.11.8"

libraryDependencies ++= Seq(
 "com.typesafe.akka" %% "akka-http-core" % "2.4.11",
 "com.typesafe.akka" %% "akka-http-experimental" % "2.4.11",
 "com.typesafe.akka" %% "akka-http-spray-json-experimental" % "2.4.11",
 "com.typesafe.akka" %% "akka-http-xml-experimental" % "2.4.11",
 "com.typesafe.slick" %% "slick" % "3.1.1",
 "org.postgresql" % "postgresql" % "9.4-1200-jdbc41"
)

build.sbtに必要なライブラリを記述し、JavaServerAppPackagingのプラグインを有効にします。

echo "web: target/universal/stage/bin/slick_akkahttp_heroku $JAVA_OPTS" >> Procfile
echo "java.runtime.version=1.8" >> system.properties
echo "sbt.version=0.13.9" >> project/build.properties
echo "addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.1.4")" >> project/plugins.sbt

Heroku を利用するにあたって、ルートディレクトリに Heroku 上で実行するコマンドを書き込んだProcfile という特別なファイルを作成します。
Java の実行環境のバージョンや、SBTのバージョンを固定する。


Source Code

// src/main/scala/Boot.scala

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.stream.ActorMaterializer
import scala.util.Properties

object Boot extends App with Routes {
    val port = Properties.envOrElse("PORT", "8080").toInt
    implicit val system = ActorSystem("test")
    implicit val materializer = ActorMaterializer()
    import system.dispatcher
    val bindingFuture = Http().bindAndHandle(routes, "0.0.0.0", port)
}

Mainの処理をBoot.scalaで書きます。

// src/main/scala/Routes.scala
import akka.http.scaladsl.model.HttpResponse
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.server.Directives._
import spray.json.DefaultJsonProtocol
import scala.concurrent.ExecutionContext.Implicits.global

trait Routes extends DefaultJsonProtocol with SprayJsonSupport with Config {
  implicit val postFormats = jsonFormat3(Post)

  val routes =
    pathPrefix("posts") {
     pathEnd {
       get {
         complete(getPosts)
       } ~
       post {
         entity(as[Post]) { post =>
           complete {
             createPost(post)
               .map {result => HttpResponse(entity = "Created")}
           }
         }
       }
     } ~
     path(IntNumber) { id =>
       get {
         complete(getPost(id))
       } ~
       put {
         entity(as[Post]) { post =>
           complete {
             updatePost(Post(Option(id), post.title, post.body))
               .map {result => HttpResponse(entity = "Updated")}
           }
         }
       } ~
       delete {
         complete {
           deletePost(id)
             .map {result => HttpResponse(entity = "Deleted")}
         }
       }
     }
   }
}

JSONで値を返すため、パラメータを3つ持つPostを暗黙にjsonFormat3で変換しています。
それぞれのレスポンスはConfigをミックスインしてます。

// src/main/scala/Config.scala

import scala.concurrent.Await
import scala.concurrent.Future
import scala.concurrent.duration.Duration
import scala.concurrent.ExecutionContext.Implicits.global
import slick.driver.PostgresDriver.api._
import slick.jdbc.meta.MTable

trait Config {

  val db = Database.forConfig("mydb")
  val posts = TableQuery[Posts]

  val check = Await.result(db.run(MTable.getTables("posts")), Duration.Inf)
  if (check.isEmpty) {
    db.run(DBIO.seq(
      posts.schema.create,
      posts ++= Seq(
        Post(None, "Scala", "Hybrid"),
        Post(None, "Ruby", "Enjoy"),
        Post(None, "Swift", "iOS")
      )
    ))
  }

  def getPosts: Future[List[Post]] =
    db.run(posts.to[List].result)

  def getPost(id: Int): Future[Option[Post]] =
    db.run(posts.filter(_.id === id).result.headOption)

  def createPost(post: Post) = db.run(posts += post)

  def updatePost(post: Post) =
    db.run(posts.filter(_.id === post.id.get).update(post))

  def deletePost(id: Int) = db.run(posts.filter(_.id === id).delete)
}

Config.scalaでは、Herokuデプロイ時にPostsテーブルを作成させるため、テーブルがない場合の作成処理を記述。
テーブル作成と共に、複数件インサートしています。
残りはCRUDのための各処理を記述しています。

// src/main/scala/Posts.scala
import slick.driver.PostgresDriver.api._

case class Post(id: Option[Int], title: String, body: String)
class Posts(tag: Tag) extends Table[Post](tag, "posts") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
  def title = column[String]("title")
  def body = column[String]("body")
  def * = (id.?, title, body) <> (Post.tupled, Post.unapply)
}

PostsModelです。id、title、bodyのパラメータを持ち、idはオートインクリメントにしています。

Deploy on Heroku

git init
heroku create
heroku addons:add heroku-postgresql --version=9.4
heroku config
  # => postgres://username:password@hostname/database

heroku config でデータベースへの接続情報が得られます。
postgres://ユーザ名:パスワード@ホスト名/データベース名
のような並びなので、これをもとにapplication.confを変更します。

# src/main/resources/application.conf

mydb = {
  url = "jdbc:postgresql://hostname/database?user=username&password=password"
  driver = org.postgresql.Driver
  connectionPool = disabled
  keepAliveConnection = true
}
sbt compile stage
git add .
git commit -m "deploy"
git push heroku master
heroku open

表示されたページにcurlwgetで確認してみてください。
POSTとPUTは、JSONで送らないといけません。


参考にしたサイト、ソースコード
[Akka HTTP + Slick]文系×体育会出身プログラマでも手軽に作れるScala API - Qiita
GitHub - rikiya-yamamoto/akka-http-heroku-template