Edited at

[Akka HTTP + Slick]誰でも手軽に作れるScala API


はじめに

外資系で働くプログラマーの@yukihirai0505です。

ポートフォリオはこちら

Scalaってちょっととっつきにくいな。。。と思ってる方のためにScalaで簡単にAPIが作れるAkka HTTPのチュートリアルを書いてみます。

この記事のゴール

=> "ゼロベースでAkka HTTPSlickを使用したAPIを作成できる"

開発環境について

OS X El Capitan 10.11.6

今回はMacにそのまま環境を構築していきます。

できるだけシンプルにするため必要最低限のものだけ取り扱うようにしま

す。

最終的にはこちらの記事を参考に下記のようなAPIを作成します。

=> RESTful API Design

GET    /dogs     list dogs

POST /dogs create a new dog
GET /dogs/:id show the dog
PUT /dogs/:id update the dog
DELETE /dogs/:id delete the dog

また今回こちらの記事で作成するAPIのサンプルは

GitHubからもご覧いただけます。

=> サンプルソース


事前知識


Scalaについて

Scalaの基本的な文法や使用方法については

有料ですが下記のドットインストールのレッスンがオススメです。

=> Scala入門

お金かけたくない!という方は英語ですがこちらのチュートリアルがオススメです。

=> Scala Documentation

また下記の記事ではScala入門時に役立つ情報がまとめられてます。

=> Scala入門時に役立つ情報まとめ

また、ScalaはJVM上で動くので

Javaのインストールが必須になります。

あとで簡単にインストール手順について記述しておきます。


Akka HTTPについて


The Akka HTTP modules implement a full server- and client-side HTTP stack on top of akka-actor and akka-stream. It's not a web-framework but rather a more general toolkit for providing and consuming HTTP-based services. While interaction with a browser is of course also in scope it is not the primary focus of Akka HTTP.


簡単にいうとWEBフレームワークではなく、HTTPモジュールです。

異なるレベルでのAPIを提供しているので作成したいアプリケーションに適したものを自分で選択することができます。フレキシブルですね。

それぞれのAPIについては下記リンクをご覧下さい。

=> The modules that make up Akka HTTP

例えば、低レベルなものだとリクエストやレスポンスのカスタマイズがフレキシブルに行えたりします。

=> Low-level HTTP server APIs

また度重なるリリースによりパフォーマンスもだいぶ向上していて2.4.9のリリースでは


Performance is on-par or better than Spray.


とも言われています。

詳細はこちらをご覧下さい。

=> Akka 2.4.9 Released

また、sprayについては少し古いですがこちらのベンチマークが参考になります。

=> Benchmarking spray


Slickについて


Slick (“Scala Language-Integrated Connection Kit”) is Typesafe‘s Functional Relational Mapping (FRM) library for Scala that makes it easy to work with relational databases.


ScalaのFRM (Functional Relational Mapping)ライブラリです。

Scalaのコレクションを扱うかのような操作でDBにアクセスしてデータを操作できるので、使い勝手が良いです。

また


Plain SQL Support


ともあるように直接SQLを実行することもできます。

今回はMySQLと連携させるための設定を行います。

詳しくはこちらをご覧下さい。

=> What is Slick?


環境構築


Javaのインストール

まずは、Javaのインストールをしましょう。

今回はJavaのバージョン1.8を使用していきます。

ご自身の環境にJavaが入っている場合は次のsbtのインストールを行ってください。

入っているかどうかは、コマンドラインで

java -version

を実行することで確認できます。

java version "1.8.~~"

みたいな表示がでれば1.8が入ってます。

入っていない場合、

Macの方は brew cask でダウンロードがお手軽です。

まずはHomebrewのインストール

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

brew update

次に brew cask のインストール

brew install caskroom/cask/brew-cask

brew cask install java

これでjava8がインストールできます。(2016.9.16時点)

Mac以外の方やbrewで入れたくないなあという方はOracleのページからインストールしましょう。


sbtのインストール


The interactive build tool


sbtはビルドツールです。

Scalaを使用するために下記のリンクの内容に従って

sbtをインストールします。

Installing sbt on Mac

Mac以外の方はMacを購入するもしくは下記のリンクを参考にインストールして下さい。

Installing sbt

Macの方はHomebrewが入ってれば

brew install sbt

で sbt をインストールできます。

ちなみに Java が入っていない場合だと

sbt: Java 1.6+ is required to install this formula.JavaRequirement unsatisfied!

みたいなエラーがでます。


MySQLのインストール

今回はSlickをMySQLと連携させるため、

brew install mysql

でmysqlを入れておきましょう。

すでにMySQLが入っていればいいですが

入っていない場合はユーザーの設定などしておきましょう。

5.7以上のバージョンが入っている場合はこちらを参考に設定してみてください。

=> MySQL5.7の初期設定まとめ

自分のリポジトリでは docker-compose up すれば

MySQL5.7のDBをDockerで立ち上げるようにしてます。

=> docker-compose.yml


APIの実装


プロジェクトの作成

次にお好きな場所にディレクトリを作成します。

mkdir [プロジェクト名]

作成したディレクトリに移動して

build.sbt ファイルを作成します。


build.sbt

scalaVersion := "2.11.8"


これでコマンドラインで

sbt console コマンドを入力して実行すれば Scala2.11.8 を使用できることがわかります。

ちょっとだけScalaを触ってみましょう。

今回は下記をimportしてみて使ってみましょう。

http://www.scala-lang.org/api/2.11.8/index.html#scala.concurrent.duration.package

sbt console

ScalaのREPLが起動してScalaをインタラクティブに試すことができます。

ちなみにScalaのREPLは control+c もしくは :qで終了することができます。

import scala.concurrent.duration._

を実行して

1.day.toSeconds.toInt

を実行してみましょう。

下記のような感じになれば大丈夫です。

sbt console

Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
...
[info] Done updating.
[info] Starting scala interpreter...
[info]
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.
scala> import scala.concurrent.duration._
import scala.concurrent.duration._
scala> 1.day.toSeconds.toInt
res0: Int = 86400

ちょっとだけScala触ってる感を感じることが出来たかと思います。

ちなみにやってることは

scala.concurrent.duration.packageをimportすることで

Intの後にday,hourなどの期間を表す単語をつけるとその期間を表すオブジェクトにできます。

今回は 1.day つまり 1日を toSeconds で 秒に変換しています。

そのままではLong型なので toInt で Int型に変換しています。

では、Scalaを感じることができたところで

今回作成するAPIのディレクトリ構成を用意していきます。

下記のような構成で進めるため必要なディレクトリやファイルを作成してください。

[プロジェクトディレクトリ]

└── src
   └── main
       ├── resources
      │   └── application.conf
       └── scala
         ├── Server.scala
         ├── connection
         │   └── MySQLDBImpl.scala
         ├── dao
         │   └── DogDao.scala
         ├── http
         │   └── DogRoutes.scala
         └── model
         └── Dog.scala


Akka HTTPの設定

次にAkka HTTPを使用するための設定をしていきます。

先ほど作成した build.sbt ファイルを編集します。


build.sbt

scalaVersion := "2.11.8"

libraryDependencies ++= {
val akkaV = "2.4.10"
Seq(
"com.typesafe.akka" %% "akka-http-experimental" % akkaV
)
}


Akka HTTPを試してみるために

src/main/scala 配下に

Sample.scala ファイルを作成します。


Sample.scala

import akka.actor.ActorSystem

import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import scala.io.StdIn

object Sample {
def main(args: Array[String]) {

implicit val system = ActorSystem("my-system")
implicit val materializer = ActorMaterializer()
// needed for the future flatMap/onComplete in the end
implicit val executionContext = system.dispatcher

val route =
path("hello") {
get {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to akka-http</h1>"))
}
}

val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)

println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
StdIn.readLine() // let it run until user presses return
bindingFuture
.flatMap(_.unbind()) // trigger unbinding from the port
.onComplete(_ => system.terminate()) // and shutdown when done
}
}


作成したらコマンドラインでプロジェクトのルートディレクトリで

sbt run を実行します。

まだ sbt console でScalaのREPLが起動している場合は control+c と入力して終了しておきましょう。

sbt run がうまく実行できれば

下記のようなログが見れます。

http://localhost:8080/hello にアクセスしてみましょう。

うまくいかない場合は sbt run を実行している場所がプロジェクトのルートディレクトリかどうかや

src/main/scala 配下に Sample.scala ファイルを作成しているかを確認してください。

sbt run                                                                                                              [master ~/cyberbuzz/test/akka_slick]

Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
...
[info] Running Sample
Server online at http://localhost:8080/
Press RETURN to stop...

ブラウザで Say hello to akka-http を確認できればAkka HTTPの設定は完了です。

Sample.scala ファイルはもう使用しないので削除しておきましょう。

またサーバーもコマンドラインで returnキーを入力して止めておきましょう。


Slickの設定

まずは今回接続するDBを作成しておきます。

MySQLで適当なデータベースを作成してください。

一応コマンドラインでのDB作成について記載しておきます。

mysql -u[ユーザー名] -p[パスワード]

CREATE DATABASE [適当なDB名];

次に build.sbt ファイルに設定を書いていきます。


build.sbt

scalaVersion := "2.11.8"

libraryDependencies ++= {
val akkaV = "2.4.10"
val slickVersion = "3.1.1"
Seq(
"com.typesafe.akka" %% "akka-http-experimental" % akkaV
, "com.typesafe.slick" %% "slick" % slickVersion
, "com.typesafe.slick" % "slick-hikaricp_2.11" % slickVersion
, "mysql" % "mysql-connector-java" % "8.0.11"
, "org.slf4j" % "slf4j-nop" % "1.6.4"
)
}


次に src/main/resourcesapplication.conf ファイルを作成します。

下記は適宜ご自身の環境に合わせて変更してください。


  • [ユーザー名]

  • [パスワード]

  • [作成したDB名]


application.conf

mysql = {

dataSourceClass="com.mysql.cj.jdbc.MysqlDataSource"
properties {
user="[ユーザー名]"
password="[パスワード]"
databaseName="[作成したDB名]"
serverName="localhost"
portNumber="3306"
}
numThreads=10
}

次に src/main/scala 配下に connection ディレクトリを作成してDBの設定を書いていきます。

MySQLDBImpl.scala ファイルを作成します。


MySQLDBImpl.scala

package connection

import slick.driver.MySQLDriver

trait MySQLDBImpl {

val driver = MySQLDriver

import driver.api._

val db: Database = MySqlDB.connectionPool

}

private[connection] object MySqlDB {

import slick.driver.MySQLDriver.api._

val connectionPool = Database.forConfig("mysql")

}


Slickの設定は以上です。


テーブルの作成

次にテーブルを作成していきます。

まず build.sbt ファイルに akka-http-spray-json-experimental を追加します。


build.sbt

scalaVersion := "2.11.8"

libraryDependencies ++= {
val akkaV = "2.4.10"
val slickVersion = "3.1.1"
Seq(
"com.typesafe.akka" %% "akka-http-experimental" % akkaV
, "com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaV
, "com.typesafe.slick" %% "slick" % slickVersion
, "com.typesafe.slick" % "slick-hikaricp_2.11" % slickVersion
, "mysql" % "mysql-connector-java" % "5.1.38"
, "org.slf4j" % "slf4j-nop" % "1.6.4"
)
}


src/main/scala/model 配下に Dog.scala ファイルを作成します。

DogはIDと名前だけを持ったシンプルな作りにしておきます。


Dog.scala

package model

import connection. MySQLDBImpl
import spray.json.DefaultJsonProtocol

trait DogTable extends DefaultJsonProtocol {
this: MySQLDBImpl =>

import driver.api._

implicit lazy val dogFormat = jsonFormat2(Dog)
implicit lazy val dogListFormat = jsonFormat1(DogList)

class DogTable(tag: Tag) extends Table[Dog](tag, "dog") {
val id = column[Int]("id", O.PrimaryKey, O.AutoInc)
val name = column[String]("name")

def * = (name, id.?) <>(Dog.tupled, Dog.unapply)

}

}

case class Dog(name: String, id: Option[Int] = None)
case class DogList(dogs: List[Dog])


追記 def * = (name, id.?) <>(Dog.tupled, Dog.unapply) の役割について下記のリンクで詳しく説明してる方がいらっしゃったのでリンク紹介しておきます。

=> scala slick method I can not understand so far

次にDAOを作成します。

src/main/scala/dao 配下に DogDao.scala ファイルを作成します。


DogDao.scala

package dao

import connection.MySQLDBImpl
import model.{Dog, DogTable}

import scala.concurrent.Future

trait DogDao extends DogTable with MySQLDBImpl {

import driver.api._

protected val dogTableQuery = TableQuery[DogTable]

def create(dog: Dog): Future[Int] = db.run {
dogTableAutoInc += dog
}

def update(dog: Dog): Future[Int] = db.run {
dogTableQuery.filter(_.id === dog.id.get).update(dog)
}

def getById(id: Int): Future[Option[Dog]] = db.run {
dogTableQuery.filter(_.id === id).result.headOption
}

def getAll: Future[List[Dog]] = db.run {
dogTableQuery.to[List].result
}

def delete(id: Int): Future[Int] = db.run {
dogTableQuery.filter(_.id === id).delete
}

def ddl = db.run {
dogTableQuery.schema.create
}

protected def dogTableAutoInc = dogTableQuery returning dogTableQuery.map(_.id)

}



ルートの作成

下記のAPIを作成します。

GET    /dogs     list dogs

POST /dogs create a new dog
GET /dogs/:id show the dog
PUT /dogs/:id update the dog
DELETE /dogs/:id delete the dog

src/main/scala/http 配下に DogRoutes.scala ファイルを作成します。


DogRoutes.scala

package http

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.model.HttpResponse
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.directives.MethodDirectives
import dao.DogDao
import model.Dog

import scala.concurrent.ExecutionContextExecutor

trait DogRoutes extends SprayJsonSupport {
this: DogDao =>

implicit val dispatcher: ExecutionContextExecutor

val routes = pathPrefix("dogs") {
pathEnd {
get {
complete(getAll)
} ~ post {
entity(as[Dog]) { dog =>
complete {
create(dog).map { result => HttpResponse(entity = "dog has been saved successfully") }
}
}
}
} ~
path(IntNumber) { id =>
get {
complete(getById(id))
} ~ put {
entity(as[Dog]) { dog =>
complete {
val newDog = Dog(dog.name, Option(id))
update(newDog).map { result => HttpResponse(entity = "dog has been updated successfully") }
}
}
} ~ MethodDirectives.delete {
complete {
delete(id).map { result => HttpResponse(entity = "dog has been deleted successfully") }
}
}
}
}
}


少しだけ解説すると、

val routes = pathPrefix("dogs")

最初の部分で、 pathPrefix つまりURL部分で最初につける名前を決定します。

http://localhost:8080/dogs

といったところです。

次にその中で

pathEndpath(IntNumber) がありますが

pathEnd 部分で下記のルートを指定

GET    /dogs     list dogs

POST /dogs create a new dog

path(IntNumber) 部分で下記のルートを指定しています。

GET    /dogs/:id show the dog

PUT /dogs/:id update the dog
DELETE /dogs/:id delete the dog

非常にお手軽にルーティングできていることがわかります。


サーバーの作成

最後にサーバーファイルを作成します。

src/main/scala 配下に Server.scala ファイルを作成します。

アプリを起動したときに、最初にテーブルにいくつかデータを入れるようにしておきます。

下記のサイトを参考にお犬さんの人気の名前をチョイスしてみました。

=> TOP 100 MOST POPULAR MALE AND FEMALE DOG NAMES


Server.scala

import akka.actor.ActorSystem

import akka.http.scaladsl.Http
import akka.stream.ActorMaterializer
import dao.DogDao
import model.Dog
import http.DogRoutes
import scala.io.StdIn

import scala.concurrent.ExecutionContextExecutor

object Server extends App with DogRoutes with DogDao {

implicit val system: ActorSystem = ActorSystem()

implicit val materializer = ActorMaterializer()

implicit val dispatcher: ExecutionContextExecutor = system.dispatcher

ddl.onComplete {
_ =>
create(Dog("Bailey"))
create(Dog("Max"))
create(Dog("Charlie"))
create(Dog("Bella"))
create(Dog("Lucy"))
create(Dog("Molly"))
val bindingFuture = Http().bindAndHandle(routes, "localhost", 8080)
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
StdIn.readLine()
bindingFuture
.flatMap(_.unbind())
.onComplete(_ => system.terminate())
}
}


プロジェクトのルートディレクトリで

sbt run

とすればアプリを起動することができます。

Mysqlを起動するのを忘れないようにしましょう。

Homebrewで入れた方は下記のコマンドで起動しておきましょう。

mysql.server start


curlでテスト

サーバーが立ち上がったら下記のコマンドでそれぞれ実行して試してみることができます。

よかったら使用してみてください。


  • list dogs

curl --request GET 'localhost:8080/dogs'


  • create a new dog

curl -H "Content-Type: application/json" --request POST 'localhost:8080/dogs'  -d '{"name":"NEW DOG"}'


  • show the dog

curl --request GET 'localhost:8080/dogs/1'


  • update the dog

curl -H "Content-Type: application/json" --request PUT 'localhost:8080/dogs/1'  -d '{"name":"Updated DOG"}'


  • delete the dog

curl --request DELETE 'localhost:8080/dogs/1'


参考


まとめ

こうしてみると、かなり少ない記述でDB連携からAPIの実装までできたことが分かるかと思います。

ちょっとScalaとっつきにくいぜ、と思ってる方の手助けになれば幸いです。

自分もまだまだ勉強しなきゃですね。