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

Skinny Meetup Tokyo 2 を開催しました #skinnyjp

Skinny Framework 2.0 をリリースして一ヶ月弱、@yusuke さんのご厚意により、東池袋サムライズムのオフィスをお借りして Skinny Meetup Tokyo の第 2 回を開催させていただきました。

skinnyjp.doorkeeper.jp

当日使ったスライドはこちら。

www.slideshare.net

英語版はこちら。

www.slideshare.net

関連する tweetTogetter にまとめました。

togetter.com

当日の内容

Meetup の流れは、まず、私の方から Skinny が 2.0 で変わったこと(とちょっとだけさだまさしコンパイラのデモ)をお話しました。次に、社内の情報共有ツールの実装に Skinny Framework を使ってくださっているという @roundrop さんから、その社内情報共有ツールについての紹介、初めての Scala 開発で Skinny Framework を使ってどうだったかという所感についてお話いただきました。具体的には以下のインタビューで触れられている Siita というツールです。

www.atware.co.jp

数年 Scala をやっている身からするとすっかり忘れかけていたような、初めて Scala をやったときに困った点などもバランスよくまとめていただいていたので、おそらくこれから Scala、Skinny をやってみようという方々に大変参考になったのではないかと思います。この発表のスライドは後日公開いただけるとのことでしたので、楽しみにしております。 (追記)公開していただいたので、以下に埋め込みました。

www.slideshare.net

サムライズム最高

当初は一人千円いただいてピザとドリンクを準備予定でしたが、サムライズム @yusuke さんのご厚意に甘えさせていただきまして、なんと無料で懇親会を行うことができました。再度、お礼申し上げる次第です。

tweet とこのブログで紹介させていただく程度のことしかできませんが、以下のようにサムライズムでは OSS コミュニティも応援されているそうです。また IntelliJ IDEA や JRebel の業務導入を検討されている企業様におかれましては、ぜひ日本の商慣行にも柔軟に対応しているサムライズム社でご購入いただければと思います!

Skinny Micro

今回、初めて Skinny Micro についてちゃんと説明させていただきましたが、Scalatra を fork してリファクタリング、機能追加したものでかなり手軽で使いやすいライブラリになったのではないかと思っています。

Skinny Micro

どれくらい手軽かというと、以下に示すこれだけのコードで Web アプリをつくれるくらい手軽です。この Web アプリは Jetty サーバで起動して curl -v 'localhost:4567/say-hello?name=Martin' というリクエストに対して Hello, Martin! というレスポンスボディを返します。

https://github.com/skinny-framework/skinny-micro/tree/master/scalas-samples

#!/usr/bin/env scalas
// or ./scalas Hello.scala
/***
scalaVersion := "2.11.7"
libraryDependencies += "org.skinny-framework" %% "skinny-micro-server" % "1.0.0"
*/
import skinny.micro._
object HelloApp extends WebApp {
  get("/say-hello") {
    s"Hello, ${params.getOrElse("name", "Anonymous")}!\n"
  }
}
WebServer.mount(HelloApp).port(4567).start()

Heroku にすぐにデプロイできるサンプルや基本的な使い方を列挙したサンプルもありますので、ご参照いただければと思います。

https://github.com/skinny-framework/skinny-micro-usage-example

https://github.com/skinny-framework/skinny-micro-heroku-example

まだ公開して日が浅く GitHub スター数がちょっと少なめなので、気に入った方はぜひ stargazer になっていただけると嬉しいです。

github.com

Skinny の今後

Skinny Framework

Skinny 2 は大きな非互換を入れたわけでなく Scalatra からの移行という大きなイベントがあったのでメジャーバージョンを上げたという事情がありました。既に 1.3 でフレームワーク本体で備えるべき機能はそれなりに揃ってきているという認識で、大きくコードベースを変えていくことは考えていません。しかし、当然ながらソフトウェアに完成ということはありませんし、また利用者からするとメンテナンスされ続けていることは非常に重要なことですので、これからも手を入れ続け、マイナーバージョンでのリリースは続けていきます。

Skinny Framework の売りとしては、以下のような内容に共感いただける現場・用途には非常にマッチすると考えています。改めて明文化してみましたが、これは公式サイトにも明記しておくべきですね。

  • 今後も Scala 界における Servlet での Web アプリケーション開発フレームワークデファクトを目指し続けます
  • 今後も(既にコモディティ化している)Rails のイディオムや基本的なコンセプトに近い立場を保つことで、その下地がある開発者にとって敷居の低いフレームワークであり続けます
  • 既に 1.3 で当初予定していた最低限の機能セットは実現できたと考えており、安定フェーズとしてプロジェクトを運営していきます
  • 外的要因(Scala 本体など)の大きな変化以外での非互換な変更は基本的に入れない方針で運営していきます
  • Servlet に依存していないサブモジュール(ORM や Validator、HTTP クライアントなど)は今後も独立したライブラリとして利用可能であることを保証し続けます
  • 日本語圏に閉じず、世界中のさらなるユーザ拡大に努めてガラパゴス化のリスクを避ける取り組みを今後も続けます

特に、運用に入ってからのメンテコストを抑えたい、新しいメンバのキャッチアップのしやすさを重視したいという目的の場合、期待に答えられるフレームワークだと思っておりますので、選択肢の一つとしてご検討いただければと思います。

まだ時期やプランの詳細は未定なのですが、来年は Skinny Framework を業務の現場で利用いただきやすくなるようなサポートにもチャレンジできないかと検討しているところですので、引き続きよろしくお願いいたします。

ScalikeJDBC / Skinny Framework グッズ 2015 春モデル

SUZURI 最高

去年から GMO ペパボさんの suzuri.jp を利用して Skinny Framework のロゴをプリントしたマグカップを販売しています。

これまでに 13 個お買い上げいただいております。ありがとうございます!*1

Skinny Framework 2015 Spring

さて、春ですし 2015 春モデルと称して新しく T シャツもつくってみました。

お子さんが生まれた方はぜひロンパースを。

暖かくなってきたので時期的にちょっと微妙ですが、スウェットもつくってみました。

ScalikeJDBC 2015 Spring

待望の? ScalikeJDBC グッズもつくりました。ScalikeJDBC を使って開発されている方もどうぞ。

みんなで買えば送料がお得

販売しているグッズ一覧はこちらで見ることができます。

https://suzuri.jp/seratch

複数口で買うと送料がお得になります。お買い上げいただける際はぜひ開発チーム、部署、会社単位でまとめてご注文ください!

*1:トリブンは 0 円設定なので、私の懐には 1 円も入れておりませんが

Future についての私見

qiita.com

この記事を読んで、記事の本筋からはそれますが、前から思っていたことをつぶやいたのでまとめておきます。

「それ、今まで通り同期処理で Web ページ返す処理つくるだけで全然いいよね」という要件なのに、非同期縛りをやるのはちょっと不毛な気がするな...という。超高負荷な環境で CPU 使用抑えたい、同時接続が多いんで、とか何かしらの強いモチベーションがあることが大事で、それ以外は適材適所がよいと思うのです。

もちろん新しいアプローチにトライするのは楽しいことだし、そういうコミュニティの盛り上がりは素晴らしいことなので水を差したいわけでは全くないんだけど、仕事でやる開発において「Play とか Scala が盛り上がってる。Rails でやってた案件を今度は Scala でやろう。」みたいなことだとまず幸せにならないなと。小規模な案件は実はただもう少し静的型が欲しいだけなんじゃないかなって思ったりしています。

Java を Scala の中で使うこと

ScalikeJDBC は手軽なはず

ScalaでORマッパーというとSlickやScalikeJDBC等色々あるが、ほんとにちょっとしたツールを作りたいだけならばもっと手軽にやりたいと思うはず。

ScalaでDBを使った小物ツールをサクッと作れるJDBI - 気まぐれラボラトリィ

手軽ですよということを示すためにちょっとしたサンプルを書きました。ScalikeJDBC や Skinny ORM のミニマムな導入方法としても参考になるかなと思います。

https://github.com/seratch/jdbi-scalikejdbc-skinny-orm-example/blob/master/src/main/scala/app.scala

JDBI については私も結構好きなんですが Java であることの限界が見えてしまっている感はあります。SQLアノテーションに書くわけですが、改行もできないですし、アノテーションに渡す文字列はベタ書きするしかないので、こういう簡単なケースではなく、それなりの SQL を書くとなるとかなり苦しいと言わざるをえません。

また Java ライブラリ一般に言えることとして @BeanProperty とか Java っぽいアノテーション駆動な処理が入るコードでは Scala の恩恵を受けづらいことが多いと思います。コンパイルの重さを考慮するとそういうケースでは Groovy の方がよかったりするかもしれません。

JavaScala の中で使うこと

今更という気もしますが、ちょうどよい機会なので Scala の中で Java を使うことについて意見を書いておこうかなと思います。

いざとなれば Java の既存コードをそのまま使えるのは Scala のよいところなのですが、あくまで「いざとなれば」というオプションであり、かなりニッチな分野であればまだしも、RDB アクセスのようなありふれたユースケースではファーストチョイスが Java の利用とは考えない方が幸せになれると思います。

自分自身を振り返っても Scala を始めた頃は「手慣れた Java のライブラリを使ってやった方が楽なのでは?」と思ったりしたものですが、結局 JavaScala の架け橋となるところをケアしながら書かないといけないので簡潔にはなりにくく、落とし穴もあったりします。

例えば null は Scala の世界では「なかったこと」になっていて Option にしてあげないといけないですが、そういうことをやってくれる Java のライブラリはまずないので JavaAPI からの戻り値を自分で Option で包んであげないといけなかったりします。また Scala のコレクション型のオブジェクトは Java の世界からはうまく扱えないので Java の世界に渡す前に必ず scala.collection.JavaConverters なりで変換してあげる必要がありますし、逆に java.util.List などが Java から返ってくれば毎回 #asScala を呼んで変換する必要があります。

どういうときに Java を混ぜるか

ということで Scala であえて Java API を使う場合というのは、既に JavaSDK が提供されている場合だったり、よほど Scala で実装されたライブラリで選択肢がないケース(枯れてない、難解すぎる、機能が足りない、メンテされてないなど)に限るのがよいかと思います。

「どうしてもこの Java ライブラリを流用したい」というケースはあるかと思いますが、それほどのものなのであれば自分で Scala ラッパーを書いてみることも検討してもよいでしょう。OSS として公開すれば一定の需要もあるかもしれません。

以上、あるべき論で Scala らしいコードを書くべきということではなく「(ニッチなケースでなければ)普通は Scala ネイティブなやり方をした方が結果的に楽ですよ」という話でした。

Anorm についてのふりかえり

Twitter 上でのやりとりを読んでいて、Anorm は 2.4 から Play Framework 本体からは分離されたとはいえ Play チームがメンテし続けるであろうライブラリであることには変わりないので、機能の比較以前にそういったバックアップ体制を重視するなら Anorm を選択するという判断もあるのかなと思いました。

https://github.com/playframework/anorm

Anorm といえば Play 2.0 が出た直後の勉強会で Anorm のコードをざっと読んで短い発表をしたことがありました。当時の Scala の DB ライブラリ事情は ScalQuery、Squeryl、Lift Mapper、Querulous(MySQL のみ対応)といったあたりがメジャーどころで ScalikeJDBC は Querulous にインスパイアされた初期バージョンの段階でした。

http://www.slideshare.net/seratch/reading-anorm-20-12238243

当時の私の印象は「何かすごくいい点があるというわけではないが ScalaQuery とかに比べると使い方は単純でわかりやすそう、SQL を書いて次にどう取り出すか書くという API も直感に合っているし、いろんな場面でツールとしては使えるのかもしれないな」という感じでした。ScalikeJDBC にもその後 SQL オブジェクトの API を追加しましたが、これは見て明らかな通り Anorm に強く影響されています。

Anorm と ScalikeJDBC の比較

Anorm に影響を受けている ScalikeJDBC の機能面での優位性を挙げるなら Anorm でできることができるだけでなく interpolation に SQLSyntax として bind 変数以外の SQL の部品を安全に埋め込める機能があることが最も大きいかと思います。

http://scalikejdbc.org/documentation/sql-interpolation.html

Anorm の interpolation には同等の機能は現在も存在していません(Slick の StaticQuery は #$ で外部パラメータを何でも埋めることができるようです)。他のライブラリがこういうアプローチを真似しない理由はよくわかりませんが、ある程度こういうサポートがないと join クエリをたくさん書いたりする場合にかなりしんどいのではないかなと思います。少なくとも Scala 2.9 時代の ScalikeJDBC はそこがつらいなと自分でも感じていました。

逆に Anorm にあって ScalikeJDBC にないものを挙げるとすれば、あの Parser API かなと思います。

ドキュメントの写経

そもそもちゃんと比較したことがなかったので、思い立って Anorm のドキュメントを ScalikeJDBC で書いてみることにしました。久しぶりに Anorm のドキュメントを眺めてみましたが、2.3.x のものは同じページの中にコピペらしき重複があったりしますね。2.4.x ではそこは直っていましたが。

https://www.playframework.com/documentation/2.3.x/ScalaAnorm

https://www.playframework.com/documentation/2.4.x/ScalaAnorm

書いてみましたが... Anorm 固有の事情や制限のための例が多く、差を出しづらかったので... 途中でやめてしまいました。興味があればどなたかやってみてください。しかし、今となっては interpolation がないコードを書くのは結構つらいですね。

project/build.properties

sbt.version=0.13.7

project/plugins.sbt

addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0")
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")

build.sbt

scalaVersion := "2.11.5"
libraryDependencies ++= Seq(
  "org.scalikejdbc" %% "scalikejdbc"       % "2.2.4",
  "com.h2database"  %  "h2"                % "1.4.185",
  "ch.qos.logback"  %  "logback-classic"   % "1.1.2"
)
scalariformSettings

src/main/scala/Example.scala

import scalikejdbc._

object Example extends App {

  // initialize JDBC driver & connection pool
  Class.forName("org.h2.Driver")
  ConnectionPool.singleton("jdbc:h2:mem:hello;MODE=PostgreSQL", "user", "pass")
  implicit val session = AutoSession

  sql"create table City (id serial not null primary key, name varchar(100), country varchar(100))".execute.apply()
  sql"create table Country (Code varchar(3) not null primary key, Name varchar(100), Population bigint)".execute.apply()
  sql"create table CountryLanguage (id serial not null primary key, Language varchar(100), CountryCode varchar(3) references Country(Code))".execute.apply()
  sql"create table prod (id varchar(10) not null primary key, name varchar(100), price float)".execute.apply()
  sql"create table tbl (str_arr array)".execute.apply()
  sql"create table item (id varchar(10) not null primary key, last_modified timestamp)".execute.apply()
  sql"create table books (title varchar(100), author varchar(100))".execute.apply()
  sql"create table test (id varchar(10) not null primary key, cat varchar(10), a varchar(10), b varchar(10), c varchar(10), colA varchar(10), colB varchar(10))".execute.apply()

  // ----------------------------------------------
  // Anorm Documentation Examples with ScalikeJDBC
  // https://www.playframework.com/documentation/2.3.x/ScalaAnorm
  // ----------------------------------------------

  {
    /*
import anorm._
import play.api.db.DB
DB.withConnection { implicit c =>
  val result: Boolean = SQL("Select 1").execute()
}
     */
    val result: Boolean = DB.autoCommit { implicit s =>
      SQL("select 1").execute.apply()
    }
  }

  {
    /*
val result: Int = SQL("delete from City where id = 99").executeUpdate()
     */
    val result: Int = SQL("delete from City where id = 99").update.apply()
  }

  {
    /*
val id: Option[Long] =
  SQL("insert into City(name, country) values ({name}, {country})")
  .on('name -> "Cambridge", 'country -> "New Zealand").executeInsert()
     */
    val id: Long =
      SQL("insert into City(name, country) values ({name}, {country})")
        .bindByName('name -> "Cambridge", 'country -> "New Zealand")
        .updateAndReturnGeneratedKey
        .apply()
  }

  {
    /*
import anorm.SqlParser.str
val id: List[String] =
  SQL("insert into City(name, country) values ({name}, {country})")
  .on('name -> "Cambridge", 'country -> "New Zealand")
  .executeInsert(str.+) // insertion returns a list of at least one string keys
     */
    // Although ScalikeJDBC 2.2 doesn't support multiple generated keys, it's possible to specify generated key to be returned
    val id: Long = SQL("insert into City(name, country) values ({name}, {country})")
      .bindByName('name -> "Cambridge", 'country -> "New Zealand")
      .updateAndReturnGeneratedKey("id")
      .apply()
  }

  {
    /*
import anorm.{ SQL, SqlParser }
val code: String = SQL(
  """
    select * from Country c
    join CountryLanguage l on l.CountryCode = c.Code
    where c.code = {countryCode}
  """)
  .on("countryCode" -> "FRA").as(SqlParser.str("code").single)
     */
    val code: Option[String] = SQL("""
      select * from Country c
      join CountryLanguage l on l.CountryCode = c.Code
      where c.code = {countryCode}
      """).bindByName('countryCode -> "FRA").map(rs => rs.get[String]("code")).single.apply()
  }

  {
    /*
// Parsing column by name or position
val parser =
  SqlParser(str("name") ~ float(3) map {
    case name ~ f => (name -> f)
  }
val product: (String, Float) = SQL("SELECT * FROM prod WHERE id = {id}").
  on('id -> "p").as(parser.single)
     */
    val parser = (rs: WrappedResultSet) => rs.get[String]("name") -> rs.get[Float](3)
    val product: Option[(String, Float)] = SQL("SELECT * FROM prod WHERE id = {id}")
      .bindByName('id -> "p").map(parser).single.apply()
  }

  {
    /*
val name = "Cambridge"
val country = "New Zealand"
SQL"insert into City(name, country) values ($name, $country)"
     */
    val (name, country) = ("Cambridge", "New Zealand")
    sql"insert into City(name, country) values ($name, $country)"
    /*
val lang = "French"
val population = 10000000
val margin = 500000
val code: String = SQL"""
  select * from Country c
    join CountryLanguage l on l.CountryCode = c.Code
    where l.Language = $lang and c.Population >= ${population - margin}
    order by c.Population desc limit 1"""
  .as(SqlParser.str("Country.code").single)
     */
    val lang = "French"
    val population = 10000000
    val margin = 500000
    val code: Option[String] = sql"""
      select * from Country c
        join CountryLanguage l on l.CountryCode = c.Code
        where l.Language = $lang and c.Population >= ${population - margin}
        order by c.Population desc limit 1"""
      .map(_.get[String]("Country.code")).single.apply()
  }

  {
    /*
// Create an SQL query
val selectCountries = SQL("Select * from Country")
// Transform the resulting Stream[Row] to a List[(String,String)]
val countries = selectCountries().map(row =>
  row[String]("code") -> row[String]("name")
).toList
     */
    val selectCountries = sql"Select * from Country"
    val countries = selectCountries.map(rs => rs.get[String]("code") -> rs.get[String]("name")).toList.apply()
    /*
// First retrieve the first row
val firstRow = SQL("Select count(*) as c from Country").apply().head
// Next get the content of the 'c' column as Long
val countryCount = firstRow[Long]("c")
     */
    val countryCount: Long = sql"Select count(*) as c from Country".map(_.long("c")).single.apply().get
  }

  {
    /*
// With default formatting (", " as separator)
SQL("SELECT * FROM Test WHERE cat IN ({categories})").
  on('categories -> Seq("a", "b", "c")
    */
    val categories = Seq("a", "b", "c")
    sql"SELECT * FROM Test WHERE cat IN (${categories})"
    /*
// With custom formatting
import anorm.SeqParameter
SQL("SELECT * FROM Test t WHERE {categories}").
  on('categories -> SeqParameter(
    values = Seq("a", "b", "c"), separator = " OR ",
    pre = "EXISTS (SELECT NULL FROM j WHERE t.id=j.id AND name=",
    post = ")"))
    */
    val pre = sqls"EXISTS (SELECT NULL FROM j WHERE t.id=j.id AND name="
    val condition = sqls.joinWithOr(categories.map(c => sqls"$c"): _*)
    val post = sqls")"
    sql"SELECT * FROM Test t WHERE ${pre}${condition}${post}"
  }

  {
    /*
import anorm.SQL
import anorm.SqlParser.{ scalar, * }
// array and element parser
import anorm.Column.{ columnToArray, stringToArray }
val res: List[Array[String]] =
  SQL("SELECT str_arr FROM tbl").as(scalar[Array[String]].*)
     */
    val res = sql"SELECT str_arr FROM tbl".map(_.get[java.sql.Array]("str_arr")).list.apply()
  }

  // Batch update
  {
    /*
import anorm.BatchSql
val batch = BatchSql(
  "INSERT INTO books(title, author) VALUES({title}, {author}",
  Seq(Seq[NamedParameter](
    "title" -> "Play 2 for Scala", "author" -> Peter Hilton"),
    Seq[NamedParameter]("title" -> "Learning Play! Framework 2",
      "author" -> "Andy Petrella")))
val res: Array[Int] = batch.execute() // array of update count
     */
    val paramsList = Seq(
      Seq('title -> "Play 2 for Scala", 'author -> "Peter Hilton"),
      Seq('title -> "Learning Play! Framework 2", 'author -> "Andy Petrella")
    )
    sql"INSERT INTO books(title, author) VALUES({title}, {author})"
      .batchByName(paramsList: _*).apply()
  }

  // Edge cases
  {
    /*
// Wrong #1
val p: Any = "strAsAny"
SQL("SELECT * FROM test WHERE id={id}").on('id -> p) // Erroneous - No conversion Any => ParameterValue
// Right #1
val p = "strAsString"
SQL("SELECT * FROM test WHERE id={id}").on('id -> p)
*/

    val p: Any = "strAsAny"
    // ScalikeJDBC binds params with their actual types
    SQL("SELECT * FROM test WHERE id = {id}").bindByName('id -> p)
      .toMap.list.apply()

    /*
// Wrong #2
val ps = Seq("a", "b", 3) // inferred as Seq[Any]
SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").
  on('a -> ps(0), // ps(0) - No conversion Any => ParameterValue
    'b -> ps(1), 'c -> ps(2))
// Right #2
val ps = Seq[anorm.ParameterValue]("a", "b", 3) // Seq[ParameterValue]
SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").
  on('a -> ps(0), 'b -> ps(1), 'c -> ps(2))
*/
    val ps = Seq("a", "b", 3) // inferred as Seq[Any]
    SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}")
      .bindByName('a -> ps(0), 'b -> ps(1), 'c -> ps(2))
      .toMap.list.apply()

    /*
// Wrong #3
val ts = Seq( // Seq[(String -> Any)] due to _2
  "a" -> "1", "b" -> "2", "c" -> 3)
val nps: Seq[NamedParameter] = ts map { t =>
  val p: NamedParameter = t; p
  // Erroneous - no conversion (String,Any) => NamedParameter
}
SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").on(nps :_*)
// Right #3
val nps = Seq[NamedParameter]( // Tuples as NamedParameter before Any
  "a" -> "1", "b" -> "2", "c" -> 3)
SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").
  on(nps: _*) // Fail - no conversion (String,Any) => NamedParameter
*/
    val ts = Seq( // Seq[(String -> Any)] due to _2
      "a" -> "1", "b" -> "2", "c" -> 3)
    SQL("SELECT * FROM test WHERE (a={a} AND b={b}) OR c={c}").bindByName(ts.map { case (k, v) => Symbol(k) -> v }: _*)
      .toMap.list.apply()

    /*
import anorm.features.anyToStatement
val d = new java.util.Date()
val params: Seq[NamedParameter] = Seq("mod" -> d, "id" -> "idv")
// Values as Any as heterogenous
SQL("UPDATE item SET last_modified = {mod} WHERE id = {id}").on(params:_*)
 */
    val (mod, id) = (new java.util.Date(), "idv")
    sql"UPDATE item SET last_modified = ${mod} WHERE id = ${id}".update.apply()

  }

  {
    /*
case class SmallCountry(name:String)
case class BigCountry(name:String)
case class France
val countries = SQL("Select name,population from Country")().collect {
  case Row("France", _) => France()
  case Row(name:String, pop:Int) if(pop > 1000000) => BigCountry(name)
  case Row(name:String, _) => SmallCountry(name)
}
*/
    sealed trait Country
    case class SmallCountry(name: String) extends Country
    case class BigCountry(name: String) extends Country
    case object France extends Country

    val queryResults = sql"Select name,population from Country"
      .map { rs => rs.get[String]("name") -> rs.get[Int]("population") }
      .list.apply()
    val countries: Seq[Country] = queryResults.map {
      case ("France", _) => France
      case (name, pop) if pop > 1000000 => BigCountry(name)
      case (name, pop) => SmallCountry(name)
    }

  }

  // Using for-comprehension
  {
/*
import anorm.SqlParser.{ str, int }
val parser = for {
  a <- str("colA")
  b <- int("colB")
} yield (a -> b)
val parsed: (String, Int) = SELECT("SELECT * FROM Test").as(parser.single)
*/
     // for-comprehension is not suitable for ScalikeJDBC
     sql"SELECT * FROM Test".map(rs => rs.get[String]("colA") -> rs.get[Int]("colB")).list.apply()
  }

}

specs2 で unresolved dependency: org.scalaz.stream#scalaz-stream_2.11;0.5a: not found

こんな感じのエラーになってググってここにたどり着いたでしょうか?

[info] Resolving org.scalaz.stream#scalaz-stream_2.11;0.5a ...
[warn]  module not found: org.scalaz.stream#scalaz-stream_2.11;0.5a
[warn] ==== local: tried
[warn]   /Users/k-sera/.ivy2/local/org.scalaz.stream/scalaz-stream_2.11/0.5a/ivys/ivy.xml
[warn] ==== public: tried
[warn]   https://repo1.maven.org/maven2/org/scalaz/stream/scalaz-stream_2.11/0.5a/scalaz-stream_2.11-0.5a.pom
[info] Resolving jline#jline;2.12 ...
[info] downloading https://repo1.maven.org/maven2/org/specs2/specs2_2.11/2.4.4/specs2_2.11-2.4.4.jar ...
[info]  [SUCCESSFUL ] org.specs2#specs2_2.11;2.4.4!specs2_2.11.jar (56915ms)
[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
[warn]  ::          UNRESOLVED DEPENDENCIES         ::
[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
[warn]  :: org.scalaz.stream#scalaz-stream_2.11;0.5a: not found
[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
[warn]
[warn]  Note: Unresolved dependencies path:
[warn]      org.scalaz.stream:scalaz-stream_2.11:0.5a
[warn]        +- org.specs2:specs2_2.11:2.4.4 (/Users/k-sera/tmp/zzz/build.sbt#L1-2)
[warn]        +- default:zzz_2.11:0.1-SNAPSHOT
sbt.ResolveException: unresolved dependency: org.scalaz.stream#scalaz-stream_2.11;0.5a: not found

ということで specs2 は2.4.2 -> 2.4.3 のマイナーアップデートから突然 Maven Central に存在しない scalaz-stream に依存するようになりました・・というか、これまでの specs2 と同等のものは specs2-core となり、specs2 という artifact はより多くのモジュールを含むものになりました。

specs2 のバージョンを上げて上記のようなエラーになったら "org.specs2" %% "specs2""org.specs2" %% "specs2-core" に変えましょう。この記事を読んでいる人は、それで問題ないはずです。

2015/03/12 追記

specs2 3.0 からは specs2-core であっても scalaz-stream に依存するようになったようです(specs2-common が依存しているので)。ということで全ての specs2 ユーザの方は 3.0 に上げるタイミングから Scalaz の bintry repository を resolvers に追加する必要があります。

resolvers += "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases"

というか Maven Central にあげてほしいですよね。+1 しましょう、日本からも。