seratch's weblog in Japanese

About Scala, Java and Ruby programming in Japaense. If you need English information, go to http://blog.seratch.net/

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 に同意してから自分のプラットフォームにあったインストーラをダウンロードして実行してください。

f:id:seratch2:20160523212824p:plain:w500

インストール後はターミナルから 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/ にダウンロードボタンがあります。

f:id:seratch2:20160523213032p:plain:w500

Mac OS X の場合

Mac OS XHomebrew を使っているなら

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コマンドプロンプトで作業する場合は ./skinnyskinny で読み替えてください。

$ ./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 プロジェクトのディレクトリにアクセスします。

f:id:seratch2:20160523234750p:plain:w500

このようにディレクトリ自体が青色のアイコンになっていれば skinny プロジェクト(というより sbt プロジェクト)として認識されています。これ以降の手順を進めてください。

以下のスクリーンショットでは hello-skinny となっていますが、zip を解凍した方は skinny-blank-app となっていますので、読みかえてください。

もし普通のディレクトリのように肌色で表示されていたら、ターミナルから ./skinny idea を実行してから IntelliJ IDEA の Open を試してください(一度 IDEA を再起動してから Open を試した方がいいかもしれません)。

f:id:seratch2:20160523234813p:plain:w500

このように sbt プロジェクトとして import する設定があらわれます。デフォルトで OK ですので、このまま進めてください。

f:id:seratch2:20160523234823p:plain:w500

このように処理が始まるのでしばらく待ちます。

f:id:seratch2:20160523234833p:plain:w500

このように 4 つのプロジェクトを読み込むかどうか聞かれますが、このまま OK を押してください。

f:id:seratch2:20160523234846p:plain:w500

おそらく No Scala SDK in module と表示されていて、Scala ソースコードコンパイルエラー表示になっているかと思います。Setup Scala SDK というリンクから設定してください。

f:id:seratch2:20160523234907p:plain:w500

このようなダイアログで OK を押します。Scala SDK が未設定の場合は洗濯して設定します。このスクリーンショットでは 2.11.7 になっていますが 2.11.8 が選べるならその方が望ましいですが 2.11.x ならどれでも大丈夫です。

f:id:seratch2:20160523234918p:plain:w500

しばらく待って src/main/scala/controller/RootController.scala などをクリックして開いてみて赤いコンパイルエラー表示がなくなっていればセットアップ完了です。

f:id:seratch2:20160523234930p:plain:w500

設定がおかしくなったら

を試してみてください。

最初のコード生成

以下のページにならって最初のコードを自動生成してみましょう。

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 です。:extjsonxml でアクセスできます。

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 画面が自動生成されているはずです。

f:id:seratch2:20160528134757p:plain:w500

「New」ボタンを押すとこのように validation も設定済の入力画面が表示されます。

f:id:seratch2:20160523235553p:plain:w500

テストも自動生成されているので実行してみましょう。

./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 の実装を見てみてわからない点があれば私やチューター係の人に聞いてみてください。

ルーティング定義は 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))

より詳しい操作についてはドキュメントを参考にしてみてください。

ORM - Skinny Framework

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 と連携するサンプルアプリケーションです。

github.com

これをベースに Typetalk に投稿するアプリをつくったり、他のサービスでもログインできるようにしてみたりしてみてはどうでしょう?

see also: http://skinny-framework.org/documentation/oauth.htmlskinny-framework.org

Redmine のデータベースから reverse-scaffold してみる

以下は Redmine のデータベースから reverse scaffold してみた結果です。生成してから全く手を加えずにちゃんと動いています。

github.com

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 で確認してください