Edited at

PlayFrameworkでTODOアプリ

More than 1 year has passed since last update.

ScalaとSlickに慣れるためにPlayFrameworkを使ってTODOアプリを作ってみました。ソースはGitHubにあります。主にDocumentationtakuya0301さんの記事を参考にしました。


目的


  • 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ファイルを置きます。


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を定義することができるようです。


Models.scala

package models

case class Todo(id: Long, content: String)


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


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から削除)


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時にはアクションの種類によって処理を分岐させます。


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


動作確認

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

スクリーンショット 2017-05-06 22.52.28.png

テキストボックスが表示されましたね。いくつか登録してみたいと思います。

スクリーンショット 2017-05-06 22.53.17.png

登録できました。次は更新してみます。

スクリーンショット 2017-05-06 22.55.52.png

"掃除"から"ラジオ体操"で更新成功しました。次は"ラジオ体操"を削除します。

スクリーンショット 2017-05-06 22.56.19.png

削除に成功しました。


最後に

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