櫻です。
前回はRuby on Railsを触ってみました。
rubyもRailsもすばらしいのですが、静的な型付けが欲しくなる事もあります。
ということで今回はPlay Framework(2.1.4+Slick)で前回のサンプルを作成してみましょう。
今回は以下のサイトをベースにサンプルを作成しました。
http://www.playframework.com/documentation/2.1.x/ScalaTodoList
http://www.playframework-ja.org/documentation/2.1.3/ScalaTodoList
準備
JDK(6以降)がインストールされていればOKです。
[sakura@MACmini250301 ~]$ java -version java version "1.7.0_40" Java(TM) SE Runtime Environment (build 1.7.0_40-b43) Java HotSpot(TM) 64-Bit Server VM (build 24.0-b56, mixed mode) [sakura@MACmini250301 ~]$
インストール
Macでhomebrewを導入している場合は
$ brew install play
でインストールできます。それ以外の環境の場合は
http://www.playframework.com/からzipファイルをダウンロードしてきて展開+パスの設定をすれば完了です。
詳細はこちらを参考にしてください。
※プロキシを利用している場合は
環境変数HTTP(S)_PROXYを設定しましょう。
export HTTP_PROXY=http://proxy.example.com:8080/ export HTTPS_PROXY=http://proxy.example.com:8080/
正しく設定できれば、play helpで以下のようなヘルプが表示されます。
[sakura@MACmini250301 ~]$ play help Getting net.java.dev.jna jna 3.2.3 ... :: retrieving :: org.scala-sbt#boot-jna confs: [default] 1 artifacts copied, 0 already retrieved (838kB/13ms) Getting play console_2.9.2 2.1.4 ... :: retrieving :: org.scala-sbt#boot-app confs: [default] 29 artifacts copied, 0 already retrieved (5976kB/33ms) Getting Scala 2.9.2 (for console)... :: retrieving :: org.scala-sbt#boot-scala confs: [default] 4 artifacts copied, 0 already retrieved (20090kB/30ms) _ _ _ __ | | __ _ _ _| | | '_ \| |/ _' | || |_| | __/|_|\____|\__ (_) |_| |__/ play! 2.1.4 (using Java 1.7.0_40 and Scala 2.10.0), http://www.playframework.org Welcome to Play 2.1.4! These commands are available: ----------------------------- license Display licensing informations. new [directory] Create a new Play application in the specified directory. You can also browse the complete documentation at http://www.playframework.org. [sakura@MACmini250301 ~]$
プロジェクト作成
プロジェクト作成は
$ play new blog
で行えます。
途中でアプリ名とScala/Javaのどちらを利用するかを聞かれます。
[sakura@MACmini250301 play_sample]$ play new blog _ _ _ __ | | __ _ _ _| | | '_ \| |/ _' | || |_| | __/|_|\____|\__ (_) |_| |__/ play! 2.1.4 (using Java 1.7.0_40 and Scala 2.10.0), http://www.playframework.org The new application will be created in /Users/sakura/Documents/play_sample/blog What is the application name? [blog] > Which template do you want to use for this new application? 1 - Create a simple Scala application 2 - Create a simple Java application > 1 OK, application blog is created. Have fun! [sakura@MACmini250301 play_sample]$ cd blog/ [sakura@MACmini250301 blog]$
プロジェクトの作成で以下のファイルが作成されています。
[sakura@MACmini250301 blog]$ tree -a . ├── .gitignore ├── README 作成したアプリのREADME ├── app アプリのソース │ ├── controllers コントローラ │ │ └── Application.scala │ └── views ビュー │ ├── index.scala.html │ └── main.scala.html ├── conf アプリの設定ファイルなど │ ├── application.conf 設定ファイル │ └── routes ルーティング定義 ├── project プロジェクトの設定ファイル │ ├── Build.scala │ ├── build.properties │ └── plugins.sbt ├── public assets(JavaScript、css、画像などブラウザから参照するファイル) │ ├── images │ │ └── favicon.png │ ├── javascripts JavaScript │ │ └── jquery-1.9.0.min.js │ └── stylesheets css │ └── main.css └── test 単体テストコード ├── ApplicationSpec.scala └── IntegrationSpec.scala 10 directories, 14 files [sakura@MACmini250301 blog]$
自動で.gitignoreも作成されるのが嬉しいですね。
この状態でplayを実行するとplay consoleが起動します。
[sakura@MACmini250301 blog (master)]$ play Getting org.scala-sbt sbt 0.12.2 ... :: retrieving :: org.scala-sbt#boot-app confs: [default] 40 artifacts copied, 0 already retrieved (8381kB/141ms) [info] Loading project definition from /Users/sakura/Documents/play_sample/blog/project [info] Set current project to blog (in build file:/Users/sakura/Documents/play_sample/blog/) _ _ _ __ | | __ _ _ _| | | '_ \| |/ _' | || |_| | __/|_|\____|\__ (_) |_| |__/ play! 2.1.4 (using Java 1.7.0_40 and Scala 2.10.0), http://www.playframework.org > Type "help play" or "license" for more information. > Type "exit" or use Ctrl+D to leave this console. [blog] $
play console内でrunを実行すると開発モードでサーバが起動します。
[blog] $ run Getting Scala 2.10.0 ... :: retrieving :: org.scala-sbt#boot-scala confs: [default] 5 artifacts copied, 0 already retrieved (24106kB/366ms) [info] Updating {file:/Users/sakura/Documents/play_sample/blog/}blog... [info] Done updating. --- (Running the application from SBT, auto-reloading is enabled) --- [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000 (Server started, use Ctrl+D to stop and go back to the console...)
ブラウザでhttp://localhost:9000/を開くと画像のような画面が開きます。
開発モードではソースコードを書き換えてブラウザで読み込むだけで自動でコンパイルされて実行されます。
サーバを停止させる時はCtrl-Dです。
デバッグ
デバッガを接続する場合はplay実行時にdebugを指定すればOKです。
デフォルトでは9999番ポートで待ち受けるようです。
[sakura@MACmini250301 blog (master)]$ play debug Listening for transport dt_socket at address: 9999 [info] Loading project definition from /Users/sakura/Documents/play_sample/blog/project [info] Set current project to blog (in build file:/Users/sakura/Documents/play_sample/blog/) _ _ _ __ | | __ _ _ _| | | '_ \| |/ _' | || |_| | __/|_|\____|\__ (_) |_| |__/ play! 2.1.4 (using Java 1.7.0_40 and Scala 2.10.0), http://www.playframework.org > Type "help play" or "license" for more information. > Type "exit" or use Ctrl+D to leave this console. [blog] $
IDEの設定ファイル生成
さらに、play console内でeclipseやideaコマンドを実行すると、IDE用のプロジェクトファイルが生成されます。
[blog] $ idea with-sources=yes [info] Trying to create an Idea module blog [info] Excluding folder targett_2.10;2.1.4 ... [info] Created /Users/sakura/Documents/play_sample/blog/.idea/IdeaProject.iml [info] Created /Users/sakura/Documents/play_sample/blog/.idea [info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/scala-2.10/cache [info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/scala-2.10/classes [info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/scala-2.10/classes_managed [info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/native_libraries [info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/resolution-cache [info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/scala-2.10/resource_managed [info] Excluding folder /Users/sakura/Documents/play_sample/blog/target/streams (commons-codec_commons-codec_1.6_test,List(commons-codec_commons-codec_1.3)) (org.apache.httpcomponents_httpcore_4.1.3_test,List(org.apache.httpcomponents_httpcore_4.0.1)) (org.apache.httpcomponents_httpclient_4.1.2_test,List(org.apache.httpcomponents_httpclient_4.0.1)) [info] Created /Users/sakura/Documents/play_sample/blog/.idea_modules/blog.iml [info] Created /Users/sakura/Documents/play_sample/blog/.idea_modules/blog-build.iml [blog] $
これで、必要なクラスパス等が設定された状態のプロジェクトが作成されています。
Hello World
まずはHello Worldを作成してみましょう。
app/controllers/Application.scalaのindexメソッドを以下の様に書き換えます。
object Application extends Controller { def index = Action { Ok("Hello World") } }
ファイルを保存してhttp://localhost:9000/を開くと無事Hello Worldが表示されます。
ルーティングの設定…?
Railsではこの後ルーティングの設定を行いましたが、Play Frameworkで同じ事を行うと、コントローラに対応するメソッドが無いというコンパイルエラーが発生します。
これをエラーとするかどうかが、静的/動的型付けの発想の違いですね。
とはいえ、全てのアクションを定義するまで他の部分も動作できなくなるのは大変なのでダミーのアクションを作成してコンパイルを通す事が可能です。
conf/routeファイルに以下を追加します。
GET /posts controllers.Application.posts POST /posts controllers.Application.newPost GET /posts/new controllers.Application.showNewPost GET /posts/:id controllers.Application.showPost(id: Long) POST /posts/:id/delete controllers.Application.deletePost(id: Long)
この時点でhttp://localhost:9000/postsへアクセスすると以下の様にコンパイルエラーになります。
playコンソール
[info] Compiling 8 Scala sources and 1 Java source to /Users/sakura/Documents/play_sample/blog/target/scala-2.10/classes... [error] /Users/sakura/Documents/play_sample/blog/conf/routes:8: value newPost is not a member of object controllers.Application [error] POST /posts controllers.Application.newPost [error] /Users/sakura/Documents/play_sample/blog/conf/routes:9: value showNewPost is not a member of object controllers.Application [error] GET /posts/new controllers.Application.showNewPost [error] /Users/sakura/Documents/play_sample/blog/conf/routes:10: value showPost is not a member of object controllers.Application [error] GET /posts/:id controllers.Application.showPost(id: Long) [error] /Users/sakura/Documents/play_sample/blog/conf/routes:7: value posts is not a member of object controllers.Application [error] GET /posts controllers.Application.posts [error] /Users/sakura/Documents/play_sample/blog/conf/routes:11: value deletePost is not a member of object controllers.Application [error] POST /posts/:id/delete controllers.Application.deletePost(id: Long) [error] /Users/sakura/Documents/play_sample/blog/conf/routes:7: value posts is not a member of object controllers.Application [error] GET /posts controllers.Application.posts [error] /Users/sakura/Documents/play_sample/blog/conf/routes:8: value newPost is not a member of object controllers.Application [error] POST /posts controllers.Application.newPost [error] /Users/sakura/Documents/play_sample/blog/conf/routes:9: value showNewPost is not a member of object controllers.Application [error] GET /posts/new controllers.Application.showNewPost [error] /Users/sakura/Documents/play_sample/blog/conf/routes:10: value showPost is not a member of object controllers.Application [error] GET /posts/:id controllers.Application.showPost(id: Long) [error] /Users/sakura/Documents/play_sample/blog/conf/routes:11: value deletePost is not a member of object controllers.Application [error] POST /posts/:id/delete controllers.Application.deletePost(id: Long) [error] 10 errors found [error] (compile:compile) Compilation failed [error] application -
ブラウザ
コンパイルが通る様にコントローラに対応するアクションメソッドを追加します。
object Application extends Controller { def index = Action { Ok("Hello World") } def posts = TODO def newPost = TODO def showNewPost = TODO def showPost(id: Long) = TODO def deletePost(id: Long) = TODO }
この状態でhttp://localhost:9000/posts などへアクセスするとTODO(ダミー)画面が表示されます。
モデル作成
Slickの追加・設定
Play Framework 2.1ではDBアクセスまわりはデフォルトでanormというライブラリを利用しますが、2.3からはSlickというライブラリに変更されるようなので、今回はSlickを使ってみます。
Slick関連は
http://gan.hatenablog.jp/entry/2013/06/09/133204
http://slick.typesafe.com/doc/1.0.1/lifted-embedding.html#tables
を参考にしました。
必要なライブラリの依存をproject/Build.scalaに追加します。
val appDependencies = Seq( // Add your project dependencies here, jdbc, "com.typesafe.play" %% "play-slick" % "0.4.0" )
playコンソールを起動し直すと自動でライブラリがダウンロードされます。
IDEの設定を生成している場合は、再作成する必要があるかもしれません。
続いて、DBの接続設定もしておきます。
conf/application.confファイルに定義があります。
デフォルトではH2データベース用の設定がコメントアウトされているので、これを有効にします。
db.default.driver=org.h2.Driver db.default.url="jdbc:h2:mem:play" db.default.user=sa db.default.password=""
さらにSlick用の設定も追加しておきます。
# Slick slick.default="models.*"
Postモデルの作成
Railsではrailsコマンドでモデルクラスのひな形を作成しましたが、Play Frameworkではクラス定義を元にいろいろ行ってくれるようです。
Slickのサンプルをみるとモデルはcase classとして作成し、DBアクセス周りは専用のobjectを作成するようです。
ということで、app/models/Post.scalaは以下の様になります。
import play.api.db.slick.Config.driver.simple._ import play.api.Play.current import play.api.db.slick._ case class Post(id: Long, title: String, text: String) object Posts extends Table[Post]("post") { def id = column[Long]("post_id", O.PrimaryKey, O.AutoInc) def title = column[String]("title") def text = column[String]("text") def * = id ~ title ~ text <> (Post, Post.unapply _) def ins = title ~ text returning id def all() = DB.withSession{ implicit session:Session => Query(Posts).sortBy(_.id).list } def create(title: String, text: String) = DB.withSession { implicit session: Session => Posts.ins.insert(title, text) } def delete(id: Long) = DB.withSession { implicit session: Session => Posts.where(_.id === id).delete } def apply(id: Long): Post = DB.withSession { implicit session: Session => Query(Posts).where(_.id === id).list.head } }
id、title、textメソッドの定義がカラムの定義として使われる事になります。
def * = id ~ title ~ text <> (Post, Post.unapply _) def ins = title ~ text returning id
この辺りはDSLの様に見えますが、ただのscalaプログラムです。
今回はLifted Embeddingと呼ばれるスタイルでDB操作を記述していますが、SQLを直接記述するスタイルもあります。
val post_id = 10 val post = sql"select * from post where post_id = $post_id".as[Post].head
一覧画面の作成
一覧画面のviewを作成します。
Play Frameworkでは専用のテンプレート言語でviewを作成します。@の後ろだけ特殊な動作を行います。
app/views/index.scala.htmlファイルを以下の様に変更します。
1行めの@(posts: List[Post])はこのテンプレートの引数になります。アクションや他のテンプレートから値を受け取る事になります。
@(posts: List[Post]) @import helper._ @main("Listing posts") { <h1>Listing posts</h1> <table> @posts.map { post => <tr> <td><a href="@routes.Application.showPost(post.id)">@post.title</a></td> <td> @form(routes.Application.deletePost(post.id)) { <input type="submit" value="Delete"> } </td> </tr> } </table> <a href="@routes.Application.showNewPost">New Post</a> }
続いてアクションを作成します。
app/controllers/Application.scalaを以下の様に変更します。
import追加
import models._
postsメソッド変更
def posts = Action {
Ok(views.html.index(Posts.all()))
}
views.html.index(...)の読み出しがテンプレートの実行になります。
Posts.all()メソッドの結果がビューの引数として渡されます。
両者の型がそろっているのがポイントで、指定の間違いや開発中にテンプレートへ渡す型を変更した場合不整合があればコンパイルエラーとして検出されます。
これでhttp://localhost:9000/posts へアクセスすると、DBアクセス時にテーブルが存在しないため、エラーになります。
エラーにはなりますが、きちんと必要なSQLが提示され、画面のボタンを押すと自動で実行してくれます。またSQLファイルもconf/evolutions.[dbname]/[n].sqlに保存されています。
「apply this script now」ボタンを押すと空の一覧画面が表示されます。
新規作成画面
次に新規作成の画面のviewを作成します。
app/views/newPost.scala.htmlを以下の様に作成します。
@(postForm: Form[(String, String)]) @import helper._ @main("New Post") { <h1>Add a new Post</h1> @form(routes.Application.newPost) { @inputText(postForm("title")) @inputText(postForm("text")) <input type="submit" value="Create"> } <a href="@routes.Application.posts()">Post List</a> }
アクションも作成します。app/controllers/Application.scalaにimportを追加し、
import play.api.data._ import play.api.data.Forms._
showNewPostメソッドの変更とpostFormの定義を追加します。
def showNewPost = Action { Ok(views.html.newPost(postForm)) } val postForm = Form( tuple( "title" -> nonEmptyText, "text" -> nonEmptyText ) )
このFormの定義で入力必須などの指定が可能です。
ファイルを保存し、一覧画面のNew Postをクリックすると入力画面が表示されます。
続いて投稿のアクションを作成します。
newPostメソッドを以下の様に変更します。
def newPost = Action { implicit request =>
postForm.bindFromRequest.fold(
errors => BadRequest(views.html.newPost(errors)),
form => {
Posts.create(form._1, form._2)
Redirect(routes.Application.posts())
}
)
}
フォームの内容を受け取り、エラーがあれば入力画面へもどし、なければDBへ登録して一覧画面へ遷移させます。
ファイルを保存し、入力画面で適当に入力後createボタンを押すと一覧に追加されます。
詳細画面
詳細画面も作成しましょう。
app/views/post.scala.htmlを以下の様に作成します。
@(post: Post) @import helper._ @main(post.title) { <p><strong>Title:</strong> @post.title</p> <p><strong>Text:</strong> @post.text</p> <a href="@routes.Application.posts()">Post List</a> }
app/controllers/Application.scalaのshowPostメソッドを以下の様に変更します。
def showPost(id: Long) = Action {
Ok(views.html.post(Posts(id)))
}
ファイルを保存して一覧画面のリンクをクリックすると、詳細が表示されます。
削除ボタン
最後に削除機能を作成します。
app/controllers/Application.scalaのdeletePostメソッドを以下の様に変更します。
def deletePost(id: Long) = Action {
Posts.delete(id)
Redirect(routes.Application.posts())
}
ファイルを保存し、一覧画面のdeleteボタンを押すと、投稿が削除されます。
プロジェクト全体のコードはこちらにあります。
まとめ
Play Frameworkで簡単なサンプルを作成しました。
お手軽さではRailsの方が上かもしれませんが、型のチェックが入るのは大きな安心感が生まれるのではないでしょうか。