LoginSignup
10
10

More than 5 years have passed since last update.

PlayFrameworkでTODOアプリ

Last updated at Posted at 2017-05-06

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マッパーがないのはなぜなのだろう。

10
10
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10