ScalaとSlickに慣れるためにPlayFrameworkを使ってTODOアプリを作ってみました。ソースはGitHubにあります。主にDocumentationとtakuya0301さんの記事を参考にしました。
目的
- TODOの登録、更新、削除ができるアプリを作る
- Scalaに慣れる
- Slickに慣れる
環境
- PlayFramework2.5
- Scala2.11
- H2DB
設定
まずSlick、Evolutions、H2DB関連のライブラリを取得します。build.sbtに以下を追記してreloadとupdateをします。このときjdbcがlibraryDependenciesに定義してある場合は削除してください、Slickの邪魔になるようです。
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ファイルを置きます。
# 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に設定してください。
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を定義することができるようです。
package models
case class Todo(id: Long, content: String)
続いてDBにアクセスするDaoクラスを以下のように作ります。メソッドにはTODOの全検索、登録、更新、削除を作りました。登録ではIDをオートインクリメントするようにしています。
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から削除)
@(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時にはアクションの種類によって処理を分岐させます。
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, ""))
}
ルーティング設定も忘れないように。
GET / controllers.IndexController.get
POST / controllers.IndexController.post
動作確認
ここまで来たらサーバを起動して実際に動作確認をします。
テキストボックスが表示されましたね。いくつか登録してみたいと思います。
"掃除"から"ラジオ体操"で更新成功しました。次は"ラジオ体操"を削除します。
削除に成功しました。
最後に
正直Daoに関しては参考記事に習って作っているので全て理解しておらず、まだ課題があります。Slickを使ってみて思ったのは、TableごとにDaoを用意するのが面倒だなと感じました。JavaのEbeanみたいなORマッパーがないのはなぜなのだろう。