argius note

プログラミング関連

Skinny Frameworkで始めるScalaのWebアプリ開発

3/28にSkinny Frameworkのバージョン1.0.0 finalがリリースされました。
今回のエントリーでは、Skinny Frameworkの紹介も兼ねて簡単なWebアプリを作ってみます。


リリースのペースが速いので、1.0.0以降も既にいくつかリリースされています。
今回はバージョン1.0.4を使いました。



あ、あと、めずらしく画像ありのエントリーになっています。


概要

Skinny Framework(以下、skinnyと表記)は、Scala向けのフルスタックWebアプリケーションフレームワークで、ScalaRailsのようなことができます。
メイン開発者は@seratchさんで、GitHub上で開発が進められています。

私が把握しているレベルでの、主な特徴は以下。

Scalatraがベースなので(私は詳しくないです)、いくつかの種類のテンプレートが選べます。デフォルトでSSP,Scaml,Jade、オプションでFreeMaker,Thymeleafなどが利用できます。
Skinny ORMは、同じく@seratchさんが開発されているScalikeJDBCを土台としたオリジナルORMです。


用意するもの

これだけあれば始められます。

skinny-blank-app.zipは、https://github.com/skinny-framework/skinny-framework/releasesから最新版をダウンロードします。今回は、1.0.4を使用します。
追記(2014-05-09): バージョン1.0.6-1以降では、必須のライブラリを同梱したskinny-blank-app-with-deps.zipが提供されるようになりました。これを使えば、パッケージ自体のダウンロードは時間がかかる代わりに、最初の依存性解決で時間がかからなくなります。他の開発でivy2リポジトリを使ってないような場合は、こちらを使うのが良さそうです。


skinnyはパッケージにSBTランチャー(bin/sbt-launch.jar)が含まれていますので、SBT単独のインストールは不要です。但し、SBTのライブラリー管理用にそこそこディスク容量を消費しますので、念のためご注意ください。(Windowsの場合は、%USERPROFILE%\.ivy2の下にライブラリーがダウンロードされるので、Cドライブに少なくとも136MBくらいの空きが必要です。)
あと、空きメモリーがそこそこ必要です。

SBTについて詳しく知りたい方は、以下のページなどをご覧下さい。


Scalaが使えるIDEがあると、より一層楽に開発できます。少なくとも、文字コードセットにUTF-8が使えるテキストエディターがあればできます。今回のアプリは、テキストエディターだけで書いています。


Hello, world

skinny-blank-app.zipを、お好きなフォルダーに解凍します。ここではユーザーのフォルダーの下にします。ユーザーargiusの場合だと、C:\Users\argius\skinny-blank-appになります。
コマンドプロンプトを起動して、解凍したskinny-blank-appフォルダーに移動します。

C:\Users\argius>cd skinny-blank-app
C:\Users\argius\skinny-blank-app>

以下、プロンプトは省略して、" > "だけ書きます。

次に最初の起動を行います。dependencies(ライブラリー依存関係)の解決のため、初回だとダウンロードで少々時間がかかります。

 > skinny run
[info] Loading project definition from C:\Users\argius\skinny-blank-app\project
[info] Updating {file:/C:/Users/argius/skinny-blank-app/project/}skinny-blank-app-build...
[info] Resolving org.scalatra.sbt#scalatra-sbt;0.3.4 ...
  [info] Resolving com.earldouglas#xsbt-web-plugin;0.7.0 ...
  [info] Resolving org.mortbay.jetty#jetty;6.1.22 ...
  [info] Resolving org.mortbay.jetty#project;6.1.22 ...
  [info] Resolving org.mortbay.jetty#jetty-parent;8 ...

 (中略)

  [info] Resolving org.slf4j#slf4j-api;1.6.1 ...
[info] Done updating.
[success] Total time: 0 s, completed 2014/04/04 9:57:18
[info] Generating C:\Users\argius\skinny-blank-app\target\dev\scala-2.10\resource_managed\main\rebel.xml.
[info] Compiling 6 Scala sources to C:\Users\argius\skinny-blank-app\target\dev\scala-2.10\classes...
2014-04-04 09:57:39.097:INFO:oejs.Server:jetty-8.1.14.v20131031
2014-04-04 09:57:39.781:INFO:oejw.StandardDescriptorProcessor:NO JSP Support for /, did not find org.apache.jasper.servlet.JspServlet
2014-04-04 09:57:40,730 INFO [pool-15-thread-3] o.s.s.ScalatraListener [slf4j.scala:128] The cycle class name from the config: ScalatraBootstrap
2014-04-04 09:57:41,147 DEBUG [pool-15-thread-3] o.s.s.ScalatraListener [slf4j.scala:86] Loaded lifecycle class: class ScalatraBootstrap
2014-04-04 09:57:41,334 INFO [pool-15-thread-3] o.s.s.ScalatraListener [slf4j.scala:128] Initializing life cycle class: ScalatraBootstrap
2014-04-04 09:57:42,365 DEBUG [pool-15-thread-3] s.ConnectionPool$ [Log.scala:45] Registered connection pool : ConnectionPool(url:jdbc:h2:file:db/development;MODE=PostgreSQL;AUTO_SERVER=TRUE, user:sa)

2014-04-04 09:57:45.223:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:8080
[success] Total time: 26 s, completed 2014/04/04 9:57:45
1. Waiting for source changes... (press enter to interrupt)



Waiting for source changesが出たら、サーバーの準備ができています。
http://localhost:8080にアクセスしてみましょう。

  • Hello, worldのページ

f:id:argius:20140404204438p:plain

コマンドラインでENTERキーを押すとサーバーは終了します。

scaffold - CRUDを自動生成

ここまでは、まだトップページだけの状態です。
次は、scaffoldコマンドを使って、CRUDのページを自動生成してみます。
scaffoldとは「骨格」「足場」などを意味する単語です。skinnyでは、データベースのテーブルと、そのCRUD、すなわち追加・取得・更新・削除のページを自動で作ってくれます。


ここでは、本のリストのアプリを作ってみましょう。

  • scaffoldコマンドで本のリストのscaffoldを生成
 > skinny g scaffold BookList Book title:String author:String point:Option[Int]



第1引数のBookListはコントローラー(リクエストを処理する部分)の名前とデータベースのテーブルの名前(book_list)に、第2引数のBookはモデル(データベースとやりとりする部分)の名前に使われます。その後ろは、book_listテーブルの列を指定します。列は、列名とデータ型をコロンでつなげます。データベース型も指定したい場合は、title:String:varchar(100)のように指定します。

 > skinny g scaffold BookList Book title:String author:String point:Option[Int]
指定されたファイルが見つかりません。
3 個のファイルをコピーしました
[info] Loading project definition from C:\Users\argius\skinny-blank-app\project
[info] Set current project to dev (in build file:/C:/Users/argius/skinny-blank-app/)
[info] Generating C:\Users\argius\skinny-blank-app\task\target\scala-2.10\resource_managed\main\rebel.xml.
[info] Compiling 1 Scala source to C:\Users\argius\skinny-blank-app\task\target\scala-2.10\classes...
[info] Running TaskRunner generate:scaffold BookList Book title:String author:String point:Option[Int]

 *** Skinny Generator Task ***

  "src\main\scala\controller\ApplicationController.scala" skipped.
  "src\main\scala\controller\BookListController.scala" created.
  "src\main\scala\controller\Controllers.scala" modified.
  "src\test\scala\controller\BookListControllerSpec.scala" created.
  "src\test\scala\integrationtest\BookListController_IntegrationTestSpec.scala" created.
  "src\test\resources\factories.conf" modified.
  "src\main\scala\model\Book.scala" created.
  "src\test\scala\model\BookSpec.scala" created.
  "src\main\webapp\WEB-INF\views\bookList\_form.html.ssp" created.
  "src\main\webapp\WEB-INF\views\bookList\new.html.ssp" created.
  "src\main\webapp\WEB-INF\views\bookList\edit.html.ssp" created.
  "src\main\webapp\WEB-INF\views\bookList\index.html.ssp" created.
  "src\main\webapp\WEB-INF\views\bookList\show.html.ssp" created.
  "src\main\resources\messages.conf" modified.
  "src\main\resources\db\migration\V20140404100256__Create_bookList_table.sql" created.

[success] Total time: 6 s, completed 2014/04/04 10:02:56

 >

生成されるファイルのうち、Viewテンプレートはsspになります。Jadeで生成したい場合は、scaffoldのところをscaffold:jadeにすればOKです。

この時点では、まだデータベースにテーブルが作成されていません。テーブルを作成するには、db:migrateを実行します。

 > skinny db:migrate

db/development.h2.dbというファイルが作られ、その中にbook_listテーブルが作成されました。

これを動かしてみましょう。skinny runして、今度はhttp://localhost:8080/book_listにアクセスします。

  • book_listの一覧(ゼロ件)

f:id:argius:20140404204439p:plain

Newボタンを押すと、新しいレコードの登録するフォームが表示されます。ここで登録(CREATE)ができます。同様に、取得(RETRIEVE)、更新(UPDATE)、削除(DELETE)ができます。

  • book_listの一覧(いくつか登録した)

f:id:argius:20140404204440p:plain

日本語で表示

デフォルトでは、ボタンや列の名前は英語になっています。
これらを日本語に変えてみましょう。

サーバーは起動したままでもOKです。


まず、src/main/scala/controller/ApplicationController.scalaの16行目のdefaultLocaleコメントアウトを外します。

  with ErrorPageFilter {

  override def defaultLocale = Some(new java.util.Locale("ja"))

}



次に、src/main/resources/messages.confファイルをコピーして、messages_ja.confファイルを作ります。そして、33行目からの内容を、以下のものに置き換えます。33行目より前は使わないのでここでは省略。
ファイルを保存する時に、文字コードセットをUTF-8にして保存するのを忘れないように!

top="トップページへ"
backToList="一覧へ戻る"
submit="送信"
cancel="キャンセル"
detail="詳細"
new="追加"
edit="変更"
delete="削除"


book {
  flash {
    created="レコードが作成されました。"
    updated="レコードが更新されました。"
    deleted="レコードが削除されました。"
  }
  list="本の一覧"
  detail="本の詳細"
  edit="本の編集"
  new="本の新規登録"
  delete.confirm="削除してもいいですか?"
  id="ID"
  title="タイトル"
  author="著者"
  point="点数"
}
  • 日本語に変えたbook_list


絞り込み機能を追加する

せっかくリストを作ったので、キーワードでデータを探せるようにしたいですね。
タイトルか著者のいずれかがキーワードと一致したレコードだけ表示できるようにしてみましょう。
一覧のページをちょっと改造して作ってみます。


最初に、BookListControllerfindメソッドを実装します。

  • src/main/scala/controller/BookListController.scala findメソッド
  def find = {
    import scalikejdbc._, SQLInterpolation._

    val q = params.getAs[String]("q").getOrElse("")
    if (q.trim.isEmpty) redirect(s"${viewsDirectoryPath}/")

    // select a.* from book_list a where a.title like '%?%' or a.author like '%?%'
    val a = Book.defaultAlias
    val clauses = sqls.joinWithOr(sqls.like(a.title, s"%${q}%"), sqls.like(a.author, s"%${q}%"))
    val items = Book.findAllBy(clauses)

    set(itemsName -> items)
    set(totalPagesAttributeName -> 1)
    render(s"${viewsDirectoryPath}/index")
  }



次に、ControllersBookListController.findのURLを設定します。
19行目に1行追加します。

  • src/main/scala/controller/Controllers.scalaに追加するコード (findUrl)
  object bookList extends _root_.controller.BookListController {
    val findUrl = get(s"${viewsDirectoryPath}/find?")(find).as('find) // find = BookListController.find
  }


最後に、一覧ページに検索フォームを追加します。
src/main/webapp/WEB-INF/views/bookList/index.html.sspが一覧のテンプレートファイルです。
この15行目付近に、検索フォームを追加します。

  • src/main/webapp/WEB-INF/views/bookList/index.html.sspに追加するコード
<div class="form-group">
  <form action="${s.url(Controllers.bookList.findUrl)}">
    <label class="control-label">Find</label>
    <input type="text" class="" name="q" value="${request.getAttribute("q")}"/>
    <input class="btn btn-primary" type="submit" value="${s.i18n.get("submit")}"/>
    <a class="btn btn-default" href="${s.url(Controllers.bookList.indexUrl)}">${s.i18n.get("cancel")}</a>
  </form>
</div>
  • 絞り込んだところ


ページ機能は単純化のために省いています。


その他の機能

既存のデータベースのテーブルからリバースエンジニアリングでscaffoldができるreverse-scaffoldもあります。
特に、プライマリーキーが"id"でLong型に対応したDB型であれば、コマンドだけで完結します。プライマリーキーがid:Longでなくてもreverse-scaffoldは使えますが、その場合はSkinnyCRUDMapperWithId,primaryKeyFieldNameを設定する必要があります。(ORM - Skinny FrameworkSkinnyCRUDMapperWithId,primaryKeyFieldNameキーワードを参照)
1.0.4のバグみたいです。
(追記2014-04-19)1.0.6では直っています。

詳しくは、下記ページの後半をご覧ください。


他にもValidatorやMailerなどの便利な機能があります。テスト環境も充実しています。





今回はここまでです。
skinny自体もまだ新しいので、これからが楽しみです。