Finch + MySQLでREST APIサーバを構築する

これはScala Advent Calendar 2015(Adventar版)7日目です。6日目はHiroyuki-NagataさんのScalatraとnon-blocking APIについてメモ - なんとな~くしあわせ?の日記でした。

さて、7日目はFinchというFinagleラッパーを紹介しようと思います。よくあるSinatraライクなマイクロフレームワークのひとつです。Hello Wordをブラウザに出力してはい終わりというのも味気ないので、MySQLに接続してレコードを取り出したり登録出来るところまで持っていきます。

はじめに

今回は以下のエンドポイントを作ることにします(更新と削除は面倒になったので無し!ごめんなさい)

GET  /users
GET  /users/:id
POST /users

コードを書く前にMySQLにテーブルを作っておきましょう。

CREATE TABLE `users` (
  `id`          BIGINT       NOT NULL AUTO_INCREMENT,
  `email`       VARCHAR(255) NOT NULL,
  `screen_name` VARCHAR(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

依存関係

開発版の0.9.2-SNAPSHOTを利用します。またMySQLに繋ぐためにfinagle-mysqlも入れます。

build.sbtは下記のようになります。

name := "finchtest"

version := "0.1.0"

scalaVersion := "2.11.7"

resolvers += Resolver.sonatypeRepo("snapshots")

libraryDependencies ++= Seq(
  "com.twitter"         %% "finagle-mysql"   % "6.30.0",
  "com.github.finagle"  %% "finch-core"      % "0.9.2-SNAPSHOT" changing(),
  "com.github.finagle"  %% "finch-argonaut"  % "0.9.2-SNAPSHOT" changing()
)

実装

日本語の情報がほとんどないことや公式のドキュメントがやや分かりづらいことを除けば1Finch自体の使いかたはそんなに難しくないです。Finchよりもfinagle-mysqlの使いかたを調べるのに苦労したのは内緒です。

今回はDBのCRUD操作をするUser.scalaMain.scalaの2ファイル書きました。

package jp.dakatsuka.finch

import argonaut.Argonaut._
import argonaut.CodecJson
import com.twitter.finagle.exp.mysql._
import com.twitter.util.Future

case class User(id: Long, email: String, screen_name: String)

object User {
  implicit val userCodec: CodecJson[User] =
    casecodec3(User.apply, User.unapply)("id", "email", "screen_name")

  def all()(implicit client: Client): Future[Seq[User]] =
    client.select("SELECT * FROM users")(convertToEntity)

  def find(id: Long)(implicit client: Client): Future[Option[User]] =
    client.prepare("SELECT * FROM users WHERE id = ?")(id) map { result =>
      result.asInstanceOf[ResultSet].rows.map(convertToEntity).headOption
    }

  def create(email: String, screen_name: String)(implicit client: Client): Future[Long] =
    client.prepare("INSERT INTO users (email, screen_name) VALUES(?, ?)")(email, screen_name) map { result =>
      result.asInstanceOf[OK].insertId
    }

  def convertToEntity(row: Row): User = {
    val LongValue(id)            = row("id").get
    val StringValue(email)       = row("email").get
    val StringValue(screen_name) = row("screen_name").get

    User(id, email, screen_name)
  }
}
package jp.dakatsuka.finch

import com.twitter.finagle.Http
import com.twitter.finagle.exp.Mysql
import com.twitter.util.Await
import io.finch._
import io.finch.argonaut._

object Main {
  implicit val client = Mysql.client
    .withCredentials("user", "password")
    .withDatabase("database")
    .newRichClient("127.0.0.1:3306")

  case class UserParams(email: String, screen_name: String)

  val userParams: RequestReader[UserParams] = for {
    email <- param("email")
    screen_name <- param("screen_name")
  } yield UserParams(email, screen_name)

  val listUser: Endpoint[Seq[User]] = get("users") {
    Ok(User.all)
  }

  val showUser: Endpoint[User] = get("users" / long) { id: Long =>
    User.find(id).map {
      case Some(user) => Ok(user)
      case _ => NotFound(new Exception("Record Not Found"))
    }
  }

  val createUser: Endpoint[User] = post("users" ? userParams) { p: UserParams =>
    (for {
      id   <- User.create(p.email, p.screen_name)
      user <- User.find(id)
    } yield user) map {
      case Some(user) => Created(user)
      case _ => NotFound(new Exception("Record Not Found"))
    }
  }

  val userService = (listUser :+: showUser :+: createUser).toService

  def main(args: Array[String]): Unit = {
    Await.ready(Http.serve(":8080", userService))
  }
}

動かしてみよう

いつものコマンドでサーバが起動します。

$ sbt run
[info] Set current project to finchtest (in build file:/path/to/project/)
[info] Compiling 1 Scala source to /path/to/project/target/scala-2.11/classes...
[info] Running jp.dakatsuka.finch.Main
12 05, 2015 11:30:49 午後 com.twitter.finagle.Init$$anonfun$1 apply$mcV$sp
情報: Finagle version 6.30.0 (rev=745578b931893c432e51da623287144e548cc489) built at 20151015-163818

curlでリクエストを送ってみましょう。

$ curl http://localhost:8080/users
[]

$ curl http://localhost:8080/users/1
Record Not Found

$ curl -X POST http://localhost:8080/users
Required param 'email' not present in the request.

$ curl -X POST -d "email=user1@example.com" http://localhost:8080/users
Required param 'screen_name' not present in the request.

$ curl -X POST -d "email=user1@example.com" -d "screen_name=user1" http://localhost:8080/users
{"id":1,"email":"user1@example.com","screen_name":"user1"}

$ curl -X POST -d "email=user2@example.com" -d "screen_name=user2" http://localhost:8080/users
{"id":2,"email":"user2@example.com","screen_name":"user2"}

$ curl http://localhost:8080/users
[{"id":1,"email":"user1@example.com","screen_name":"user1"},{"id":2,"email":"user2@example.com","screen_name":"user2"}]

$ curl http://localhost:8080/users/1
{"id":1,"email":"user1@example.com","screen_name":"user1"}

期待通りの動きですね 😏

まとめ

Finchはいかがでしたか?

Finagleのエコシステムの上に乗っているので、Finagleに慣れている人がサクッとREST APIを実装するには良い選択肢だと思います。finagle-*なライブラリも当然使えます。Finagle使ったことない人にも、静的型付けの恩恵を受けながらSinatraライクに開発出来るというのは魅力的に映るかもしれませんね。

個人的にはエンドポイントの戻り値が型で保証出来るのが高ポイントです!

ちなみに、似たようなフレームワークとしてFinagle, Akka HTTP, Scalatra, http4sなどがあります。http4sはScalaのHTTPインターフェース http4s 超入門で雑に紹介しているので、興味ある方はどうぞ。

  1. Scalaライブラリにはよくあることなので頑張って貢献したい…