1. AKB428

    No comment

    AKB428
Changes in tags
Changes in body
Source | HTML | Preview
@@ -1,273 +1,273 @@
Scalaの勉強もかねてPlay FrameworkでDBを利用したREST APIサーバーを作ってみました。
自分がつまずいたポイントを記述しておきます。
## ソースコード
https://github.com/Project-ShangriLa/sora-playframework-scala
## システム環境
-* ローカル開発環境: MaxOSX + MySQL5.5
+* ローカル開発環境: 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ぐらいひっぱってくるのはちょっと気持ち悪い。