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アプリケーションフレームワークで、ScalaでRailsのようなことができます。
メイン開発者は@seratchさんで、GitHub上で開発が進められています。
私が把握しているレベルでの、主な特徴は以下。
- JDK以外のインストールが不要
- Scalatraフレームワークがベース
- CRUDでMVCな「ひな形」が自動生成できる
- 独自ORM (Skinny ORM)
- WAR or スタンドアロンJARで配備できる
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のページ
コマンドラインで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の一覧(ゼロ件)
New
ボタンを押すと、新しいレコードの登録するフォームが表示されます。ここで登録(CREATE)ができます。同様に、取得(RETRIEVE)、更新(UPDATE)、削除(DELETE)ができます。
- book_listの一覧(いくつか登録した)
日本語で表示
デフォルトでは、ボタンや列の名前は英語になっています。
これらを日本語に変えてみましょう。
サーバーは起動したままでも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
絞り込み機能を追加する
せっかくリストを作ったので、キーワードでデータを探せるようにしたいですね。
タイトルか著者のいずれかがキーワードと一致したレコードだけ表示できるようにしてみましょう。
一覧のページをちょっと改造して作ってみます。
最初に、BookListController
にfind
メソッドを実装します。
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") }
次に、Controllers
にBookListController.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 FrameworkのSkinnyCRUDMapperWithId
,primaryKeyFieldName
キーワードを参照)
1.0.4
のバグみたいです。
(追記2014-04-19)1.0.6
では直っています。
@argius それも revers-scaffold コマンドがよろしくやってくれるんですが 1.0.3 -> 1.0.4 で ParamType を拡張可能にしたら reverse-scaffold が壊れていることにきづきました.. 直します。
— Kazuhiro Sera (@seratch_ja) April 4, 2014
詳しくは、下記ページの後半をご覧ください。
他にもValidatorやMailerなどの便利な機能があります。テスト環境も充実しています。
今回はここまでです。
skinny自体もまだ新しいので、これからが楽しみです。