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
で確認してください
Skinny Meetup Tokyo 2 を開催しました #skinnyjp
Skinny Framework 2.0 をリリースして一ヶ月弱、@yusuke さんのご厚意により、東池袋のサムライズムのオフィスをお借りして Skinny Meetup Tokyo の第 2 回を開催させていただきました。
当日使ったスライドはこちら。
www.slideshare.net
英語版はこちら。
www.slideshare.net
関連する tweet を Togetter にまとめました。
当日の内容
Meetup の流れは、まず、私の方から Skinny が 2.0 で変わったこと(とちょっとだけさだまさしコンパイラのデモ)をお話しました。次に、社内の情報共有ツールの実装に Skinny Framework を使ってくださっているという @roundrop さんから、その社内情報共有ツールについての紹介、初めての Scala 開発で Skinny Framework を使ってどうだったかという所感についてお話いただきました。具体的には以下のインタビューで触れられている Siita というツールです。
数年 Scala をやっている身からするとすっかり忘れかけていたような、初めて Scala をやったときに困った点などもバランスよくまとめていただいていたので、おそらくこれから Scala、Skinny をやってみようという方々に大変参考になったのではないかと思います。この発表のスライドは後日公開いただけるとのことでしたので、楽しみにしております。 (追記)公開していただいたので、以下に埋め込みました。
www.slideshare.net
サムライズム最高
当初は一人千円いただいてピザとドリンクを準備予定でしたが、サムライズム @yusuke さんのご厚意に甘えさせていただきまして、なんと無料で懇親会を行うことができました。再度、お礼申し上げる次第です。
tweet とこのブログで紹介させていただく程度のことしかできませんが、以下のようにサムライズムでは OSS コミュニティも応援されているそうです。また IntelliJ IDEA や JRebel の業務導入を検討されている企業様におかれましては、ぜひ日本の商慣行にも柔軟に対応しているサムライズム社でご購入いただければと思います!
今日は何から何までサムライズム .@yusuke さんのお世話になりっぱなしでした。ありがとうございました!IntelliJ IDEA と JRebel の購入はサムライズムさんで! #skinnyjp #さだまさしjp
— seratch_ja (@seratch_ja) 2015, 12月 22
サムライズムさんでは JVM を中心とした OSS コミュニティのイベント開催など応援されているそうですよ! #skinnyjp #さだまさしjp
— seratch_ja (@seratch_ja) 2015, 12月 22
そんな大きな箱ではありませんが、私が関われるようなコミュニティであれば弊社の場所お貸しできます!有償イベントはできません
— 山本裕介 Yusuke Yamamoto (@yusuke) 2015, 12月 22
Skinny Micro
今回、初めて Skinny Micro についてちゃんと説明させていただきましたが、Scalatra を fork してリファクタリング、機能追加したものでかなり手軽で使いやすいライブラリになったのではないかと思っています。
どれくらい手軽かというと、以下に示すこれだけのコードで 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 になっていただけると嬉しいです。
Skinny の今後
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 を使って開発されている方もどうぞ。
みんなで買えば送料がお得
販売しているグッズ一覧はこちらで見ることができます。
複数口で買うと送料がお得になります。お買い上げいただける際はぜひ開発チーム、部署、会社単位でまとめてご注文ください!
*1:トリブンは 0 円設定なので、私の懐には 1 円も入れておりませんが
Future についての私見
この記事を読んで、記事の本筋からはそれますが、前から思っていたことをつぶやいたのでまとめておきます。
前後関係がある Future 処理を一つの処理として抽出すればシンプルなケースではおかしなことにはならないと思うけど real world では横からはさみこみたくなる要件が継ぎ足されていくとつらくなる。
— seratch_ja (@seratch_ja) 2015, 3月 26
そういう要件は本当は同期処理でやるのがよいわけで、そこをあえて Future とかコールバック関数縛りでやらなければならない理由が何かを考える。リソース効率とか膨大な同時接続への耐久性を可読性を犠牲にしてでも得たいのかという。
— seratch_ja (@seratch_ja) 2015, 3月 26
適当に作るとデバッグとか運用のトラブルシュートとか大変になるし、開発の難易度はそれなりに上がると思う。本当にトラフィック数や同時接続がすさまじい環境なら別だけど、そうでもない案件の場合だとそこを頑張るのって見合ってるのかなと。
— seratch_ja (@seratch_ja) 2015, 3月 26
"Future の先の隠蔽された世界" 感
— seratch_ja (@seratch_ja) 2015, 3月 26
向こう側では何ミリ秒タイムアウトなのか使う側は知らなくてもいい?そんなことないよね、みたいな感覚。同じアプリの中なんだけど別の何かと連携しているかのような。
— seratch_ja (@seratch_ja) 2015, 3月 26
どういうアーキテクチャだろうが丁寧につくりましょうというのは変わらないだけかもしれないけど。
— seratch_ja (@seratch_ja) 2015, 3月 26
「それ、今まで通り同期処理で 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 の方がよかったりするかもしれません。
Java を Scala の中で使うこと
今更という気もしますが、ちょうどよい機会なので Scala の中で Java を使うことについて意見を書いておこうかなと思います。
いざとなれば Java の既存コードをそのまま使えるのは Scala のよいところなのですが、あくまで「いざとなれば」というオプションであり、かなりニッチな分野であればまだしも、RDB アクセスのようなありふれたユースケースではファーストチョイスが Java の利用とは考えない方が幸せになれると思います。
自分自身を振り返っても Scala を始めた頃は「手慣れた Java のライブラリを使ってやった方が楽なのでは?」と思ったりしたものですが、結局 Java と Scala の架け橋となるところをケアしながら書かないといけないので簡潔にはなりにくく、落とし穴もあったりします。
例えば null は Scala の世界では「なかったこと」になっていて Option にしてあげないといけないですが、そういうことをやってくれる Java のライブラリはまずないので Java の API からの戻り値を自分で Option で包んであげないといけなかったりします。また Scala のコレクション型のオブジェクトは Java の世界からはうまく扱えないので Java の世界に渡す前に必ず scala.collection.JavaConverters なりで変換してあげる必要がありますし、逆に java.util.List などが Java から返ってくれば毎回 #asScala を呼んで変換する必要があります。
どういうときに Java を混ぜるか
ということで Scala であえて Java API を使う場合というのは、既に Java で SDK が提供されている場合だったり、よほど 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.3 からいきなり central に存在しない scalaz-stream に依存してるのか。うーん。
— Kazuhiro Sera (@seratch_ja) September 22, 2014
ということで specs2 は2.4.2 -> 2.4.3 のマイナーアップデートから突然 Maven Central に存在しない scalaz-stream に依存するようになりました・・というか、これまでの specs2 と同等のものは specs2-core となり、specs2 という artifact はより多くのモジュールを含むものになりました。
@seratch_ja 今、依存関係こんな感じなので http://t.co/ZkHDLOjEjH scalaz-streamに限らず全部入りのjar使うと余計な依存ついてくるので、必要最小限だけ使ったほうがいいですね
— Kenji Yoshida (@xuwei_k) October 2, 2014
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 しましょう、日本からも。
.@seratch_ja https://t.co/Z1WJZgIwlM あとこれに+1しましょう(と薦めるとか)
— Kenji Yoshida (@xuwei_k) March 12, 2015