PlayFrameworkでTODOアプリ

ScalaとSlickに慣れるためにPlayFrameworkを使ってTODOアプリを作ってみました。ソースは[GitHub](https://github.com/hys-rabbit/TodoApp)にあります。主に[Documentation](https://www.playframework.com/documentation/2.5.x/PlaySlick)と[takuya0301さんの記事](http://qiita.com/takuya0301/items/5f3e535bb00a208699a2)を参考にしました。

# 目的
- TODOの登録、更新、削除ができるアプリを作る
- Scalaに慣れる
- Slickに慣れる　

# 環境
- PlayFramework2.5
- Scala2.11
- H2DB

# 設定
まずSlick、Evolutions、H2DB関連のライブラリを取得します。build.sbtに以下を追記してreloadとupdateをします。このときjdbcがlibraryDependenciesに定義してある場合は削除してください、Slickの邪魔になるようです。

```:build.sbt
libraryDependencies ++= Seq(
  "com.h2database" % "h2" % "1.4.191",
  "com.typesafe.play" %% "play-slick" % "2.0.0",
  "com.typesafe.play" %% "play-slick-evolutions" % "2.0.0"
)
```

次はTODOを格納するテーブルのスキーマファイルを作ります。今回は入力されたTODOをIDで管理するだけのTODOテーブルを作成します。conf/evolutions/defaultディレクトリを作り、以下のSQLファイルを置きます。

```sql:conf/evolutions/default/1.sql
# Todos schema

# --- !Ups

CREATE TABLE Todo (
  id bigint NOT NULL AUTO_INCREMENT,
  content varchar(255) NOT NULL,
  PRIMARY KEY (id)
);

# --- !Downs

drop table if exists Todo;

```

application.confにDB接続の設定をします。以下を追記してください。applyEvolutions.defaultをtrueにしていますが、サーバ起動の度にconf/evolutions/default配下のSQLでDBを初期化するので、実DBに接続させる場合はfalseに設定してください。

```:conf/application.conf
slick.dbs.default {
  driver="slick.driver.H2Driver$"
  db.driver="org.h2.Driver"
  db.url="jdbc:h2:mem:play;MODE=MYSQL;DB_CLOSE_DELAY=-1"
  db.user=sa
  db.password=""
}

applyEvolutions.default=true # H2DBなので
```
# ModelとDao
DB情報を格納するModelとDBアクセスを行うDaoを作っていきたいと思います。Todoテーブル情報を格納するためのModelは以下のようになります。Scalaの場合case classを使えば簡単にModelを定義することができるようです。

```scala:Models.scala
package models

case class Todo(id: Long, content: String)
```
続いてDBにアクセスするDaoクラスを以下のように作ります。メソッドにはTODOの全検索、登録、更新、削除を作りました。登録ではIDをオートインクリメントするようにしています。

```scala:TodoDao.scala
package daos

import javax.inject.Inject

import models.Todo
import play.api.db.slick.{ DatabaseConfigProvider, HasDatabaseConfigProvider }
import slick.driver.JdbcProfile

import scala.concurrent.{ ExecutionContext, Future }

class TodoDao @Inject() (protected val dbConfigProvider: DatabaseConfigProvider)(implicit executionContext: ExecutionContext) extends HasDatabaseConfigProvider[JdbcProfile] {
  import driver.api._

  private val Todos = TableQuery[TodosTable]

  /** 全検索 */
  def all(): Future[Seq[Todo]] = db.run(Todos.result)

  /** 登録 */
  def insert(todo: Todo): Future[Unit] = {
    val todos = Todos returning Todos.map(_.id) into ((todo, id) => todo.copy(id = id)) += todo
    db.run(todos.transactionally).map(_ => ())
  }

  /** 更新 */
  def update(todo: Todo): Future[Unit] = {
    db.run(Todos.filter(_.id === todo.id).map(_.content).update(todo.content)).map(_ => ())
  }

  /** 削除 */
  def delete(todo: Todo): Future[Unit] = {
    db.run(Todos.filter(_.id === todo.id).delete).map(_ => ())
  }

  /** マッピング */
  private class TodosTable(tag: Tag) extends Table[Todo](tag, "TODO") {
    def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
    def content = column[String]("CONTENT")
    def * = (id, content) <> (Todo.tupled, Todo.unapply _)
  }

}
```

# View
画面には以下の部品を設置します。画面上部にはTODOを入力するテキストボックスと、登録ボタンを設置。画面下部にはTODOの検索結果を登録順に表示し、それぞれ更新ボタンと削除ボタンを設置します。

- テキストボックス（画面に一つ設置）
- 登録ボタン（画面に一つ設置 テキストボックスに入力された文字列をDBに登録）
- 更新ボタン（レコード単位に設置 テキストボックスに入力された文字列で更新）
- 削除ボタン（レコード単位に設置 DBから削除）

```html:index.scala.html
@(todoForm: Form[TodoForm], list: Seq[models.Todo])(implicit messages:Messages)

@main("Todo App") {

    @helper.form(action = routes.IndexController.post){

        @helper.inputText(todoForm("content"), '_label -> "登録内容")

        <button name="action" value="insert" >登録</button>

        <br />

        @if(!list.isEmpty){
        <table>
            <thead><tr>
                <th>No</th>
                <th>TODO</th>
                <th></th>
                <th></th>
            </tr></thead>
            <tbody>
                @for((todo, i) <- list.zipWithIndex){
                <tr>
                    <td>@(i+1)</td>
                    <td>@todo.content</td>
                    <td><button name="action" value="update:@todo.id">更新</button></td>
                    <td><button name="action" value="delete:@todo.id">削除</button></td>
                </tr>
                }
            </tbody>
        </table>
        }

    }

}
```

# Controller
最後に画面アクションをコントロールするクラスを作ります。GETとPOSTの二つのメソッドを作り、POST時にはアクションの種類によって処理を分岐させます。

```scala:IndexController.scala
package controllers

import scala.concurrent.ExecutionContext.Implicits.global

import daos.TodoDao
import javax.inject.{ Inject, Singleton }
import play.api.data.Form
import play.api.data.Forms.{ mapping, text }
import play.api.i18n.{ I18nSupport, MessagesApi }
import play.api.mvc.{ Action, Controller }

case class TodoForm(action: String, content: String)

@Singleton
class IndexController @Inject() (val todoDao: TodoDao, val messagesApi: MessagesApi) extends Controller with I18nSupport {

  val todoForm = Form(
    mapping(
      "action" -> text,
      "content" -> text
    )(TodoForm.apply)(TodoForm.unapply))

  /**
   * 初期表示(GET)
   */
  def get = Action.async {
    todoDao.all().map(todos => Ok(views.html.index(todoForm, todos)))
  }

  /**
   * アクション(POST)
   */
  def post = Action.async { implicit request =>
    todoForm.bindFromRequest.fold(
      formWithErrors => {
        todoDao.all().map(todos => Ok(views.html.index(formWithErrors, todos)))
      },
      todoData => {
        val action = todoData.action.split(":")
        action(0) match {
          case "insert" => insert(todoData)
          case "update" => update(action(1), todoData)
          case "delete" => delete(action(1))
          case _ => println("No Action!!")
        }
        todoDao.all().map(todos => Ok(views.html.index(todoForm.fill(todoData), todos)))
      })
  }

  /** 登録 */
  def insert(todoForm: TodoForm) = todoDao.insert(Todo(0, todoForm.content))

  /** 更新 */
  def update(id: String, todoForm: TodoForm) = todoDao.update(Todo(id.toLong, todoForm.content))

  /** 削除 */
  def delete(id: String) = todoDao.delete(Todo(id.toLong, ""))

}

```
ルーティング設定も忘れないように。

```:routes
GET     /                           controllers.IndexController.get
POST    /                           controllers.IndexController.post
```

# 動作確認
ここまで来たらサーバを起動して実際に動作確認をします。

<img width="239" alt="スクリーンショット 2017-05-06 22.52.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/169092/e0c814c5-e0df-4c7d-da01-b75ddeed4d6b.png">

テキストボックスが表示されましたね。いくつか登録してみたいと思います。
<img width="219" alt="スクリーンショット 2017-05-06 22.53.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/169092/d2b447bb-e977-bb27-269d-8b11e0a05a29.png">

登録できました。次は更新してみます。
<img width="222" alt="スクリーンショット 2017-05-06 22.55.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/169092/b9a5e338-35de-c793-b60e-94a71a2025ea.png">

"掃除"から"ラジオ体操"で更新成功しました。次は"ラジオ体操"を削除します。
<img width="210" alt="スクリーンショット 2017-05-06 22.56.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/169092/b79477f7-93af-b147-63c4-403e58019a3c.png">

削除に成功しました。

# 最後に
正直Daoに関しては参考記事に習って作っているので全て理解しておらず、まだ課題があります。Slickを使ってみて思ったのは、TableごとにDaoを用意するのが面倒だなと感じました。JavaのEbeanみたいなORマッパーがないのはなぜなのだろう。