play-logo

こんにちは、荒木です。
今回は Play Framework と MySQL を使って、CRUD(※) で RESTful な JSON API の作り方をご紹介します。

※CRUD とはCreate(生成)、Read(読み取り)、Update(更新)、Delete(削除)の機能のこと。

環境準備

Java

Play Framework には Java 1.8 が必要です。Java のバージョンは次のコマンドを実行して確認しましょう。

java -version

JDK がない場合は、OracleのJDKサイト からインストールする必要があります。

sbt で Play Frameworkをインストールする

  • sbt は、Scala製のビルドツールです。今回は sbt から Play Framework をインストールします。

Mac の場合

  • 以下のコマンドでインストール。
brew install sbt 

Windows の場合

  • Windows の方は、ネイティブで開発する場合は Chocolateyか、Windows のインストーラーを使うか、2通りあります。
iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex
choco install sbt
sbt v0.13.13.1
  • あるいは VirtualBox をインストールして、Vagrant で、Linux をインストールして、sbt をインストールしてもOKです。

sbtのバージョン

  • sbt のバージョンは最新の 0.13系以上。以下のコマンドで、sbt のバージョンを確認できます。
sbt sbt-version 

ProjectName: PlayScalaIntro
Project location: ~/github/PlayScalaSeed
Project SDK: java version 1.8 (最新版)
TemplateにPlay Scala Intro

を選んで Finish!

4

このときは、まだ sbt run をしておらず template のソースが全部ダウンロードされてないので、sbt run を以下のようにして実行します。

  • 下三角▽ボタンを選択して、Edit Configuration を選択。
    5

  • または、Run メニューから Edit Configuration を選択。
    6

  • + を選択。
    7

  • SBT Task を選択。
    8

  • 以下の図のように設定して、OK を選択。
    9

  • 下の緑の三角▶︎ボタンを選択。
    10

  • または、Run メニューから、Run 'sbt run' を選択すると、PlayScalaSeed プロジェクトのファイルがダウンンロードされ、ビルドして、実行される。

11

ダウンロードされてこなかったら、いったん IntelliJ を再起動してください。

12

ビルドが終わるまで待ちましょう。

13

2重に sbt run していると、同じ POST がかぶって1つしか起動しないので、もう一つを右クリックします。赤い ■ で Stop して、Close Tab で閉じましょう。

14

『Command+F2 で Stop』、『Command+R で Run』なので、覚えておくと便利です。
うまく1つだけ sbt run できたら、以下のようになります。

15

"Run -> Edit Configuration" の sbt run の設定にて、"Before launch: Build, Activate tool window" の Build を「-」ボタンで削除して sbt run を走らせると、sbt run する前に古い target フォルダを build せず、新しくbuild フォルダに生成されるので、うまくいきます。

  • 変更前
    17

  • 変更後
    18

本番は、実行を早くするために "sbt sbt-assembly" などで jar ファイルに固め、java でjar ファイルを実行します。"sbt clean" は、"sbt run" したときに target フォルダ以下の java のソースが作られる target を削除してくれます。
"sbt run" がうまくいくと下のような画面になります。

19

  • 動かすためには、下の"Apply this script now!"の赤いボタンを押すと、

20

  • h2データーベースに初期データが入って、以下のような画面に切り替わります。

21

  • h2データベースの設定は、"conf/application.conf"の一番下に書いてあります。

  • conf/application.conf

slick.dbs.default.driver="slick.driver.H2Driver$" # Slickドライバーの設定
slick.dbs.default.db.driver="org.h2.Driver" # Slickのバックエンドで使用されるJDBCドライバーの設定
slick.dbs.default.db.url="jdbc:h2:mem:play;DB_CLOSE_DELAY=-1" # Slickのバックエンドで使用されるJDBCのDatabase URLの設定

最初はh2データーベースには何もデータはありませんが、"Apply this script now!" の赤いボタンを押した際、
"conf/evolutions.default/1.sql" のスクリプトが実行され、初期データが投入されたので、正常な画面に遷移できた・・・というわけです。

  • conf/evolutions.default/1.sql
# --- !Ups

create table "people" (
  "id" bigint generated by default as identity(start with 1) not null primary key,
  "name" varchar not null,
  "age" int not null
);

# --- !Downs

drop table "people" if exists;

仕組みとしては、Rails と似たような感じで、"http://localhost:9000/" にアクセスすると、"conf/routes" のルーティングファイルから "/" のリクエストに GET マッチするコントローラの関数を呼び出します。
以下の、"conf/routes" ファイルを見てみると、

  • /のパスに、GET リクエストでマッチするものは、controllers パッケージの PersonController オブジェクトの index 関数を呼び出しています。
  • /person のパスに、POST リクエストでマッチするものは、controllers パッケージの PersonController オブジェクトの addPerson 関数を呼び出しています。
  • /persons のパスに、GET リクエストでマッチするものは、controllers パッケージの PersonController オブジェクトの getPersons 関数を呼び出しています。

  • conf/routes

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /                           controllers.PersonController.index
POST    /person                     controllers.PersonController.addPerson
GET     /persons                    controllers.PersonController.getPersons

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)
  • app/controllers/PersonController.scala
package controllers

import play.api._
import play.api.mvc._
import play.api.i18n._
import play.api.data.Form
import play.api.data.Forms._
import play.api.data.validation.Constraints._
import play.api.libs.json.Json
import models._
import dal._

import scala.concurrent.{ ExecutionContext, Future }

import javax.inject._

class PersonController @Inject() (repo: PersonRepository, val messagesApi: MessagesApi)
                                 (implicit ec: ExecutionContext) extends Controller with I18nSupport{

  /**
   * The mapping for the person form.
   */
  val personForm: Form[CreatePersonForm] = Form {
    mapping(
      "name" -> nonEmptyText,
      "age" -> number.verifying(min(0), max(140))
    )(CreatePersonForm.apply)(CreatePersonForm.unapply)
  }

  /**
   * The index action.
   */
  def index = Action {
    Ok(views.html.index(personForm))
  }

  /**
   * The add person action.
   *
   * This is asynchronous, since we're invoking the asynchronous methods on PersonRepository.
   */
  def addPerson = Action.async { implicit request =>
    // Bind the form first, then fold the result, passing a function to handle errors, and a function to handle succes.
    personForm.bindFromRequest.fold(
      // The error function. We return the index page with the error form, which will render the errors.
      // We also wrap the result in a successful future, since this action is synchronous, but we're required to return
      // a future because the person creation function returns a future.
      errorForm => {
        Future.successful(Ok(views.html.index(errorForm)))
      },
      // There were no errors in the from, so create the person.
      person => {
        repo.create(person.name, person.age).map { _ =>
          // If successful, we simply redirect to the index page.
          Redirect(routes.PersonController.index)
        }
      }
    )
  }

  /**
   * A REST endpoint that gets all the people as JSON.
   */
  def getPersons = Action.async {
    repo.list().map { people =>
      Ok(Json.toJson(people))
    }
  }
}

/**
 * The create person form.
 *
 * Generally for forms, you should define separate objects to your models, since forms very often need to present data
 * in a different way to your models.  In this case, it doesn't make sense to have an id parameter in the form, since
 * that is generated once it's created.
 */
case class CreatePersonForm(name: String, age: Int)

"http://localhost:9000/" にアクセスすると、"conf/routes" のルーティングファイルから "/" のパスへ GET リクエストにマッチするのは、controllers パッケージの PersonController オブジェクトの index 関数を呼び出しているので、index 関数を見てみましょう。

  • app/controllers/PersonController.scala
 // 省略
  def index = Action {
    Ok(views.html.index(personForm))
  }
 // 省略

index 関数は、Action 関数としてOk 関数を呼び出しています。
(『Command+B』で定義元をジャンプ / 『Command+Shift+[』で、戻る)
Ok 関数は views.html.index にて、以下のテンプレートファイル "app/views/index.scala.html" からビルド時に生成された "target/scala-2.11/twirl/main/views/html/index.template.scala" を呼び出し、引数のpersonに personForm を渡します。
personFormは、『Command+B』で定義元ジャンプすると、"app/controller/PersonController.scala" で定義されていることがわかります。

  • app/controller/PersonController.scala
  // 省略
  val personForm: Form[CreatePersonForm] = Form {
    mapping(
      "name" -> nonEmptyText,
      "age" -> number.verifying(min(0), max(140))
    )(CreatePersonForm.apply)(CreatePersonForm.unapply)
  }
  // 省略
  • app/views/index.scala.html
@(person: Form[CreatePersonForm])(implicit messages: Messages)

@import helper._

@main("Welcome to Play") {


    <ul id="persons"></ul>

  @form(routes.PersonController.addPerson()) {
        @inputText(person("name"))
        @inputText(person("age"))

        <div class="buttons">

        </div>
    }
}

Play Framework の Twirl というテンプレートエンジンを使っていて、Twirl では @ は特殊文字で、html の中で Scala が使うことができます。
この @main で、"app/views/main.scala.html"のテンプレートから生成された "target/scala-2.11/twirl/main/views/html/main.template.scala" を呼び出します。

"app/views/main.scala.html" では、『Command+B』で、定義元にジャンプできませんが、そのファイルから生成された、"target/scala-2.11/twirl/main/views/html/index.template.scala" では、『Command+B』で定義元にジャンプできます。

以下の、@(title: String)(content: Html) の第一引数に "Welcome to Play" が渡され、@main のクロージャーの返り値が、第二引数の content に渡されます。@form と @inputText は、@import helper._ で、インポートされた関数を呼び出しています。

つまり、view.html.helper.form の関数と、view.html.helper.inputText の関数で定義されているものを呼び出しているのです。

@(title: String)(content: Html)





        <title>@title</title>





        @content


main.scala.html の template で作成された html は、Ok 関数の引数になり表示されます。『command+B』を押すと定義元を確認できます。

  • Results.scala
  /** Generates a ‘200 OK’ result. */
  val Ok = new Status(OK)
  • Ok 関数は、Status のクラスの Object になっています。
  def index = Action {
    Ok(views.html.index(personForm))
  }

上記のように、"views.html.index(views.html.index(personForm))" という引数を渡されており、Object の apply 関数の引数に渡しています( apply 関数は省略できるため)。
"new Status(OK)" の Status オブジェクトは apply 関数の引数に渡されます。

Results.scala

  class Status(status: Int) extends Result(header = ResponseHeader(status), body = HttpEntity.NoEntity) {

    /**
     * Set the result's content.
     *
     * @param content The content to send.
     */
    def apply[C](content: C)(implicit writeable: Writeable[C]): Result = {
      Result(
        header,
        writeable.toEntity(content)
      )
    }

OK 関数も Command+B で定義元ジャンプすると 200 であり、Status クラスの第一引数にステータス 200 を渡していることがわかります。

  • StandardValues.scala
trait Status {

  val CONTINUE = 100
  val SWITCHING_PROTOCOLS = 101

  val OK = 200
  • Play Frameworkの主な構成は以下の通りです。
app                      → アプリケーションソース
 └ assets                → コンパイルされたソース
    └ stylesheets        → 通常はLESS CSSのソース
    └ javascripts        → 通常はCoffeeScriptのソース
 └ controllers           → アプリケーションコントローラー
 └ models                → アプリケーションビジネス層
 └ views                 → テンプレート
build.sbt                → アプリケーションビルドスクリプト
conf                     →  設定ファイル、および (クラスパス上の) その他のコンパイルされないリソース
 └ application.conf      → メイン設定ファイル
 └ routes                → ルート定義
dist                     → プロジェクト成果物に含める任意のファイル
public                   → 公開アセット
 └ stylesheets           → CSSファイル
 └ javascripts           → Javascriptファイル
 └ images                → 画像ファイル
project                  → sbt 設定ファイル群
 └ build.properties      → sbt プロジェクトの目印
 └ plugins.sbt           → Play 自身の定義を含む sbt プラグイン
lib                      → 管理されていない依存ライブラリ
logs                     → ログフォルダ
 └ application.log       → デフォルトログファイル
target                   → 生成物
 └ resolution-cache      → 依存性に関する情報
 └ scala-2.11
    └ api                → 生成された API ドキュメント
    └ classes            → コンパイルされたクラスファイル
    └ routes             → routesから生成されたソース
    └ twirl              → テンプレートから生成されたソース
 └ universal             → アプリケーションパッケージ
 └ web                   → コンパイルされた web アセット
test                     → 単体、および機能テスト用のソースフォルダ

ここまでできたら、git にコミットして、バックアップをとっておきましょう。

git init
git add -A
git commit -m "create repository by PlayScalaIntro"

その2へ続く。