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

  • 43
    いいね
  • 0
    コメント

はじめに

サイバー・バズ@yukihirai0505です。
私はサイバー・バズで主に 日本最大級のモニターサイト ポチカム固定費見直し比較サイト コンシェルタント のサーバーサイドを担当してます。
また、現在は新規サービスをPlayframework2.5(Scala)で開発しております。

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の初期設定まとめ

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" % "5.1.38"
        , "org.slf4j" % "slf4j-nop" % "1.6.4"
  )
}

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

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

  • [ユーザー名]
  • [パスワード]
  • [作成したDB名]
application.conf
mysql = {
  dataSourceClass="com.mysql.jdbc.jdbc2.optional.MysqlDataSource"
  properties {
    user="[ユーザー名]"
    password="[パスワード]"
    databaseName="[作成したDB名]"
    serverName="localhost"
  }
  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])

次に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とっつきにくいぜ、と思ってる方の手助けになれば幸いです。
自分もまだまだ勉強しなきゃですね。

おわりに

サイバー・バズでは一緒にサービスを創れるエンジニアを募集しています :)