[Japanese] Skinny Framework で始める Scala #jjug_ccc #ccc_r24

Post on 27-Jun-2015

10.561 views 7 download

description

English version is here: http://www.slideshare.net/seratch/jjug-ccc-2014springeng http://www.java-users.jp/?page_id=1048#R2-4

Transcript of [Japanese] Skinny Framework で始める Scala #jjug_ccc #ccc_r24

Skinny Framework で始める Scala

瀬良 和弘 @seratch(_ja) エムスリー株式会社 ソフトウェアエンジニア 日本 Skinny Framework ユーザ会 JJUG CCC 2014 Spring 2014/05/18

私について・本業は主に Web アプリ、バックエンド API 開発、社内ツール諸々の運用整備の整備などに従事 ・Scala は 2009 年に始めて触った(だけ) ・2011 年の正月休みから少しずつ使い始めた ・2011 - 2012 年、Akasaka.scala という勉強会を隔週で主催していた(現在は休止中) ・社内外の Scala プロジェクトでメイン開発者として開発している OSS を利用いただくようになってきた(ScalikeJDBC、Skinny Framework)

今日は、JJUG の皆さんに Scala を宣伝するために来ました。 Java から Scala へ!!(真顔)

!

Skinny Framework は Java な現場、SI 業界の案件でも 十分使えるはずと思っています。

!

!

今日は私の話を聴いて 実際に使えそうか見極めてください。

!

!

- Scala について -

Scala のおさらい・2003 年に生まれた比較的新しい言語 ・オブジェクト指向と関数型プログラミングの融合 ・スイス連邦工科大学教授 Martin Odersky ・2011 年に出資を受けて Typesafe 社を立ち上げ ・Typesafe 社は Scala の開発だけでなく sbt、Akka、Play2、Slick、Spray 等のツール・ライブラリのメンテナンス・商用サポートも提供 ・Rod Johnson 氏が Typesafe 社に参加し話題に

JJUG 的? Scala のメリット・オブジェクト指向 + 関数型プログラミング ・静的型付け、型推論により記述の簡潔さを両立 ・デフォルトが immutable な API ・並行処理、非同期プログラミングとの親和性 ・JVM レベルの処理性能が期待できる ・Java で書かれた枯れた実装も流用できる ・大規模なプロダクション利用実績が多い ・GitHub 時代の言語、活気のあるコミュニティ

JJUG 的? Scala のデメリット・Java と比べてコンパイル時間が長い ・言語のバイナリ互換性などに対応する運用コスト ・Java エンジニアにとって意外にジャンプが大きい ・表現力の裏返しで OOP と関数型プログラミングが混在するなどスタイルが統一されない懸念がある ・Java と比べて、標準規格よりも動く OSS 実装がリードする世界なので、あまり受け身なスタンスだと難しい ・既に Scala を使いこなしているエンジニアの母数はまだまだ少なく、人材採用面での不安がある

Scala の互換性の件・最新バージョンは 2.11.0 ・2.10.0 ~ 2.10.4 はバイナリ互換あり ・2.10.4 と 2.11.0 はバイナリ互換なし ・2.9 までは 2.9.x ごとに互換性がなかった(!) ・2.9(と 2.10 の一部)は Java8 で動作しない ・Maven でいう artifactId にバイナリバージョンが suffix としてつく(例:scalikejdbc_2.11) ・メンテされなくなったライブラリは新しいバイナリバージョンでビルドされなくなるので選定に注意が必要

- フレームワークの現状 -

Web フレームワーク事情・長らく Lift ほぼ一択の時代(事例:4sq) ・2012 年春 Play Framework 2.0 が登場 ・約 2 年経ち、現在は圧倒的に Play2 の人気が高い ・より軽量なフレームワークはいくつか現実的な選択肢がある(Scalatra、Unfiltered、Spray など) ・Play2、Spray は現在 Typesafe 社のプロダクト !

・2014 年春 Skinny Framework 1.0

DB ライブラリ事情・あくまで目安として GitHub スター数で比較 ・Slick (879)、Squeryl (358)、ScalikeJDBC (254) がベスト 3、Slick は旧 ScalaQuery ・他には scala-activerecord (164)、Activate (155)、SORM (124) など !

・ScalikeJDBC は機能不足によって実務上困ることはないが ORM よりもコード量は多くなりがち ・そこを埋めるのが Skinny ORM

テンプレートエンジン事情・Scalate は長年メンテされており、SSP、Scaml、Jade、Mustache と複数の文法が使える ・Play2 の Scala Template が最近独立(Twirl) ・Lift はタグを埋める独自のやり方 ・Circumflex という Web フレームワークは FreeMarker の Scala 対応を実装(Skinny はこれを fork して Scala 2.10 に対応させた) !

・Skinny では Scalate を採用(理由は後ほど)

- Skinny とは -

Skinny?

・響きもいいし、自然とこの名前を選びました、一応 3 つの理由があります !

・Application should be skinny ・Framework should be skinny ・”好きに”(su-ki-ni)

Skinny Framework・2014/03/28 に 1.0.0 リリース(現在 1.0.14) ・Scala on Rails がキャッチコピー ・機能豊富なフルスタック Web フレームワーク ・MVC、ORM、DB マイグレーション、メール送信 ・scaffold などの自動生成機能 ・Play2 と違い Servlet コンテナで動作 ・Java 1.6 以上、Servlet 3.0 以上 ・war、Jetty 埋め込み jar ファイル生成

フルスタック?・フルスタック Web フレームワークはもはや死語? ・定型な機能を提供するフレームワークが珍しくなくなり、あえてアピールポイントとすることが少なくなった ・ライブラリを組み合わせること前提の開発は柔軟だが、依存先が増えるリスク、組み合わせの検証コストがある ・チーム開発の場合、ある程度やり方が決められている方がありがたい場合も多い(もちろんチーム次第) ・よくある要件の実現に必要な時間は極力減らし、差別化できる要件の開発にこそ時間を使いたい

Scala on Rails?・それって Play2 のことではないの?(否) ・Play2 本体は HTTP 周り以外の機能をあまり持っていない、今後も周辺を拡充するようには見えない ・Rails、Play1 のように HTML を返す Web アプリ開発の利便性に最適化しているわけではない ・Play2 は Rails 風味な記述で NIO ベースのサーバアプリを開発するためのフレームワーク ・JSON API サーバ、Akka の特長を活かしたアプリケーションなどに適している

Skinny の構成・極力自作を避け、実績があり、かつ Skinny の思想に合うライブラリを土台とし、その上に作り込んでいる ・ルーティング、Servlet ラッパーのために Scalatra ・デフォルトテンプレートエンジンとして Scalate ・ORM の土台として ScalikeJDBC ・DB マイグレーションは Flyway ・JSON の処理に json4s ・入力バリデーション、メール送信はオリジナル ・後ほど、個別の機能を順次紹介します

- Skinny をはじめよう -

起動まで(1)・必要なのは JDK(1.6 以上)のみ ・skinny-framework.org にある以下のダウンロードボタンから約 80MB の zip ファイルをダウンロードしてください !

!

!

!

!

起動まで(2)・zip ファイルを解凍して skinny run で起動 ・ブラウザから localhost:8080 にアクセス ・Windows OS でも同じ手順です !

!

!

!

!

!

起動まで(3)・Skinny が推奨する sbt プロジェクトのひな形 ・依存する jar がダウンロード済なので、初回起動時にダウンロード待ちをする必要がありません ・基本的な用途はデフォルトでカバーしているので最初から sbt の設定方法を学ぶ必要がありません ・skinny スクリプトだけで起動するので Jenkins で動かす場合もセットアップは一切不要 ・2014 年時点で Scala は Eclipse ではなく IntelliJ IDEA 推奨(Community Edition で可)

- Skinny MVC -

・#set で request scope に設定した値は controller/view から参照可能となります ・メソッドの戻り値が response body となります !

!

!

!

!

!

最初の Controller(1)

// src/main/scala/controller/RootController.scala!!package controller!import skinny._!class RootController extends ApplicationController {! def index = {! set(“name”, “JJUG”)! render(“/root/index”)! }!}

・「Hello ${name}!」に「JJUG」が設定されました !

!

!

!

!

!

!

!

最初の Controller(2)

! -# src/main/webapp/WEB-INF/views/root/index.html.ssp!<%@val name: String %>!<h1>Hello, ${name}!</h1>

・#set には様々な型の値を渡すことができます ・ここでは Scala のコレクションオブジェクトです !

!

!

!

!

!

!

最初の Controller(3)

// src/main/scala/controller/RootController.scala!!package controller!import skinny._!class RootController extends ApplicationController {! def index = {! set(“numbers”, Seq(1,2,3,4,5))! render(“/root/index”)! }!}!

・${numbers}」を使ったループと if 分岐の例です。 !

!

!

!

!

!

!

!

!

最初の Controller(4)

! -# src/main/webapp/WEB-INF/views/root/index.html.ssp!!<%@val numbers: Seq[Int] %>!#for (n <- numbers)! #if (n % 2 == 0)! ${n}! #end!#end!

・Scalatra が Servlet API の薄いラッパーとして簡潔に記述できる DSL を提供しています ・例:status = 201, redirect(url), halt(400) ・どうしても必要になれば Servlet API を直接使うことも可能です(まずないとは思いますが) ・Servlet をある程度知っている人なら controller を書く上でつまづくポイントは特にないはずです !

最初の Controller(5)

・Skinny の Routing は Scalatra の DSL を使います(Routes という trait で少しだけ拡張) ・controller の実体は Filter/Servlet なので ServletContext にひもづけるだけです !

!

!

!

!

最初の Routing(1)

! // src/main/scala/controller/Controllers.scala!! object Controllers {!! object root extends RootController with Routes {!! val indexUrl = get(“/“)(index).as(‘index)! }!! override def mount(ctx: ServletContext): Unit = {!! root.mount(ctx)!! }!! }!

・以下の場合 s.url(Controllers.root.indexUrl) で実際の URL を取得できるので view template に URL を直接書かずにすみます !

!

!

!

!

!

最初の Routing(2)

! // src/main/scala/controller/Controllers.scala!! object Controllers {!! object root extends RootController with Routes {!! val indexUrl = get(“/“)(index).as(‘index) ! }!! // 省略!! }!

・入力がある場合はバリデーション DSL を使います ・エラーの場合、エラーメッセージが request scope に設定されるので入力フォームで表示するだけで OK !

!

!

!

!

!

!

SkinnyValidator

! class MembersController extends ApplicationController {!! protectFromForgery()!! def createForm = validation(params,!! paramKey(“name”) is required & maxLength(64),!! paramKey(“email”) is email!! )!! def create = {!! if (createForm.validate()) {!! doCreate(params.permit(!! “name” -> ParamType.String, “email” -> ParamType.String))! } else {!! render(“/members/input”)!! }!! }!! }

・Scalate SSP(Scala Server Pages)の例は先ほどの Controller のところで少し触れました ・Scalate は SSP 以外に Scaml(≒Haml)、Jade、Mustache もサポートしています ・SSP 以外を使う場合、拡張子を変えるだけで OK ・FreeMarker、Thymeleaf のサポートも拡張ライブラリとして提供していますが Scala の API に十分対応できないケースがあります

最初の View(1)

・Scalate も Play2 の Twirl もテンプレートファイルから Scala のソースコードを出力してコンパイルします ・メリットはコンパイラによるチェックがあること ・デメリットはコンパイル待ちが発生すること ・Skinny での Scalate は、ローカルデバッグでは precompile せずにこのデメリットを緩和し war 生成時には precomile して、コンパイラチェックの恩恵を受けるようにしています

最初の View(2)

・Skinny ORM を使って Rails ActiveRecord 的な DB アクセスを行うモジュールの例を示します ・case class がエンティティで object が DAO です !

!

!

!

!

!

最初の Model(1)

! sql”create table member (id serial, name varchar(64))”!! ! .execute.apply()! !!! // src/main/scala/model/Member.scala!! case class Member(id: Long, name: Option[String])!!! object Member extends SkinnyCRUDMapper[Member] {!! lazy val defaultAlias = createAlias(“m”)!! def extract(rs: WrappedResultSet, m: ResultName[Member]) =!! new Member(rs.get(m.id), rs.get(m.name))!! }

・Flyway を使った DB マイグレーションを使えばローカル開発用 DB を手軽に準備できます !

!

!

!

!

!

!

最初の Model(2)

! ./skinny g migration createMember!! // src/main/resources/db/migration/V20140514141738__createMember.sql!! // V20140514141738__createMember.sql に DDL を書く!!! // “development” ENV の DB!! ./skinny db:migrate!!! // “test” ENV の DB!! ./skinny db:migrate test

・これだけで各種 API をすぐに使えます !

!

!

!

!

!

!

!

最初の Model(3)

! // Insert!! val id: Long = Member.createWithAttributes('name -> “JJUG")!! // insert into member (name) values ('JJUG')!!! // Finder API!! val ms: Seq[Member] = Member.findAll()!! // select m.id as i_on_m, m.name as n_on_m from member m order by m.id;!!! // Querying API!! val ms: Seq[Member] = Member.where(‘name -> “JJUG").apply()!! // from member m where m.name = ‘JJUG'!!! // ScalikeJDBC QueryDSL!! val m = Member.defaultAlias!! val mopt: Option[Member] = Member.findBy(sqls.eq(m.name, "JJUG"))!! // from member m where m.name = ‘JJUG'

・さらに country テーブルを追加して model を二つに増やして・・ !

!

!

!

!

!

!

最初の Model(4)

! case class Member(id: Long, name: Option[String])!! object Member extends SkinnyCRUDMapper[Member] {!! lazy val defaultAlias = createAlias(“m”)!! def extract(rs: WrappedResultSet, m: ResultName[Member] =!! new Member(rs.get(m.id), rs.get(m.name))!! }!!! // create table country(id serial, name varchar(128) not null);!! object class Country(id: Long, name: String)!! object Country extends SkinnyCRUDMapper[Country] {!! lazy val defaultAlias = createAlias(“m”)!! def extract(rs: WrappedResultSet, c: ResultName[Country] =!! new Country(rs.get(c.id), rs.get(c.name))!! }

・関連を持たせます !

!

!

!

!

!

!

!

最初の Model(5)

! case class Member(!! id: Long, !! name: Option[String]!! countryId: Option[Long],!! country: Option[Country] = None)!!! object Member extends SkinnyCRUDMapper[Member] {!! val defaultAlias = createAlias(“m”)!! def extract(rs: WrappedResultSet, m: ResultName[Member] =!! new Member(rs.get(m.id), rs.get(m.name), rs.get(m.countryId))!!! val country = belongsTo[Country](Country, !! (member, country) => member.copy(country = country)!! )!! }

・#joins で設定した association を解決した query を実行します !

!

!

!

!

!

!

!

最初の Model(6)

! // Insert!! val countryId: Long = Country.createWithAttributes('name -> “Japan”)!! val id = Member.createWithAttributes(!! ‘name -> “JJUG”, ‘countryId -> countryId)!!! // member だけ!! val m = Member.findById(id)!! // from member m where m.id = ?!!! // country も!! Member.joins(Member.country).findById(id)!! // from member m left join country c on m.country_id = c.id where m.id = ?!

・#byDefault を指定すると #joins を呼ばなくても常に join して取得します !

!

!

!

!

!

!

!

最初の Model(7)

! case class Member(!! id: Long, !! name: Option[String]!! countryId: Option[Long],!! country: Option[Country] = None)!!! object Member extends SkinnyCRUDMapper[Member] {!! val defaultAlias = createAlias(“m”)!! def extract(rs: WrappedResultSet, m: ResultName[Member] =!! new Member(rs.get(m.id), rs.get(m.name), rs.get(m.countryId))!!! val country = belongsTo[Country](Country, !! (member, country) => member.copy(country = country)!! ).byDefault!! }

・auto commit でよければ AutoSession ベースで ・トランザクションは ScalikeJDBC の機能を使うか 1 リクエスト 1 トランザクションでよければ、スレッドローカルに扱う TxPerRequestFilter を使います !

!

!

!

!

!

最初の Model(8)

! DB.localTx { implicit session =>!! // このブロックから例外で抜けるときは全て自動 rollback される!! Member.findById(id).map { member =>!! member.copy(name = newName).save() // SkinnyRecord!! MemberStatus.setAsActive(member)! }.getOrElse {!! val id = Member.createWithAttributes(‘name -> newName)!! MemberStatus.addNewMemberAsActive(id)! }!! }

・controller などからそのまま呼び出すだけ ・fat controller にならないように注意(controller に一通り実装した後で service object としてまとめる、model のインタフェースを先に決める) !

!

!

!

!

!

最初の Model(9)

! // src/main/scala/controller/MembersController.scala!!! import model.Member!! class MembersController extends ApplicationController {!! def showAll = {!! set(“members”, Member.findAll())!! render(“/members/showAll”)!! }!! }!!

・ここで示した例は最も規約に沿っているケースです ・ニッチなケースでは少し煩雑になる場合もあります ・最悪、泥臭く対応する場合 ScalikeJDBC を直接扱えば何でもできるので「詰む」ことはありません ・SkinnyModel の API を実装していれば Slick など別の DB ライブラリによる実装でも scaffold の CRUD テンプレートに対応できます ・複数 DB 接続などよくあるケースは基本的に対応済ですが、未対応のニーズがあれば気軽にリクエストをください

最初の Model(10)

FactoryGirl・Ruby で人気の thoughtbot/factory_girl にインスパイアされたテスト用の DB データ生成ツール ・YAML ではなく typesafe-config の HOCON で記述、Scala コードも書ける !

!

!

!

!

! member {!! name=“JJUG”!! luckyNumber="${scala.util.Random.nextInt(64)}"!! }

! val member: Member = FactoryGirl(Member).create()!! val member = FactoryGirl(Member).create(‘name -> “ScalaJP”)

- Skinny その他の機能 -

最初の scaffold(1)・CRUD 画面を生成するなら scaffold が便利です ・テストコードも生成されます !

!

!

!

!

!

!

! ./skinny g scaffold members member name:String birthday:Option[LocalDate] active:Boolean!!! *** 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/V20140514173530__Create_members_table.sql" created.

最初の scaffold(2)! ./skinny db:migrate !! ./skinnny run!!! ./skinny db:migrate test!! ./skinny test

・バリデーションを含む入力フォームの自動生成 !

!

!

!

!

!

!

!

最初の scaffold(3)

・flash も動作する状態になっています !

!

!

!

!

!

!

!

最初の scaffold(4)

・あらかじめ pagination も実装されています !

!

!

!

!

!

!

!

最初の scaffold(5)

既存 DB から scaffold・既存 DB から scaffold を生成します ・ID となる PK があることが前提です !

!

!

!

!

!

!

! ./skinny g reverse-scaffold members members member!!! *** Skinny Reverse Engineering Task ***!!! Table : members!! ID : id:Long!! Resources : members!! Resource : member!!! Columns:!! - name:String:varchar(512)!! - birthday:Option[LocalDate]!! - createdAt:DateTime!! - updatedAt:DateTime!!! *** Skinny Generator Task ***!!! "src/main/scala/controller/ApplicationController.scala" skipped.!! "src/main/scala/controller/MembersController.scala" created.!! "src/main/scala/controller/Controllers.scala" modified.

・JavaMail をメソッドチェーンで簡潔に書けます ・Skinny 以外でも利用できます !

!

!

!

!

!

!

!

SkinnyMailer

! val config = SkinnyMailerConfig.default.copy(!! // JavaMail, SMTP configuration !! )!! val mailer = SkinnyMailer(config)!! mailer!! .to(“seratch@example.com”)!! .cc(“users-ml@example.com”)!! .subject(“JJUG CCC で登壇します!”)!! .body {“””瀬良です。!! |今度の日曜日に JJUG CCC 2014 Spring で Skinny について!! |お話しさせていただくことになりました。!! |”””.stripMargin}!! .deliver()

Assets(1)・ローカル起動時に CoffeeScript、React JSX、Scala.js、Sass、LESS を JS/CSS に変換 ・Sass 以外は JVM だけでも動作するので Windows マシンを使う開発プロジェクトにも導入しやすいです ・Source Maps にも対応(要 native compiler) ・src/main/webapp/WEB-INF/assets/coffee の下の CoffeeScript は src/main/webapp/assets/js にある JS としてアクセスできます ・本番では出力された JS/CSS ファイルを使います

Assets(2)・Scala.js、プロダクション利用可能な段階ではないですが、興味深いので触ってみてください !

!

!

!

!

!

!

! ./skinny run!! ! ! // Terminal A!! ./skinny scalajs:watch! // Terminal B!! vim src/main/webapp/WEB-INF/assets/scala/Sample.scala! // Terminal C

デプロイ・skinny package で war ファイルを生成 ・package では全ての Scalate テンプレートをコンパイルするのでいつもアクセスしていないページのリンク切れやエラーを検知できる場合もあります ・skinny package:standalone は java -jar で Web サーバとして起動可能な jar を生成します ・Heroku などの PaaS へのデプロイも可能です !

!! ./skinny package!! ./skinny package:standalone

- まとめと今後 -

Skinny の何が嬉しいか・Play1/Rails 的なものが欲しいなら最適です ・全体的に Java での一般的なスタイルよりもほぼ本質的なコードのみで簡潔に同じ機能を実装できます ・Skinny ORM + ScalikeJDBC は Java では実装できない利便性・柔軟性を提供しています ・Scala での開発のボトルネックはコンパイル時間、周辺ツールや流儀の学習コストと考え、それらを解決・軽減して現実解を提案したいと思っています(そして、ある程度は示せたと思っています)

Reactive だけじゃない・Scala 界隈では Akka のアクターモデルや非同期処理が注目されることが多く、またそれが本流です ・一方で Ruby っぽいけど型検査のあるもの、Java との互換性があってより簡潔なものへの需要も相当あると思っています(少なくとも私はそれも欲しい) ・Skinny(Scalatra)と Akka を組み合わせたり 非同期処理を実装することも問題なくできますが、フレームワークが提供する機能はいわゆる Better Java 的な利便性を重視しています

ロードマップ・Skinny 1.1 - Scala 2.10/2.11 クロスビルドと各種ライブラリメジャーバージョンアップ対応 ・極力、基本的な機能の互換性を崩す予定はなく、開発者が楽できるように細かい改善を続けていく予定です ・安定して使っていただけるようにプロジェクトをハンドルしていきたいと思っています ・Scalatra 側で NIO なチャレンジも続けているようなので、こちらが実用段階になってきたら Skinny でも何かできるかもしれません

Skinny コミュニティ・日本 Skinny Framework ユーザ会 http://skinnyjp.doorkeeper.jp/ ・Skinny Framework Meetup Tokyo というイベントを先日初めて行い 25 名の方々にご参加いただきました、定期的に開催したいと思っています ・Tokyo 以外でも Meetup やりたいです ・Google Group に ML があります(英語) ・何かあればお気軽に Twitter で @seratch_ja や @skinny-framework(英語)まで

Skinny Framework なかなかよさそうだと思った方?

!

メディアでの日本語のドキュメントなど 機会があればぜひやりたいです。 公式ドキュメントも協力者がいれば 日本語版をやれるかもしれません。

!

!

(時間があれば)質疑応答。 何でも聞いてください。

!

!