はじめに
外資系で働くプログラマーの@yukihirai0505です。
ポートフォリオはこちら。
Scalaってちょっととっつきにくいな。。。と思ってる方のためにScalaで簡単にAPIが作れるAkka HTTPのチュートリアルを書いてみます。
この記事のゴール
=> "ゼロベースでAkka HTTPとSlickを使用した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をインストールします。
Mac以外の方はMacを購入するもしくは下記のリンクを参考にインストールして下さい。
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で立ち上げるようにしてます。
APIの実装
プロジェクトの作成
次にお好きな場所にディレクトリを作成します。
mkdir [プロジェクト名]
作成したディレクトリに移動して
build.sbt
ファイルを作成します。
scalaVersion := "2.11.8"
これでコマンドラインで
sbt console
コマンドを入力して実行すれば Scala2.11.8
を使用できることがわかります。
ちょっとだけScalaを触ってみましょう。
今回は下記をimportしてみて使ってみましょう。
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
ファイルを編集します。
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
ファイルを作成します。
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
ファイルに設定を書いていきます。
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/resources
に application.conf
ファイルを作成します。
下記は適宜ご自身の環境に合わせて変更してください。
- [ユーザー名]
- [パスワード]
- [作成したDB名]
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
ファイルを作成します。
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
を追加します。
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と名前だけを持ったシンプルな作りにしておきます。
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
ファイルを作成します。
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
ファイルを作成します。
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
といったところです。
次にその中で
pathEnd
と path(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
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'
参考
-
Routing DSL for HTTP servers
- Sample.scalaで使用
-
knoldus/akka-http-slick
- Akka HTTPとSlickの連携で参考にさせて頂きました。
まとめ
こうしてみると、かなり少ない記述でDB連携からAPIの実装までできたことが分かるかと思います。
ちょっとScalaとっつきにくいぜ、と思ってる方の手助けになれば幸いです。
自分もまだまだ勉強しなきゃですね。