ぜったいってなに

Software Engineerのブログです。

Play Scala ViewからDBレコード追加

モデル追加に続いて、ViewからDBレコードのCLUDができるようにする。

前回に引き続き、こちらを参考に。 qiita.com



routesの設定

/conf/routes

  1 # Routes
  2 # This file defines all application routes (Higher priority routes first)
  3
  4 GET     /                           controllers.HomeController.index
  5 GET     /article                    controllers.ArticleController.index # added
  6 POST    /article                    controllers.ArticleController.create # added
  7
  8 # Map static resources from the /public folder to the /assets URL path
  9 GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)


今回はArticlesControllerではなくArticleControllerと命名した。

Ruby on RailsではController名は複数形で書くことを推奨されている。
「推奨されている」というのはどういうことかというと、Controllerが複数形で書かれていれば、resources{resource}s_{action}_pathのように、Railsがパスを自動生成してくれる。


が、この「Controller名は複数形で書くべき」という設計(思想?)は間違っているのでは?と思う。


Railsの開発者であるDHHは、Controllerのアクションは全部resourcesで間に合わせるべきで、それ以外のアクションが必要になったらController自体を分けることを推奨している。

postd.cc


でも、全部resourcesで書くべきならば、ControllerによってはORM(Railsの場合はActiveRecord)を継承しないクラスを扱うこともあるだろうし、Controller内でindexアクションを使わないならば、それ以外のresourcesのアクションに関してはresourceが複数であることを意識しないので、どちらかというと単数形で統一するのが正しいのではないかと思う。


Play Scalaでは、今のところ「Controllerは複数形を使ってね」と言われていないので、
単数形で書くことにした。


evolutionsの修正

テーブル名が複数形になっていないのが気持ち悪いので、全部複数形に直す。

/conf/evolutions/default/1.sql

  1 # --- !Ups
  2
  3 CREATE TABLE "users" ( # user -> users
  4     "username" TEXT NOT NULL,
  5     "password" TEXT NOT NULL,
  6     "name" TEXT NOT NULL
  7 );
  8
  9 CREATE TABLE "articles" ( # article -> articles
 10     "user_id" INT NOT NULL,
 11     "content" TEXT NOT NULL
 12 );
 13
 14 CREATE TABLE "comments" ( # comment -> comments
 15     "user_id" INT NOT NULL,
 16     "article_id" INT NOT NULL,
 17     "content" TEXT NOT NULL
 18 );
 19
 20 # --- !Downs
 21
 22 DROP TABLE "users"; # user -> users
 23 DROP TABLE "articles"; # article -> articles
 24 DROP TABLE "comments"; # comment -> comments

DAOの修正

  1. daoディレクトリが複数形になっていなかったので直す。
  2. evolutionsの修正にて、複数形に直したテーブルを参照するように修正する。

/app/daos/articleDAO.scala

  1 package daos // dao -> daos
  2
  3 import scala.concurrent.Future
  4
  5 import javax.inject.Inject
  6 import models.Article
  7 import play.api.db.slick.DatabaseConfigProvider
  8 import play.api.db.slick.HasDatabaseConfigProvider
  9 import play.api.libs.concurrent.Execution.Implicits.defaultContext
 10 import slick.driver.JdbcProfile
 11
 12 class ArticleDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider) extends HasDatabaseConfigProvider[JdbcProfile] {
 13   import driver.api._
 14
 15   private val Articles = TableQuery[ArticlesTable]
 16
 17   def all(): Future[Seq[Article]] = db.run(Articles.result)
 18
 19   def insert(article: Article): Future[Unit] = db.run(Articles += article).map { _ => () }
 20
 21   private class ArticlesTable(tag: Tag) extends Table[Article](tag, "articles") { // article -> articles
 22
 23     def user_id = column[Int]("user_id")
 24     def content = column[String]("content")
 25
 26     def * = (user_id, content) <> (Article.tupled, Article.unapply _)
 27   }
 28 }

uerDAO, commentDAOも同様に修正(省略)。


Controllerの設定

例に倣って書く。

/app/ArticleController.scala

  1 package controllers
  2
  3 import javax.inject._
  4 import play.api._
  5 import play.api.mvc._
  6 import play.api.db._
  7
  8 import daos.ArticleDAO
  9 import models.Article
 10 import play.api.data.Form
 11 import play.api.data.Forms.mapping
 12 import play.api.data.Forms.number
 13 import play.api.data.Forms.text
 14 import play.api.libs.concurrent.Execution.Implicits.defaultContext
 15
 16 @Singleton
 17 class ArticleController @Inject() (articleDao: ArticleDAO) extends Controller {
 18   def index = Action.async {
 19     articleDao.all().map {
 20       articles => Ok(views.html.article.index(articles))
 21     }
 22   }
 23
 24   def create = Action.async { implicit request =>
 25     val article: Article = articleForm.bindFromRequest.get
 26     articleDao.insert(article).map(_ => Redirect(routes.ArticleController.index))
 27   }
 28
 29   val articleForm = Form(
 30     mapping(
 31       "user_id" -> number,
 32       "content" -> text()
 33     )(Article.apply)(Article.unapply)
 34   )
 35 }


ちなみに31行目、user_idはDB上ではINT、Modelとしてもintとして扱っているので、formでtext()にmappingするとエラーになる。

Playのドキュメントを漁ってみると、int型を扱うにはnumberが適切っぽいのでnumberを設定した。


f:id:zettaittenani:20170914164451p:plain

参照元: play.api.data.Forms



Viewの設定

これも例に倣う。

/app/views/article/index.html.scala

  1 @(articles: Seq[Article])
  2 @main("Article database") {
  3 <div>
  4   <div id="articles">
  5     <h2>Insert an article here:</h2>
  6
  7     <form action="/article" method="POST">
  8       <input name="user_id" type="text" placeholder="user_id"/>
  9       <input name="content" type="text" placeholder="content"/>
 10       <input type="submit"/>
 11     </form>
 12
 13     <h2>Previously created articles:</h2>
 14     <table>
 15       <tr><th>UserId</th><th>Content</th></tr>
 16       @for(a <- articles){
 17       <tr><td>@a.user_id</td><td>@a.content</td></tr>
 18       }
 19     </table>
 20   </div>
 21 </div>
 22 }

実行してみる

sbt runしてhttp://localhost:9000/articleにアクセスしたら問題なく動作しました。

f:id:zettaittenani:20170914162319p:plain


レコードのcreateもちゃんとできた。

f:id:zettaittenani:20170914163123p:plain

@playdb, PostgreSQL

playdb=> select * from articles;
 user_id |     content
---------+------------------
       1 | hogehoge
       2 | 日本語
       3 | Play Scala
       4 | ぜったいってなに
(4 rows)


次はModelにビジネスロジックの実装かな。
Controllerに関しては他のresourcesアクションを追加するくらいしかやることないだろうし、
Viewに関してはControllerから値が渡せればサーバーサイドエンジニアとしてはやること終わりだと思うし。