Skinny Framework Getting Started 日本語版
この記事は Scala福岡2016 - connpass でのハンズオン向けの入門記事です。
JDK (Java SE Development Kit) インストール
http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
Oracle 社の Web サイトにアクセスして、License Agreement に同意してから自分のプラットフォームにあったインストーラをダウンロードして実行してください。
インストール後はターミナルから java コマンドに PATH が通っていることを確認してください。java -version
でエラーにならず以下のような出力が表示されれば OK です。
$ java -version java version "1.8.0_71" Java(TM) SE Runtime Environment (build 1.8.0_71-b15) Java HotSpot(TM) 64-Bit Server VM (build 25.71-b15, mixed mode) $
skinny ダウンロード
Windows / Linux の場合
https://github.com/skinny-framework/skinny-framework/releases/download/2.1.1/skinny-blank-app-with-deps.zip をダウンロードして解凍したディレクトリで skinny スクリプトを使って以降の作業をします。
http://skinny-framework.org/ にダウンロードボタンがあります。
Mac OS X の場合
brew update brew install skinny
だけで OK です。Homebrew で始める場合は skinny new
コマンドが使えます。
skinny new hello-skinny cd hello-skinny
と実行してください。
Homebrew を使わない場合は Windows / Linux と同様、skinny-blank-app-with-deps.zip をダウンロードして解凍します。
これ以降の作業は全て共通です。
プロジェクトの動作確認
ここからは skinny スクリプトがあるディレクトリで作業します。Windows のコマンドプロンプトで作業する場合は ./skinny
を skinny
で読み替えてください。
$ ./skinny run [info] Loading project definition from /Users/kazuhirosera/tmp/hello-skinny/project [info] Set current project to skinny-blank-app-dev (in build file:/Users/kazuhirosera/tmp/hello-skinny/) 2016-05-23 21:45:31.340:INFO::pool-11-thread-3: Logging initialized @8585ms 2016-05-23 21:45:31.461:INFO:oejs.Server:pool-11-thread-3: jetty-9.2.17.v20160517 2016-05-23 21:45:31.858:INFO:oejw.StandardDescriptorProcessor:pool-11-thread-3: NO JSP Support for /, did not find org.eclipse.jetty.jsp.JettyJspServlet 2016-05-23 21:45:32.091 INFO --- [ool-11-thread-3] skinny.micro.SkinnyListener : The cycle class name from the config: Bootstrap 2016-05-23 21:45:32.260 DEBUG --- [ool-11-thread-3] skinny.micro.SkinnyListener : Loaded lifecycle class: class Bootstrap 2016-05-23 21:45:32.309 INFO --- [ool-11-thread-3] skinny.micro.SkinnyListener : Initializing life cycle class: Bootstrap 2016-05-23 21:45:32.739 DEBUG --- [ool-11-thread-3] scalikejdbc.ConnectionPool$ : Registered connection pool : ConnectionPool(url:jdbc:h2:file:./db/development;MODE=PostgreSQL;AUTO_SERVER=TRUE, user:sa) using factory : <default> 2016-05-23 21:45:33.288:INFO:oejsh.ContextHandler:pool-11-thread-3: Started o.e.j.w.WebAppContext@77261de5{/,[file:/Users/kazuhirosera/tmp/hello-skinny/src/main/webapp/],AVAILABLE} 2016-05-23 21:45:33.310:INFO:oejs.ServerConnector:pool-11-thread-3: Started ServerConnector@62b55b27{HTTP/1.1}{0.0.0.0:8080} 2016-05-23 21:45:33.311:INFO:oejs.Server:pool-11-thread-3: Started @10556ms [success] Total time: 2 s, completed May 23, 2016 9:45:33 PM 1. Waiting for source changes... (press enter to interrupt)
スタックトレースが出力されず 1. Waiting for source changes... (press enter to interrupt)
で止まれば OK です。
この状態で http://localhost:8080/ で Web サーバ(Jetty)が立ち上がっています。停止させたい場合は Enter や Ctrl + C を押すとアプリが停止します。
それでは IntelliJ IDEA を使いたい人向けの解説を続けます。セットアップの必要ないエディタを使う方は読み飛ばしてください。Eclipse は筆者が Scala では使っておらずお勧めもできないため省略させていただきます。
ファイルの説明
http://skinny-framework.org/documentation/getting-started.html#project-structure
. ├── README.md # 自動生成された README です、プロジェクトに合わせて書き換えてください ├── bin │ └── sbt-launch.jar # ./skinny や ./sbt が使う sbt の launcher です ├── build.sbt # これと project/Build.scala、project/plugins.sbt が sbt の設定ファイルです ├── project # sbt が使用するディレクトリです │ ├── Build.scala # メインのビルド設定ファイルです │ ├── build.properties # sbt 自体のバージョンを指定します、この記事時点で 0.13.11 が最新です │ └── plugins.sbt # sbt プラグインを追加する場合はここに追加します ├── sbt # ./skinny が使用する sbt 起動スクリプト ├── sbt.bat # 同上、Windows 向け ├── skinny # skinny スクリプト ├── skinny.bat # 同上、Windows 向け ├── src # Scala/Java のお作法的に src の下にソースコードや設定を置いていきます │ ├── main # こちらが実アプリのディレクトリ │ │ ├── resources # 設定ファイルなどを置く場所 │ │ │ ├── application.conf # アプリケーションの設定ファイルです │ │ │ ├── logback.xml # skinny が使用する logback というログライブラリの設定ファイルです │ │ │ └── messages.conf # 入力チェックエラーメッセージなどをここで指定します、i18n(国際化)対応 │ │ ├── scala # scala ソースコードの置き場所 │ │ │ ├── Bootstrap.scala # skinny アプリケーションが最初に呼び出すクラスです、名前は決め打ちです │ │ │ ├── controller # controller を置く場所です、自由に変更してもコンパイルが通るなら問題ありません │ │ │ │ ├── ApplicationController.scala # デフォルトの親 controller です、Ruby on Rails にならった命名ですが、リネームしても問題ありません │ │ │ │ ├── Controllers.scala # ルーティングはここで指定してください │ │ │ │ └── RootController.scala # http://localhost:8080/ はこの controller を呼び出します │ │ │ ├── lib # util クラスなど置き場に困るようなコードはここに置いてください │ │ │ ├── model # Rails でいう model としての置き場ですが、service や repository などが好みであれば変えても問題ありません │ │ │ │ └── package.scala # デフォルトでこの package 全体で共有したいものがあればここに書きます │ │ │ └── templates # Scalate というデフォルトのテンプレートエンジンが期待する package と class です │ │ │ └── ScalatePackage.scala # Scalate の設定 │ │ └── webapp # Servlet の規約で置かれているディレクトリ │ │ └── WEB-INF │ │ ├── assets │ │ │ ├── build.sbt # Scala.js 用の設定ファイルです、Scala.js を使わないなら不要です │ │ │ ├── coffee # CoffeeScript を使って開発したい場合はここに *.coffee を置きます │ │ │ ├── jsx # React を使って開発したい場合はここに *.jsx を置きます │ │ │ ├── less # LESS を使って開発したい場合はここに *.less を置きます │ │ │ ├── scala # Scala.js を使って開発したい場合はここに *.coffee を置きます │ │ │ └── scss # LESS を使って開発したい場合はここに *.sass/scss を置きます │ │ ├── layouts │ │ │ └── default.ssp # デフォルトのレイアウトテンプレートです │ │ ├── views │ │ │ ├── error # HTTP ステータス 40x/50x のときに表示されるエラーページです、デフォルトでは ssp が使われます │ │ │ │ ├── 403.html.jade │ │ │ │ ├── 403.html.mustache │ │ │ │ ├── 403.html.scaml │ │ │ │ ├── 403.html.ssp │ │ │ │ ├── 404.html.jade │ │ │ │ ├── 404.html.mustache │ │ │ │ ├── 404.html.scaml │ │ │ │ ├── 404.html.ssp │ │ │ │ ├── 406.html.jade │ │ │ │ ├── 406.html.mustache │ │ │ │ ├── 406.html.scaml │ │ │ │ ├── 406.html.ssp │ │ │ │ ├── 500.html.jade │ │ │ │ ├── 500.html.mustache │ │ │ │ ├── 500.html.scaml │ │ │ │ ├── 500.html.ssp │ │ │ │ ├── 503.html.jade │ │ │ │ ├── 503.html.mustache │ │ │ │ ├── 503.html.scaml │ │ │ │ └── 503.html.ssp │ │ │ └── root │ │ │ └── index.html.ssp # RootController が render("/root/index") を呼び出していますが、このファイルが読み込まれます │ │ └── web.xml # Servlet の設定ファイルです │ └── test # テストコード、テスト用の設定ファイルの置き場です │ ├── resources │ │ ├── factories.conf # FactoryGirl を使った fixture 用のファイルです │ │ └── logback.xml # テスト時に使用されるログ設定です │ └── scala # テストソースコードの置き場です │ ├── controller │ │ └── RootControllerSpec.scala # MockController を使った controller のテストです │ └── integrationtest │ └── RootController_IntegrationTestSpec.scala # Jetty を起動した HTTP リクエストによるインテグレーションテストです └── task └── src └── main └── scala └── TaskRunner.scala # db:migrate などのタスク実行設定がされているタスクランナーです
IntelliJ IDEA の設定
この説明は IntelliJ IDEA 2016.1.2 を前提としています。違うバージョンの場合、挙動が違う場合があるのでご注意ください。
まず Open を選んで、先ほど用意した skinny プロジェクトのディレクトリにアクセスします。
このようにディレクトリ自体が青色のアイコンになっていれば skinny プロジェクト(というより sbt プロジェクト)として認識されています。これ以降の手順を進めてください。
以下のスクリーンショットでは hello-skinny
となっていますが、zip を解凍した方は skinny-blank-app
となっていますので、読みかえてください。
もし普通のディレクトリのように肌色で表示されていたら、ターミナルから ./skinny idea
を実行してから IntelliJ IDEA の Open を試してください(一度 IDEA を再起動してから Open を試した方がいいかもしれません)。
このように sbt プロジェクトとして import する設定があらわれます。デフォルトで OK ですので、このまま進めてください。
このように処理が始まるのでしばらく待ちます。
このように 4 つのプロジェクトを読み込むかどうか聞かれますが、このまま OK を押してください。
おそらく No Scala SDK in module と表示されていて、Scala ソースコードもコンパイルエラー表示になっているかと思います。Setup Scala SDK
というリンクから設定してください。
このようなダイアログで OK を押します。Scala SDK が未設定の場合は洗濯して設定します。このスクリーンショットでは 2.11.7 になっていますが 2.11.8 が選べるならその方が望ましいですが 2.11.x ならどれでも大丈夫です。
しばらく待って src/main/scala/controller/RootController.scala などをクリックして開いてみて赤いコンパイルエラー表示がなくなっていればセットアップ完了です。
設定がおかしくなったら
- IntelliJ IDEA を終了させる
.idea
ディレクトリを削除する./skinny idea
コマンドを実行する- IntelliJ IDEA を起動して対象のディレクトリを Open して import を試みる
を試してみてください。
最初のコード生成
以下のページにならって最初のコードを自動生成してみましょう。
Getting Started - Skinny Framework
./skinny g scaffold members member name:String activated:Boolean luckyNumber:Option[Long] birthday:Option[LocalDate] ./skinny db:migrate ./skinny run
を実行するだけです。一つ一つのコマンドについて説明してきます。
$ ./skinny g scaffold members member name:String activated:Boolean luckyNumber:Option[Long] birthday:Option[LocalDate] [info] Running TaskRunner generate:scaffold members member name:String activated:Boolean luckyNumber:Option[Long] birthday:Option[LocalDate] *** Skinny Generator Task *** "src/main/scala/controller/ApplicationController.scala" skipped. "src/main/scala/controller/MembersController.scala" created. "src/main/scala/controller/Controllers.scala" modified. "src/test/scala/controller/MembersControllerSpec.scala" created. "src/test/scala/integrationtest/MembersController_IntegrationTestSpec.scala" created. "src/test/resources/factories.conf" modified. "src/main/scala/model/Member.scala" created. "src/test/scala/model/MemberSpec.scala" created. "src/main/webapp/WEB-INF/views/members/_form.html.ssp" created. "src/main/webapp/WEB-INF/views/members/new.html.ssp" created. "src/main/webapp/WEB-INF/views/members/edit.html.ssp" created. "src/main/webapp/WEB-INF/views/members/index.html.ssp" created. "src/main/webapp/WEB-INF/views/members/show.html.ssp" created. "src/main/resources/messages.conf" modified. "src/main/resources/db/migration/V20160523235117__Create_members_table.sql" created. [success] Total time: 8 s, completed May 23, 2016 11:51:17 PM
この時点で MVC のファイルが生成されて、ルーティング情報も設定済です。どのようになっているか skinny routes
で確認してみましょう。:id
は path パラメータで URL の一部が controller にパラメータとして渡されます。:id
の値は後述の members テーブルの id です。:ext
は json
か xml
でアクセスできます。
GET /? GET /assets/css/* GET /assets/js/* GET /members POST /members GET /members.:ext POST /members.:ext GET /members/ POST /members/ DELETE /members/:id GET /members/:id PATCH /members/:id POST /members/:id PUT /members/:id DELETE /members/:id.:ext GET /members/:id.:ext PATCH /members/:id.:ext POST /members/:id.:ext PUT /members/:id.:ext GET /members/:id/edit GET /members/new
手順に戻ります。./skinny db:migrate
でこのファイル DB に必要な members テーブルを作成します。
create table members ( id bigserial not null primary key, name varchar(512) not null, activated boolean not null, lucky_number bigint, birthday date, created_at timestamp not null, updated_at timestamp not null )
Skinny ではデフォルトでファイルベースのデータベースと連携するよう設定されていますが、MySQL などに変更も可能です。
$ ./skinny db:migrate [info] Running TaskRunner db:migrate 2016-05-23 23:51:34.150 DEBUG --- [ run-main-0] scalikejdbc.ConnectionPool$ : Registered connection pool : ConnectionPool(url:jdbc:h2:file:./db/development;MODE=PostgreSQL;AUTO_SERVER=TRUE, user:sa) using factory : <default> 2016-05-23 23:51:34.165 INFO --- [ run-main-0] o.f.core.internal.util.VersionPrinter : Flyway 4.0.1 by Boxfuse 2016-05-23 23:51:34.577 INFO --- [ run-main-0] o.f.c.i.dbsupport.DbSupportFactory : Database: jdbc:h2:file:./db/development (H2 1.4) 2016-05-23 23:51:34.722 INFO --- [ run-main-0] o.f.core.internal.command.DbValidate : Successfully validated 1 migration (execution time 00:00.020s) 2016-05-23 23:51:34.742 INFO --- [ run-main-0] o.f.c.i.metadatatable.MetaDataTableImpl : Creating Metadata table: "PUBLIC"."schema_version" 2016-05-23 23:51:34.766 INFO --- [ run-main-0] o.f.core.internal.command.DbMigrate : Current version of schema "PUBLIC": << Empty Schema >> 2016-05-23 23:51:34.766 INFO --- [ run-main-0] o.f.core.internal.command.DbMigrate : Migrating schema "PUBLIC" to version 20160523235117 - Create members table 2016-05-23 23:51:34.792 INFO --- [ run-main-0] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.050s). [success] Total time: 9 s, completed May 23, 2016 11:51:34 PM $
ここまでできていれば ./skinny run
を実行して 1. Waiting for source changes... (press enter to interrupt)
が表示されたら、http://localhost:8080/members にアクセスしてみてください。このような CRUD 画面が自動生成されているはずです。
「New」ボタンを押すとこのように validation も設定済の入力画面が表示されます。
テストも自動生成されているので実行してみましょう。
./skinny db:migrate test ./skinny test
で、以下のように出力されます。
$ ./skinny test [info] RootControllerSpec: [info] RootController [info] - shows top page [info] MembersController_IntegrationTestSpec: [info] - should show members [info] - should show a member in detail [info] - should show new entry form [info] - should create a member [info] - should show the edit form [info] - should update a member [info] - should delete a member [info] RootController_IntegrationTestSpec: [info] - should show top page [info] MemberSpec: [info] MembersControllerSpec: [info] MembersController [info] shows members [info] - shows HTML response [info] - shows JSON response [info] shows a member [info] - shows HTML response [info] shows new resource input form [info] - shows HTML response [info] creates a member [info] - succeeds with valid parameters [info] - fails with invalid parameters [info] - shows a resource edit input form [info] - updates a member [info] - destroys a member [info] Run completed in 13 seconds, 149 milliseconds. [info] Total number of tests run: 18 [info] Suites: completed 5, aborted 0 [info] Tests: succeeded 18, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 23 s, completed May 23, 2016 11:58:23 PM
生成されたコードを理解する
Controller
Controller のコードは差分のみが実装された非常にシンプルなものになっているのでどうすればいいか戸惑うのではないかと思います。
// $ cat src/main/scala/controller/MembersController.scala package controller import skinny._ import skinny.validator._ import _root_.controller._ import model.Member class MembersController extends SkinnyResource with ApplicationController { protectFromForgery() override def model = Member override def resourcesName = "members" override def resourceName = "member" override def resourcesBasePath = s"/${toSnakeCase(resourcesName)}" override def useSnakeCasedParamKeys = true override def viewsDirectoryPath = s"/${resourcesName}" override def createParams = Params(params).withDate("birthday") override def createForm = validation(createParams, paramKey("name") is required & maxLength(512), paramKey("lucky_number") is numeric & longValue, paramKey("birthday") is dateFormat ) override def createFormStrongParameters = Seq( "name" -> ParamType.String, "activated" -> ParamType.Boolean, "lucky_number" -> ParamType.Long, "birthday" -> ParamType.LocalDate ) override def updateParams = Params(params).withDate("birthday") override def updateForm = validation(updateParams, paramKey("name") is required & maxLength(512), paramKey("lucky_number") is numeric & longValue, paramKey("birthday") is dateFormat ) override def updateFormStrongParameters = Seq( "name" -> ParamType.String, "activated" -> ParamType.Boolean, "lucky_number" -> ParamType.Long, "birthday" -> ParamType.LocalDate ) }
ベタに書いた Controller や SkinnyResource の実装を見てみてわからない点があれば私やチューター係の人に聞いてみてください。
- https://github.com/skinny-framework/skinny-framework-example/blob/master/src/main/scala/controller/CompaniesController.scala
- https://github.com/skinny-framework/skinny-framework/blob/master/framework/src/main/scala/skinny/controller/SkinnyResourceActions.scala
ルーティング定義は src/main/scala/controller/Controllers.scala で有効になっています。ルーティング定義のサンプルはこの辺にあります。
- https://github.com/skinny-framework/skinny-framework/blob/master/example/src/main/scala/controller/Controllers.scala
- https://github.com/skinny-framework/skinny-framework-example/blob/master/src/main/scala/controller/Controllers.scala
Model
ここでいう Model は Ruby on Rails にならってデータベースアクセスが可能なモジュールというニュアンスです。実際に service レイヤーを設けて entity / DAO を分離して開発することもできますが Skinny のデフォルトのやり方は Ruby on Rails にならったこのスタイルです。データベースアクセスは以下のようにして REPL(Scala コードを実行する対話環境)で試してみましょう。
$ ./skinny console
REPL が起動したら、まずはデータが入っていない状態で全件取得してみましょう。
scala> Member.findAll() [SQL Execution] select m.id as i_on_m, m.name as n_on_m, m.activated as a_on_m, m.lucky_number as ln_on_m, m.birthday as b_on_m, m.created_at as ca_on_m, m.updated_at as ua_on_m from members m order by m.id; (0 ms) [Stack Trace] ... skinny.orm.feature.FinderFeatureWithId$class.findAll(FinderFeature.scala:57) model.Member$.findAll(Member.scala:17) ... res1: List[model.Member] = List()
レコードを insert してみましょう。
scala> Member.createWithAttributes('name -> "Alice", 'activated -> false) [SQL Execution] insert into members (name, activated, created_at, updated_at) values ('Alice', false, '2016-05-24 00:17:34.008', '2016-05-24 00:17:34.008'); (0 ms) [Stack Trace] ... skinny.orm.feature.CRUDFeatureWithId$class.createWithNamedValues(CRUDFeature.scala:213) model.Member$.skinny$orm$feature$TimestampsFeatureWithId$$super$createWithNamedValues(Member.scala:17) skinny.orm.feature.TimestampsFeatureWithId$class.createWithNamedValues(TimestampsFeature.scala:24) model.Member$.createWithNamedValues(Member.scala:17) skinny.orm.feature.NoIdCUDFeature$class.createWithAttributes(NoIdCUDFeature.scala:122) model.Member$.skinny$orm$feature$CRUDFeatureWithId$$super$createWithAttributes(Member.scala:17) skinny.orm.feature.CRUDFeatureWithId$class.createWithAttributes(CRUDFeature.scala:277) model.Member$.createWithAttributes(Member.scala:17) ... res3: Long = 2
この状態でレコード件数をカウントしてみます。1 件になっているはずです。
scala> Member.count() [SQL Execution] select count(1) from members; (4 ms) [Stack Trace] ... skinny.orm.feature.CalculationFeature$class.count(CalculationFeature.scala:30) model.Member$.count(Member.scala:17) ... res5: Long = 1
このように where 句を指定することもできます。
scala> Member.where('name -> "Alice").where('activated -> false).apply() [SQL Execution] select m.id as i_on_m, m.name as n_on_m, m.activated as a_on_m, m.lucky_number as ln_on_m, m.birthday as b_on_m, m.created_at as ca_on_m, m.updated_at as ua_on_m from members m where m.name = 'Alice' and m.activated = false; (0 ms) [Stack Trace] ... skinny.orm.feature.QueryingFeatureWithId$EntitiesSelectOperationBuilder.apply(QueryingFeature.scala:326) ... res8: List[model.Member] = List(Member(2,Alice,false,None,None,2016-05-24T00:17:34.008+09:00,2016-05-24T00:17:34.008+09:00))
より詳しい操作についてはドキュメントを参考にしてみてください。
View
表示する部分は上記のコマンドの場合、SSP (Scala Server Pages) で生成されています。JSP や Velocity に似たものでループや分岐など必要な処理を素直に書くことができます。
公式ドキュメントや Scalate のドキュメントを参照してください。
http://skinny-framework.org/documentation/view-templates.html
またこちらは Play Framework を使ったサンプルになりますが view template は認証ページなどのみにとどめてサーバから JSON のみを返して JavaScript で処理するようにしても良いでしょう。
https://github.com/skinny-framework/skinny-orm-in-play
やってみよう
TODO 管理アプリを作ってみる
task テーブルを設計して scaffold して TODO 管理アプリを scaffold してみましょう。JavaScript + JSON API の構成にして TodoMVC にしてもよいですね。
一応、こちらでサンプルをつくってみましたので、迷ったら参考にしてみてください。
GitHub - seratch/scala-fukuoka-hands-on-demo: Skinny Workshop at Scala 福岡 2016
Typetalk 連携のアプリを作ってみる
以下は今回の会場を提供してくださっているヌーラボ様の Typetalk の OAuth 2.0 と連携するサンプルアプリケーションです。
これをベースに Typetalk に投稿するアプリをつくったり、他のサービスでもログインできるようにしてみたりしてみてはどうでしょう?
see also: http://skinny-framework.org/documentation/oauth.htmlskinny-framework.org
Redmine のデータベースから reverse-scaffold してみる
以下は Redmine のデータベースから reverse scaffold してみた結果です。生成してから全く手を加えずにちゃんと動いています。
Skinny の reverse-scaffold は FK(外部キー)がない場合は勝手に association を生成しません。上記の model クラスに適切な belongsTo や hasMany を定義してみて ./skinny console
で join クエリの動作を確認してみるのも良いでしょう。また、何か他の既存データベースをつかって生成してみてもよいでしょう。
Scala.js を試してみる
Skinny では ./skinny scalajs:watch
を実行するだけで Scala.js を使った開発を始めることができます。Scala.js に興味がある方はこれを機にぜひ触ってみてください。
Assets Support - Skinny Framework
FAQ
java.net.BindException: Address already in use
というエラーが出たら、すでに別の terminal で./skinny run
していないか確認してくださいUnsupported major.minor version 51.0
のようなエラーは JDK のバージョンが古くないかjava -version
で確認してください