この記事はウェブクルー Advent Calendar 2017の1日目の記事です。
はじめに
今年も Advent Calendar が始まりました。
1日目は @kouares がつとめます!!
1日目から長い内容ですが、どうぞお付き合いください。
概要
4月から PlayFramework, Scala を業務で使い始め、公式ドキュメントや先人の方々が書いた記事やコードにとても助けられました。本当にありがとうございます。
今回は私も PlayFramework, Slick に入門する方の助けになれればと思ったのと自身の振り返りも兼ねて、CRUDを実装したメモアプリ(Knowledgeのすごく簡単な感じのやつ)を作成する記事を作成しました。
単純な CRUD 以外に join や transaction 等も実装したので入門にしてはボリュームがありますが、その分 PlayFramework, Slick に入門する方の助けになると思いますので参考にして頂けたらと思います。
環境
- OS : MacOS
- JVM : Java8
- Scala : 2.12.3
- sbt : 1.0.3
- PlayFramework : 2.6.7
- Database : Mysql
テーブル構成
- Memo
- タイトル、メモ内容を格納しています。primary key が auto_increment になっています。
- TagMst
- メモに付けられるタグのマスタです。primary key が auto_increment になっています。
- TagMapping
- メモとタグのひも付きを格納しています。
プロジェクト作成
まずはプロジェクトを作成します。
Play2.5.x までは Typesafe activator を使っていましたが、Play2.6 では sbt を使います。
プロジェクトを作りたいディレクトリに移動して以下のコマンドを実行します。
sbt new playframework/play-scala-seed.g8
すると jar のダウンロードが始まり、最後に以下の内容を聞かれます。
特に変更しない場合は []
の中に表示されている内容でプロジェクトが作成されます。
name [play-scala-seed]: play_example
organization [com.example]:
play_version [2.6.7]:
sbt_version [1.0.2]:
scalatestplusplay_version [3.1.2]:
設定
依存するライブラリやビルドの設定等を行います。
ライブラリ依存性
DB の操作に Slick を使うので build.sbt に以下のライブラリ依存性を追加します。
libraryDependencies ++= Seq(
"com.typesafe.play" %% "play-slick" % "3.0.2",
"com.typesafe.slick" %% "slick-codegen" % "3.2.1",
"mysql" % "mysql-connector-java" % "5.1.42"
)
sbtプラグインの設定
eclipse を使って開発しましたので、以下の plugin を追加します。
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.3")
これで、sbt eclipse
が使えるようになり、eclipse にプロジェクトをインポート出来るようになります。
ビルドの設定
buildに使うsbtのバージョンを設定します。
sbt.version=1.0.2
がプロジェクト作成時に設定されていましたが、sbt 1.0.3
が出ているので1.0.3
に変更しました。
sbt.version=1.0.3
DB接続設定
slick で DB に接続する設定を application.conf
に書きます。
今回は接続するのに必要な設定のみですが、タイムアウトやコネクションの最大接続数なども設定できます。
slick.dbs {
default {
profile = "slick.jdbc.MySQLProfile$"
db {
driver = com.mysql.jdbc.Driver
url = "jdbc:mysql://localhost:3306/memo?characterEncoding=UTF-8"
user = memo
password = "memo"
}
}
}
Model と Dao
Model と Dao を作成します。
Model は build.sbt で設定した slick-codegen
を用いて生成します。
以下が slick-codegen
を使って Model を生成するscalaアプリケーションになります。
ModelのGenerator
package genarator
import scala.concurrent.Await
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration
import slick.codegen.SourceCodeGenerator
import slick.jdbc.meta.MTable
import slick.model.Model
object SlickModelGenerator extends App {
// 接続先
val url = "jdbc:mysql://localhost/memo"
// 出力するディレクトリ
val outputDir = "app"
// 出力するパッケージ
val pkg = "models"
// traitの名前
val topTraitName = "Tables"
// ファイル名
val scalaFileName = "Tables.scala"
// 生成するテーブルを指定、今回は全テーブルModelを作成するのでNone
val tableNames: Option[Seq[String]] = None
val slickProfile = "slick.jdbc.MySQLProfile"
val profile = slick.jdbc.MySQLProfile
val db = profile.api.Database.forURL(url, driver = "com.mysql.jdbc.Driver", user = "memo", password = "memo")
try {
import scala.concurrent.ExecutionContext.Implicits.global
val mTablesAction = MTable.getTables.map { _.map { mTable => mTable.copy(name = mTable.name.copy(catalog = None)) } }
val allModel = Await.result(db.run(profile.createModel(Some(mTablesAction), false)(ExecutionContext.global).withPinnedSession), Duration.Inf)
val modelFiltered = tableNames.fold(allModel) { tableNames =>
Model(tables = allModel.tables.filter { aTable =>
tableNames.contains(aTable.name.table)
})
}
new SourceCodeGeneratorEx(modelFiltered).writeToFile(slickProfile, outputDir, pkg, topTraitName, scalaFileName)
} finally db.close
class SourceCodeGeneratorEx(model: Model) extends SourceCodeGenerator(model) {
override def Table = new Table(_) {
//auto_incrementを識別できるようにする
//生成されるモデルはOption型になる
override def autoIncLastAsOption = true
override def Column = new Column(_) {
override def rawType = model.tpe match {
case "java.sql.Blob" =>
"Array[Byte]"
case _ =>
super.rawType
}
}
}
}
}
生成されたModel
package models
// AUTO-GENERATED Slick data model
/** Stand-alone Slick data model for immediate use */
object Tables extends {
val profile = slick.jdbc.MySQLProfile
} with Tables
/** Slick data model trait for extension, choice of backend or usage in the cake pattern. (Make sure to initialize this late.) */
trait Tables {
val profile: slick.jdbc.JdbcProfile
import profile.api._
import slick.model.ForeignKeyAction
// NOTE: GetResult mappers for plain SQL are only generated for tables where Slick knows how to map the types of all columns.
import slick.jdbc.{GetResult => GR}
/** DDL for all tables. Call .create to execute. */
lazy val schema: profile.SchemaDescription = Memo.schema ++ TagMapping.schema ++ TagMst.schema
@deprecated("Use .schema instead of .ddl", "3.0")
def ddl = schema
/** Entity class storing rows of table Memo
* @param title Database column title SqlType(VARCHAR), Length(200,true)
* @param mainText Database column main_text SqlType(VARCHAR), Length(10000,true), Default(None)
* @param upadtedAt Database column upadted_at SqlType(DATETIME), Default(None)
* @param createdAt Database column created_at SqlType(DATETIME)
* @param id Database column id SqlType(INT), AutoInc, PrimaryKey */
case class MemoRow(title: String, mainText: Option[String] = None, upadtedAt: Option[java.sql.Timestamp] = None, createdAt: java.sql.Timestamp, id: Option[Int] = None)
/** GetResult implicit for fetching MemoRow objects using plain SQL queries */
implicit def GetResultMemoRow(implicit e0: GR[String], e1: GR[Option[String]], e2: GR[Option[java.sql.Timestamp]], e3: GR[java.sql.Timestamp], e4: GR[Option[Int]]): GR[MemoRow] = GR{
prs => import prs._
val r = (<<?[Int], <<[String], <<?[String], <<?[java.sql.Timestamp], <<[java.sql.Timestamp])
import r._
MemoRow.tupled((_2, _3, _4, _5, _1)) // putting AutoInc last
}
/** Table description of table memo. Objects of this class serve as prototypes for rows in queries. */
class Memo(_tableTag: Tag) extends profile.api.Table[MemoRow](_tableTag, "memo") {
def * = (title, mainText, upadtedAt, createdAt, Rep.Some(id)) <> (MemoRow.tupled, MemoRow.unapply)
/** Maps whole row to an option. Useful for outer joins. */
def ? = (Rep.Some(title), mainText, upadtedAt, Rep.Some(createdAt), Rep.Some(id)).shaped.<>({r=>import r._; _1.map(_=> MemoRow.tupled((_1.get, _2, _3, _4.get, _5)))}, (_:Any) => throw new Exception("Inserting into ? projection not supported."))
/** Database column title SqlType(VARCHAR), Length(200,true) */
val title: Rep[String] = column[String]("title", O.Length(200,varying=true))
/** Database column main_text SqlType(VARCHAR), Length(10000,true), Default(None) */
val mainText: Rep[Option[String]] = column[Option[String]]("main_text", O.Length(10000,varying=true), O.Default(None))
/** Database column upadted_at SqlType(DATETIME), Default(None) */
val upadtedAt: Rep[Option[java.sql.Timestamp]] = column[Option[java.sql.Timestamp]]("upadted_at", O.Default(None))
/** Database column created_at SqlType(DATETIME) */
val createdAt: Rep[java.sql.Timestamp] = column[java.sql.Timestamp]("created_at")
/** Database column id SqlType(INT), AutoInc, PrimaryKey */
val id: Rep[Int] = column[Int]("id", O.AutoInc, O.PrimaryKey)
}
/** Collection-like TableQuery object for table Memo */
lazy val Memo = new TableQuery(tag => new Memo(tag))
/** Entity class storing rows of table TagMapping
* @param memoId Database column memo_id SqlType(INT), Default(None)
* @param tagId Database column tag_id SqlType(INT), Default(None) */
case class TagMappingRow(memoId: Option[Int] = None, tagId: Option[Int] = None)
/** GetResult implicit for fetching TagMappingRow objects using plain SQL queries */
implicit def GetResultTagMappingRow(implicit e0: GR[Option[Int]]): GR[TagMappingRow] = GR{
prs => import prs._
val r = (<<?[Int], <<?[Int])
import r._
TagMappingRow.tupled((_1, _2)) // putting AutoInc last
}
/** Table description of table tag_mapping. Objects of this class serve as prototypes for rows in queries. */
class TagMapping(_tableTag: Tag) extends profile.api.Table[TagMappingRow](_tableTag, "tag_mapping") {
def * = (memoId, tagId) <> (TagMappingRow.tupled, TagMappingRow.unapply)
/** Database column memo_id SqlType(INT), Default(None) */
val memoId: Rep[Option[Int]] = column[Option[Int]]("memo_id", O.Default(None))
/** Database column tag_id SqlType(INT), Default(None) */
val tagId: Rep[Option[Int]] = column[Option[Int]]("tag_id", O.Default(None))
/** Index over (memoId) (database name memo_id) */
val index1 = index("memo_id", memoId)
/** Index over (tagId) (database name tag_id) */
val index2 = index("tag_id", tagId)
}
/** Collection-like TableQuery object for table TagMapping */
lazy val TagMapping = new TableQuery(tag => new TagMapping(tag))
/** Entity class storing rows of table TagMst
* @param name Database column name SqlType(VARCHAR), Length(100,true), Default(None)
* @param id Database column id SqlType(INT), AutoInc, PrimaryKey */
case class TagMstRow(name: Option[String] = None, id: Option[Int] = None)
/** GetResult implicit for fetching TagMstRow objects using plain SQL queries */
implicit def GetResultTagMstRow(implicit e0: GR[Option[String]], e1: GR[Option[Int]]): GR[TagMstRow] = GR{
prs => import prs._
val r = (<<?[Int], <<?[String])
import r._
TagMstRow.tupled((_2, _1)) // putting AutoInc last
}
/** Table description of table tag_mst. Objects of this class serve as prototypes for rows in queries. */
class TagMst(_tableTag: Tag) extends profile.api.Table[TagMstRow](_tableTag, "tag_mst") {
def * = (name, Rep.Some(id)) <> (TagMstRow.tupled, TagMstRow.unapply)
/** Maps whole row to an option. Useful for outer joins. */
def ? = (name, Rep.Some(id)).shaped.<>({r=>import r._; _2.map(_=> TagMstRow.tupled((_1, _2)))}, (_:Any) => throw new Exception("Inserting into ? projection not supported."))
/** Database column name SqlType(VARCHAR), Length(100,true), Default(None) */
val name: Rep[Option[String]] = column[Option[String]]("name", O.Length(100,varying=true), O.Default(None))
/** Database column id SqlType(INT), AutoInc, PrimaryKey */
val id: Rep[Int] = column[Int]("id", O.AutoInc, O.PrimaryKey)
}
/** Collection-like TableQuery object for table TagMst */
lazy val TagMst = new TableQuery(tag => new TagMst(tag))
}
Dao
引き続き Dao を作成していきます。
Dao にはメモの検索、メモの id指定検索、登録、更新、削除を実装しました。
検索
検索条件の mainText が指定されていればメモを like 検索、指定されていなければ全検索します。
メモに付いているタグも取得します。
- 以下がコードで使っているslickのメソッドになります。
-
Memo.sortBy(_.id)
ORDER BY
に当たります。Memo テーブルを id の照準でソートしています。 -
Memo.filter
WHERE
に当たります。 -
(_.mainText like s"%${mainText}%"
LIKE
に当たります。
上の filter と合わせて LIKE検索を実装しています。 -
result.map
これは SQL に当たるものはありません。
クエリの実行結果を後続処理で使いやすいように変換を行っています。 -
join(TagMapping).on((m, t) => m.id === t.memoId)
INNER JOIN
に当たります。
Memo と TagMapping を結合しています。 -
.map(result => (result._1._1.id, result._2)).result
SELECT
に当たります。
ここまでで取得しているデータに射影操作を行っています。 -
db.run
DB 操作を実行します。
-
def search(mainText: Option[String]) = {
// クエリを作成
val query = for {
// 検索条件が指定されていれば条件を使って検索、指定されていないなら全件取得
memos <- mainText.fold(Memo.sortBy(_.id))(mainText => Memo.filter(_.mainText like s"%${mainText}%").sortBy(_.id)).result
.map(_.map(memo => MemoItem(memo.id.getOrElse(-1), memo.title, memo.mainText.getOrElse(""))))
// 取得したメモに紐づくタグを取得する
tags <- Memo.filter(_.id.inSetBind(memos.map(_.id)))
.join(TagMapping).on((m, t) => m.id === t.memoId)
.join(TagMst).on((mt, tm) => mt._2.tagId === tm.id)
.map(result => (result._1._1.id, result._2)).result
} yield memos.map(memo =>
MemoInfo(memo.id, memo.title, memo.mainText, tags.filter(tagMap => tagMap._1 == memo.id).map(_._2)))
db.run(query)
}
登録
入力内容を元にメモの登録、タグの登録、メモとタグのひも付きを登録します。
- 以下がコードで使っているslickのメソッドになります。
-
Memo returning Memo.map(_.id) += MemoRow(省略)
INSERT INTO
に当たります。
この実装以外にもINSERT INTO
を実装する方法はありますが、この実装だと登録したレコードの ID(AUTO INCREMENT
) を取得できます。 -
DBIO.sequence
Seq[DBIO[T]]
型をDBIO[Seq[T]]
型に変換します。
1テーブルに複数件のデータ登録を行う時などに使用します。発行しているクエリによりますが
for 式で展開することで登録された ID のリストなどを取得できます。 -
_.name.inSetBind(form.tags)
IN
に当たります。この場合はTraversable[String]
を引数にとります。 -
transactionally
トランザクションを生成します。
以下のコードだと for 式の中で発行しているクエリが一つのトランザクションになります。
-
def create(form: MemoForm) = {
// トランザクションを作成
val transaction = (for {
// メモを登録してidを取得
memoId <- Memo returning Memo.map(_.id) += MemoRow(form.title, form.mainText, None, new Timestamp(System.currentTimeMillis()))
// ひも付けられたタグがマスタに存在しない場合、タグを登録
_ <- DBIO.sequence(form.tags.map(tag =>
TagMst.filter(_.name === tag).exists.result.flatMap {
case true => DBIO.successful(0)
case false => (TagMst returning TagMst.map(_.id) += TagMstRow(Some(tag))).map(DBIO.successful(_)).flatten
}))
// メモとタグのマッピングを登録
_ <- TagMst.filter(_.name.inSetBind(form.tags)).result.flatMap(tags =>
TagMapping ++= tags.map(tag => TagMappingRow(Some(memoId), tag.id)))
} yield memoId).transactionally
db.run(transaction)
}
更新
入力内容をもとにメモの更新、タグの登録、メモとタグのひも付きを削除・登録します。
- 以下がコードで使っているslickのメソッドになります。
-
update((form.title, form.mainText, Some(new Timestamp(System.currentTimeMillis()))))
UPDATE
に当たります。
update の前に filter で更新対象を絞り込んでいます。
-
def update(id: Int, form: MemoForm) = {
// トランザクションを作成
val transaction = (for {
// メモをアップデートして更新したメモのidを取得
updatedMemoId <- Memo.filter(memo => memo.id === id)
.map(result => (result.title, result.mainText, result.upadtedAt))
.update((form.title, form.mainText, Some(new Timestamp(System.currentTimeMillis())))).map(_ match {
case updated if updated == 1 => Some(id)
case _ => None
})
// メモとタグのマッピング前にタグがマスタに存在していない場合はタグを登録
_ <- DBIO.sequence(form.tags.filter(!_.endsWith("-remove")).map(tag =>
TagMst.filter(_.name === tag)
.exists.result.flatMap {
case true => DBIO.successful(0)
case false => (TagMst returning TagMst.map(_.id) += TagMstRow(Some(tag))).map(DBIO.successful(_)).flatten
}))
// メモとタグのマッピングの削除と登録
_ <- DBIO.sequence(form.tags.map(tag => tag.endsWith("-remove") match {
case true => TagMst.filter(_.name === tag.replace("-remove", "")).result.headOption.flatMap {
case Some(tagMstRow) => TagMapping.filter(tagMap => tagMap.memoId === updatedMemoId && tagMap.tagId === tagMstRow.id).delete
case None => DBIO.successful(0)
}
case false => TagMst.filter(_.name === tag).result.headOption.flatMap(_.map(tagMstRow =>
TagMapping.filter(tagMap => tagMap.memoId === updatedMemoId && tagMap.tagId === tagMstRow.id).exists.result.flatMap {
case true => DBIO.successful(0)
case false => TagMapping += TagMappingRow(updatedMemoId, tagMstRow.id)
}).getOrElse(DBIO.successful(0)))
}))
} yield updatedMemoId).transactionally
db.run(transaction)
}
削除
指定されたIDのメモとタグのひも付きを削除します。
- 以下がコードで使っているslickのメソッドになります。
-
delete
DELETE
に当たります。deleteの前に filter で削除対象を絞り込んでいます。
-
def delete(id: Int) = {
// メモとタグのひも付きを削除
db.run(TagMapping.filter(_.memoId === id).delete.flatMap(_ => Memo.filter(_.id === id).delete))
}
View
検索、登録、更新の3画面で構成しています。
検索画面
@* list Template File *@
@(memoForm: Form[controllers.forms.Memo.MemoForm], memos: Seq[controllers.forms.Memo.MemoForm])(implicit request: Request[Any], messages: Messages)
@import helper._
@main("一覧") {
<div class="row">
<div class="form-group">
<div class="col-md-6">
<input type="text" name="mainText" class="form-control" placeholder="フリーワード例:Play" value="@memoForm("mainText").value">
</div>
<div class="col-md-6">
<!-- <button type="submit" class="btn btn-primary">検索</button> -->
<a href="@routes.MemoController.search(None)" class="btn bntn-default" role="button">検索</a>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<a href="@routes.MemoController.showCreate()" class="btn btn-default" role="button">作成</a>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-hover">
<thead>
<tr>
<th>タイトル</th>
<th>タグ</th>
<th></th>
</tr>
</thead>
<tbody>
@memos.map { memo =>
<tr>
<td><a href="@routes.MemoController.showUpdate(memo.id.getOrElse(-1))">@memo.title</a></td>
<td>
@memo.tags.map { tag =>
<span class="label label-info">@tag</span>
}
</td>
<td>
@helper.form(CSRF(routes.MemoController.delete(memo.id.getOrElse(-1)))) {
<input type="submit" value="削除" class="btn btn-danger btn-xs"/>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
登録画面
@* create Template File *@
@(memoForm: Form[controllers.forms.Memo.MemoForm])(implicit rs: Request[Any], messages: Messages)
@import helper._
@main("登録") {
<script src="@routes.Assets.versioned("javascripts/viewhelper.js")"></script>
<h1>登録</h1>
@form(CSRF(routes.MemoController.create), 'class -> "container", 'role -> "form") {
<fieldset>
<div class="row">
<div class="form-group">
@inputText(memoForm("title"), '_label -> "タイトル", 'placeholder -> "例:Play でハマった所", 'class -> "form-control", '_help->"")
</div>
</div>
<div class="row">
<div class="form-group">
@textarea(memoForm("mainText"), '_label -> "メモ", 'placeholder -> "メモ", 'class -> "form-control", 'rows -> "10")
</div>
</div>
<div class="row">
<div class="form-group">
<dl>
<dt>
<label for="tag">タグ</label>
</dt>
<dd>
<div id="tagdisp" class="tagdisp"></div>
<input type="text" id="tag" placeholder="タグ" class="form-control">
</dd>
</dl>
</div>
</div>
<div class="row">
<input type="submit" value="登録" class="btn btn-primary" />
</div>
</fieldset>
}
}
更新画面
@* update Template File *@
@(memoForm: Form[controllers.forms.Memo.MemoForm])(implicit request:Request[Any], messages: Messages)
@import helper._
@main("更新") {
<script src="@routes.Assets.versioned("javascripts/viewhelper.js")"></script>
<h1>更新</h1>
@form(CSRF(routes.MemoController.update(memoForm("id").value.getOrElse("-1").toInt)), 'class -> "container", 'role -> "form") {
<fieldset>
<div class="row">
<div class="form-group">
@inputText(memoForm("title"), '_label -> "タイトル", 'class -> "form-control", '_help -> "")
</div>
</div>
<div class="row">
<div class="form-group">
@textarea(memoForm("mainText"), '_label -> "メモ", 'class -> "form-control", 'rows -> "10")
</div>
</div>
<div class="row">
<div class="form-group">
<dl>
<dt>
<label for="tag">タグ</label>
</dt>
<dd>
<div id="tagdisp" class="tagdisp">
@memoForm.apply("tags").indexes.map { i =>
<span class="label label-info">@memoForm(s"tags[$i]").value<span class="init-tag-remove">x</span></span>
}
</div>
<input type="text" id="tag" class="form-control">
</dd>
</dl>
</div>
</div>
@memoForm("title").value.map { value =>
<input type="hidden" name="title" value="@value"/>
}
<div class="row">
<input type="submit" value="更新" class="btn btn-primary" />
</div>
</fieldset>
}
}
routesとController
routes
uri と Controller の処理を関連付けます。
# Routes
# This file defines all application routes (Higher priority routes first)
# https://www.playframework.com/documentation/latest/ScalaRouting
# ~~~~
# An example controller showing a sample home page
#GET / controllers.HomeController.index
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
# Memo
# list
GET /memos controllers.MemoController.search(mainText: Option[String])
# create
GET /memos/ controllers.MemoController.showCreate
POST /memos/ controllers.MemoController.create
# update
GET /memos/:id controllers.MemoController.showUpdate(id: Int)
POST /memos/:id controllers.MemoController.update(id: Int)
# delete
POST /memos/delete/:id controllers.MemoController.delete(id: Int)
Controller
Controller は一つです。
MemoDao を DI を使って取得し、DAO の処理を実行してその処理結果をもとにレスポンスを決定します。
package controllers
import scala.concurrent.{ ExecutionContext, Future }
import controllers.forms.Memo.{ MemoForm, memoForm }
import dao.MemoDao
import javax.inject.Inject
import play.api.Logger
import play.api.i18n.I18nSupport
import play.api.mvc.{ AbstractController, ControllerComponents }
class MemoController @Inject() (cc: ControllerComponents, memoDao: MemoDao)(implicit ec: ExecutionContext) extends AbstractController(cc) with I18nSupport {
private val logger = Logger(classOf[MemoController])
def search(mainText: Option[String]) = Action.async { implicit rs =>
memoDao.search(mainText).map(result =>
Ok(views.html.memo.list(memoForm.fill(MemoForm(None, "", mainText, Seq.empty[String])), result.map(MemoForm(_)))))
}
def showCreate = Action.async { implicit rs =>
Future { Ok(views.html.memo.create(memoForm.fill(MemoForm(None, "", None, Seq.empty[String])))) }
}
def create = Action.async { implicit rs =>
memoForm.bindFromRequest().fold(errors => Future {
BadRequest(views.html.memo.create(errors))
}, form => memoDao.create(form).map(_ => Redirect(routes.MemoController.search(None))))
}
def showUpdate(id: Int) = Action.async { implicit rs =>
memoDao.findById(id).map(_.fold(
Redirect(routes.MemoController.search(None)))(memo => {
Ok(views.html.memo.update(memoForm.fill(MemoForm(memo))))
}))
}
def update(id: Int) = Action.async { implicit rs =>
memoForm.bindFromRequest().fold(errors => Future {
BadRequest(views.html.memo.update(errors))
}, form => memoDao.update(id, form).map(_ => Redirect(routes.MemoController.search(None))))
}
def delete(id: Int) = Action.async { implicit rs =>
memoDao.delete(id).map(_ => Redirect(routes.MemoController.search(None)))
}
}
DaoのDI
DI は @Inject() (cc: ControllerComponents, memoDao: MemoDao)
で行っています。
Dao は google guice の eager singleton binding を使ってインスタンスを生成しています。
package modules
import com.google.inject.AbstractModule
import dao.MemoDao
import dao.MemoDaoImpl
class DaoModule extends AbstractModule {
override def configure() = {
bind(classOf[MemoDao]).to(classOf[MemoDaoImpl]).asEagerSingleton()
}
}
上記のモジュールは設定ファイルにて有効にしています。
play.modules {
enabled += modules.DaoModule
}
まとめ
以上、一通り CRUD を実装しましたがいかがでしたでしょうか?
上記以外にも PlayFramework の機能は数多くありますが公式ドキュメントがとても充実しており、PlayFramework を始めるのに十分な情報量があります。分からないことがあった時やハマったときも、公式ドキュメントで解決することがありますので是非一度目を通してみてください。
また、この記事で使っている PlayFramework のプロジェクトは Github にありますので、参考にしていただけたらと思います。
以上、PlayFramework を始めた OR 始める予定のエンジニアの助けになれば幸いです。
明日は @hahegawa さんです。よろしくお願いします!
参考文献
Play 2.6.x documentation
slick-doc-ja 3.0
Slick 3.2.0 manual
Slick 3.2.0 api doc
Guice’s eager singleton binding
最後に
ウェブクルーでは一緒に働いていただける方を随時募集しております。
お気軽にエントリーくださいませ。