(60分クッキング) Play Framework (Scala)+MySQLでREST APIサーバーを作る

Scalaの勉強もかねてPlay FrameworkでDBを利用したREST APIサーバーを作ってみました。
自分がつまずいたポイントを記述しておきます。

## ソースコード

https://github.com/Project-ShangriLa/sora-playframework-scala

## システム環境
* ローカル開発環境: MacOSX + MySQL5.5
* 本番環境: CenOS6 + MariaDB5.5
* Play Framework 2.3
* JDK8 (Scala 2.11ではJDK8はテストサポート扱いらしい 標準サポートは JDK6 <= JDK7 )
* Scala 2.11  
* IDE: IntelliJ IDEA 14 Ultimate + Scalaプラグイン (Ultimateは有償だがお薦め)

## データベースで管理するもの

* 2014〜2015年で放送されているアニメ作品を管理しているデータベース

## インストール&プロジェクト作成

最新のPlay Frameworkではactivator経由でインストールすることになっているので公式サイトからactivatorをダウンロードしインストールしておく。
https://www.playframework.com/

インストールが終わったらプロジェクトを作成する(Scalaプロジェクトを選択)

## build.sbtにMySQLドライバを追加

```
name := """sora_scala"""

version := "1.0-SNAPSHOT"

lazy val root = (project in file(".")).enablePlugins(PlayScala)

scalaVersion := "2.11.1"

libraryDependencies ++= Seq(
  jdbc,
  anorm,
  cache,
  ws,
  "mysql" % "mysql-connector-java" % "5.1.35"
)
```

Seq( の所に　"mysql" % "mysql-connector-java" % "5.1.35"　を追加

## conf/application.conf に DB接続設定を追加

```
db.default.driver=com.mysql.jdbc.Driver
db.default.url="jdbc:mysql://localhost/anime_admin_development?characterEncoding=UTF8"
db.default.user="root"
db.default.password=""
```

構築してあるMySQL or MariaDBの設定を記述する。
anime_admin_development は　データベース名


## コントローラーを実装する

今回はREST APIサーバーなので返却値はJSONになります。
つまりDBの値からJSONにコンバートするまでの処理を記述することになります。

### だいたいの処理フロー

1. URLからパラメーターを取得する場合はパラメーターから値を取得
2. AnormというDBライブラリを使いselect SQLを実行　
3. 結果をJSON型に変換
4. Ok関数に結果を格納しレスポンスとして返却

### 1テーブルを取得しJSON Map化してレスポンスを返す

```scala:controllers/AnimeV1.scala
package controllers

import java.util.Date

import anorm._
import play.api.Play.current
import play.api.db._
import play.api.libs.json.{JsString, JsNumber, Json}
import play.api.mvc._
import play.api.Logger

object AnimeV1 extends Controller {

  def masterList = Action {
    DB.withConnection { implicit c =>
      val records = SQL("SELECT * FROM cours_infos ORDER BY id")().map {
        row => (row[Int]("id").toString,
          Map("id" -> row[Int]("id"), "year" -> row[Int]("year"), "cours" -> row[Int]("cours")))
      }.toMap

      Ok(Json.toJson(records))
    }
  }
```

JSONの一階層をMapで返したい場合はtoMap、配列で返したい場合はtoListにする。
今回はMapで返すのでtoMapにしている。
DBのレコードのPK-IDをMapのキーとしてバリューをレコードデータにしている。
以下のようなデータが返却される。

```
{
  "4": {
    "id": 4,
    "year": 2014,
    "cours": 4
  },
  "5": {
    "id": 5,
    "year": 2015,
    "cours": 1
  }
}
```

ルーティングは引数をとらないのでシンプル

```:conf/routes
GET        /anime/v1/master/cours  controllers.AnimeV1.masterList
```




### 2テーブルを結合しJSON Listでレスポンスを返す

```scala:controllers/AnimeV1.scala
 def year(year_num: String) = Action {
    DB.withConnection { implicit c =>
      val cours_infos_records = SQL("SELECT * FROM cours_infos WHERE YEAR = {year_num} ORDER BY id").on("year_num" -> year_num)().map {
        row => row[Int]("id")
      }.toList
      Logger.debug(cours_infos_records.toString())
      if (cours_infos_records.size == 0) {
        Logger.warn(s"Execute no data request year=$year_num")
        Ok(Json.toJson(cours_infos_records))
      }
      else {

        val seq_cours_infos_records: Seq[Int] = cours_infos_records

        val bases_records = SQL("SELECT * FROM bases WHERE cours_id IN ({IDS}) ORDER BY id").on("IDS" -> seq_cours_infos_records)().map {
          row =>
            Map(
              "id" -> JsNumber(row[Int]("id")),
              "title" -> JsString(row[String]("title")))
        }.toList

        Ok(Json.toJson(bases_records))
      }
    }
  }
```

#### ポイント1 メソッドに引数を渡す

year_numをメソッドの引数として受け取るように実装しています。
この引数はルーティングの設定をすると受け取れるようになるのでコントローラー側では特にパースは不要です。

#### ポイント2 scalaにreturnはありません

returnはrubyのように省略できるというわけではなく、scalaに記法自体が存在しません。
今回2テーブルをJOINせず、2回SELECTしています。
1回目のSELECTの結果が0件だった場合、そこで処理をやめたいのですがreturnがありません。


#### ポイント3 PlayのOkメソッドはそこで処理が終わるわけではない

OK(hogehoge)でレスポンスを返すのがPlayフレームワークなのですが、別にOKがreturnの役目を担ってるわけではありません。
一度目のSELECTの結果が0だったらOK(blankJSON)を実行し空のJSON配列を返却しても後続のっ処理は実行されます。
今回は面倒だったのでif ~ else で済ませましたがcase文などもっとスマートな記法はあるようです。

### ポイント4 JSONの値がStringやNumberなど混合する場合はJsXXXXで変換する必要がある
今回返すレスポンスは以下のJSONです

```
[
  {
    "id": 187,
    "title": "魔法少女リリカルなのはViVid"
  },
  {
    "id": 188,
    "title": "やはり俺の青春ラブコメはまちがっている。続"
  }
]
```

idは数値、titleは文字列で、結果の型が混合になっています。
この場合JsXXXXメソッド(JsNumberやJsString)でDBの値を変換しないとエラーがでます。

ルーティングは引数をとる設定がミソ

```:conf/routes
GET /anime/v1/master/$year_num<[0-9]{4}+>  controllers.AnimeV1.year(year_num)
```

URLには正規表現が使える、今回引数には西暦を渡すようにしているので4桁の数値とした。
こうすると4桁の数値以外は受け付けなくなる。

### DBのselect結果が複雑な場合

```scala:controllers/AnimeV1.scala
  def yearCours(year_num: String, cours: String) = Action {
    DB.withConnection { implicit c =>
      val cours_infos_records = SQL("SELECT * FROM cours_infos WHERE YEAR = {year_num} AND cours = {cours} ").
        on("year_num" -> year_num, "cours" -> cours)().map {
        row => row[Int]("id")
      }.toList
      Logger.debug(cours_infos_records.toString())

      if (cours_infos_records.size == 0) {
        Logger.warn(s"Execute no data request year=$year_num cours=$cours")
        Ok(Json.toJson(cours_infos_records))
      }
      else {

        val seq_cours_infos_records: Seq[Int] = cours_infos_records

        val bases_records = SQL("SELECT * FROM bases WHERE COURS_ID IN ({IDS}) ORDER BY ID").on("IDS" -> seq_cours_infos_records)().map {
          row =>
            Map(
              "id" -> JsNumber(row[Int]("id")),
              "title" -> JsString(row[String]("title")),
              "title_short1" -> JsString(row[String]("title_short1")),
              "title_short2" -> JsString(row[String]("title_short2")),
              "title_short3" -> JsString(row[String]("title_short3")),
              "public_url" -> JsString(row[String]("public_url")),
              "twitter_account" -> JsString(row[String]("twitter_account")),
              "twitter_hash_tag" -> JsString(row[String]("twitter_hash_tag")),
              "cours_id" -> JsNumber(row[Int]("cours_id")),
              "sex" -> JsNumber(BigDecimal(row[Option[Int]]("sex").getOrElse(0))),
              "sequel" -> JsNumber(BigDecimal(row[Option[Int]]("sequel").getOrElse(0))),
              "created_at" -> JsString(row[Date]("created_at").toString),
              "updated_at" -> JsString(row[Date]("updated_at").toString)
            )
        }.toList

        Ok(Json.toJson(bases_records))
      }
    }
  }
```

### ポイント5 カラムにNULLを許容している場合の対応

カラムのsexとsequelはNULLを許容しているためNULLが返却されるとAnormがIntに変換しようとしてエラーになります。
これを防ぐにはOptionをかぶせる必要があります。
また、JsNumberはBigDecimalを引数にとるのでクラスを作成するようラップしています。


### ポイント6 日付型の対応

おなじみのcreated_at updated_at　ですがJSONで返す場合はUNIXTIMEにするか、Stringにするかなどあると思いますが今回はStringで返します。
DBの型クラスはDateとし、import java.util.Dateを加える必要があります。



## Play Framework + ScalaでREST APIサーバー作ってみて所感

* 一度REST APIサーバー作るとマイテンプレートとして使えるし入門には最適だと思う
* 慣れると本当に1時間でDB->JSONのRESTサーバーはバシバシ作れそう。
* DBライブラリのAnormが使いずらい。別のもあるらしいけど標準のライブラリ使いたいよね。
* Play Frameworkの日本語の情報少ない
* Play Framework自体の仕組みは素晴らしい、Java/Scalaベースとは思えないほど楽にアプリケーション・サーバーが作れる
* 手軽さではRuby Sinatraなどのスクリプト言語には負けるがJavaの資産を使いたい場合はPlay一択
* Activatorはいろいろjarライブラリを300Mぐらいひっぱってくるのはちょっと気持ち悪い。