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
福岡に行ってきた
9 月末に予定していた広島への帰省のチケットをそろそろとろうかと思っていたところ 10/10 にヌーラボさん 10 周年の NUCON というイベントがあると知ったので、良い機会だということで少し後ろにずらして、一日だけ福岡に行ってみました。
http://nucon-10th.nulab-inc.com/
ヌーラボさんのサービスといえば Backlog、Cacoo、Typetalk の三つですね。
中でも Typetalk はバックエンドでは Scala、Play Framework などを使っているということで
https://twitter.com/search?f=realtime&q=typetalk%20scala&src=typd
プロダクトオーナー兼メイン開発者の @ussy00 さんとも仲良くさせていただいているし、個人的にも応援しています。ScalikeJDBC も使ってくださっているそうで、ありがとうございます。
GitHub で Play Framework を使った OAuth2 Provider 実装を公開されているので、Scala に興味がある方は見てみるとよいのではないでしょうか。 https://github.com/nulab/scala-oauth2-provider
NUCON 本編では @tksmd さんのプレゼンにあった「何をやるかももちろん大事だけど、誰とやるのか、どんなチームにしたいのかがもっと大事」というのはとても共感するところがありました。
また「新しい何か」としてこの日にヌーラボさんが提供する各サービスの稼働状況を確認できるサービスがお披露目になりました。
こちらの開発合宿でつくられたものが正式にサービスとして公開とのことでした。golang で実装されているそうです。だから、この名前か。 http://nulab-inc.com/ja/blog/nulab/developers-camp-in-fukuoka/
アフターパーティでは、社員の方々の家族も来られていて、小さい子供のためのキッズスペースも考慮されているのがよかったですね。また、皆さん音楽好きのようで、楽器を演奏したり、爆音で音楽を楽しんでいたのが印象的でした。
さて、ここから先は技術と全く関係ないですが。。本当は別のサービスを使おうかと思ったのですが、Tweet の埋め込みなどはてなブログの方がやりやすかったので、、結局ここに書くことにしました。
まず、お昼くらいに博多駅に着いたのですが、昼食は @kaorunix さんのオススメで天神駅前の大砲ラーメンへ。とても美味しかったです。本場のラーメンは美味いし、東京に比べて安いですね。
@seratch_ja 今福岡ですよね?近いところでは https://t.co/XZpzG81o68 大砲一番好きなラーメンです。本当は本店が一番好きですが。わざわざ東京から2回食べに行きました。
— Kaoru Sato (@kaorunix) October 10, 2014
本場のラーメンだ。600 円、安い。 pic.twitter.com/I82uONoXc5
— Kazuhiro Sera (@seratch_ja) October 10, 2014
そして、夜はアフターパーティに少しおじゃました後で、天神駅前の角屋で一人飲みしました。角屋がどんな店かはこちらを見てもらえるとわかると思います。
http://www.ensen24.jp/fukuoka/2010/08/post-134.php
ビールもつまみも全部食券を買って頼むスタイルなんですが、店員さんを呼んだり、料理を持ってきてもらうのを待たなくていいし、とてもよかったです。立ち飲み屋さんだと店員さんがなかなかつかまらなかったり、料理が出るのに時間かかったりとかたまにありますが、このスタイルにすればそういう問題は起きなさそうです。東京でもこういう店が増えてほしいですね。
揚げたてのきびなご唐揚げうまー pic.twitter.com/68rBytGcRJ
— Kazuhiro Sera (@seratch_ja) October 10, 2014
備え付けのテレビで日本代表サッカー中継が流れていて、一人で来ている人は何となく観てたりしましたが、私はちょうど東京でやっていた Scala 初心者向け勉強会 のタイムラインが面白かったので iPhone で Twitter を見ていました。この店ではそういう客も別に浮かないというか、客はそれぞれ自分の好きなようにしているし、さくっと飲んで帰る人が多くて隣の人もどんどん入れ替わるので居心地よかったです。
もう一カ所くらいどこか、福岡といえばもつ鍋かラーメンかなと思いましたが「一人だし、もつ鍋ということもなかろう」ということで昼に続きもう一軒ラーメンに行くことにしました*1。
食べログのランキングを参考に、そこそこ駅から離れているものの泊まっているホテルと同じ方面だったので、元祖長浜屋へ*2。ここは一杯 500 円。安い。もうおなかいっぱいだったので替え玉できなかったけど、普通に空腹で来ていたら替え玉していたと思います。美味かった。
美味かった。福岡に思い残すことはないな。 (@ 元祖 長浜屋 in 福岡市, 福岡県) https://t.co/QDWW5oPCRw pic.twitter.com/4Xzby5vgvS
— Kazuhiro Sera (@seratch_ja) October 10, 2014
この長浜屋へ徒歩で往復して一旦ホテルに戻ったところ、アフターパーティと角屋でそこそこ飲んでいたこともあり、そのまま寝てしまいました*3。
翌日(今日ですが)は 6 時に目が覚めたので、ホテルの朝食券で少し軽食を食べた後、もう一カ所くらいどこかに行こうと調べていたら、一蘭本社総本店が歩いて 15 分くらいの場所にあり、なんと 24 時間営業で早朝でも開いていたので博多駅に行く前に立ち寄りました。
あのカウンターはちょっと苦手ではあるんですが、味はとても美味しかったです。
秘伝のたれを基本の量にしたら思ったより辛かったが、美味かった pic.twitter.com/l81zgnoGqE
— Kazuhiro Sera (@seratch_ja) October 11, 2014
ということで、博多・天神はとても楽しかったです。今度来るときは屋台など行ってみたいところです。
最後にヌーラボさん 10 周年おめでとうございました!
Scalate の公式サイトが GH pages に移行
Scala の老舗テンプレートエンジンで私自身 Skinny Framework のコアなコンポーネントとして利用している Scalate のウェブサイト http://scalate.fusesource.org これが閉じることになったらしく GitHub pages に移行しました。今日になってアクセスしてみると http://scalate.fusesource.org はもはやアクセスできなくなっていてました。一定期間リダイレクトするとか・・そういうのないですか。そうですか。
https://github.com/scalate/scalate/pull/60
The box hosting http://scalate.fusesource.org is going to be shutdown very soon (ideally today :-) ). Could someone merge this PR and also grab the gh-pages branch at https://github.com/janstey/scalate/tree/gh-pages so the website will be hosted at http://scalate.github.io/scalate ?
こんな記事もありましたが
Scala 2.11 対応の Scalate 1.7.0 が Scalatra 2.3.0 のタイミングに合わせて @rossabaker さんのリードによりリリースされたものの、公式サイトは放置されていて最新バージョンは 1.6.1 のままでした。organization(Maven でいう groupId)も変わっているというのに...
今回の公式サイト移行をみて、これはいい機会とばかりにウェブサイトを 1.7.0 対応にする Pull Request をして
Update GH page for Scalate 1.7.0 by seratch · Pull Request #61 · scalate/scalate · GitHub
日本のみなさんに :+1: をお願いしたところ
新しいページはここになるそうなんですが、1.7.0 の情報がないので PR しています。:+1: で応援してください。 http://t.co/N8x0AJWBZb https://t.co/VuZZSNndDy #ScalaJP
— Kazuhiro Sera (@seratch_ja) 2014, 10月 1
無事マージされました。ありがとうございます!
merge されました。ありがとうございました! http://t.co/N8x0AJWBZb
— Kazuhiro Sera (@seratch_ja) 2014, 10月 1
ということで Scalate の公式サイトはこちらになりました。
Skinny Framework からのリンクも更新しておきました。
最近は Scala といえば Play で Play といえば Twirl ですが、Scalate もなかなか便利ですよ。ぜひ皆さん使ってみてください。
ScalaMatsuri ふりかえり(今更)
何となく ScalaMatsuri のふりかえりでもしてみようかという気分になりました。自分の中での整理という感じなので、あまり面白みはないかもしれません。
Scala と私
というところで見直してみたけど、私の Scala への関わり方は以下のスライドに全てあらわれているので、その点はもう補足の必要がないですね。私の活動に興味を持っていただいていた方にとっては、やってきたことそのままであることがおわかりいただけるかと思いますが、この機会に明文化できて自分としては一区切りついたというか、すっきりした感はあります。
スライド
もう少し踏み込んだ話
もう少し踏み込んで言うと、もしも Scala 使うようになって前より仕事が楽になったり、楽しくなっていないとしたらそれは明らかに間違っているというか、少なくとも今いるメンバー、やろうとしてることとのミスマッチがあるわけなので「頑張って FP の勉強しなきゃ」「コンパイル早くならないかな」の前に考えるべきことがあるのでは?と思ったりしています。
もっと直球で言えば、何となく他の選択肢を検討することを嫌ってはいけないということです。それを考えてもなお Scala ということだったのかという。そして既に Scala でやっているのならば、その中でどんどんベストプラクティスを見つけていって、それをコミュニティで共有したいですよねという。まだ若いコミュニティですし。
「Scala を使って本番ロンチしました!」という事例が多く出てくることはすばらしいのですが「ぶっちゃけ困ってること結構あるでしょ」「これから持続していく必要があるわけだけど、大丈夫そうですか」というのが私の最近の関心事です。もちろん「うちは万全!」という人たちもいると思うので、そういう人にぜひコミュニティでもっと目立ってもらえるとよいかなと。怖いとか怖くないとかはそろそろいいのでもっと現実の話をしたいですね。
そして、私も微力ながら可能な限りそれを解決することに貢献していきたいと思っていますが、OSS の開発という面では特定の納期に合わせるというものでもないですし、個別の案件としては色々あるというのが実際のところかなと思います。
英語プレゼンの件
英語発表の件でスタッフの方々にお手間をおかけしたことは申し訳なかったですが、長期的には「そんなこともあったね」とふりかえることができるだろうと思っているので、後悔はしていません。ただ、私のトークに満足いただけなかった方に対しては率直に私の力不足を詫びたいと思いますし、発表内容だけでなく運営としても改善点はあっただろうとも思います。
一方で、これはあくまで私個人の意見として言わせていただきますが、コミュニティは誰かが与えるものではなくみんなで育てていきたいと思っていますし、私個人としてはそういう場所こそを大事にしたいということを感じた出来事ではありました。
これからの Scala コミュニティ
Scala が好きな人だけで集まるのも楽しいですが、そもそも今でもオブジェクト指向と関数型の交差点として混沌としている面白さがある界隈です。もし「正直 Scala 自体はそんなに好きじゃないんだけど、Scala に関連したアレが便利すぎるから Scala 界隈に絡んでいる」みたいな人たちが出てきて、もっと多様性のあるコミュニティになったりすると、より一層面白くなるのではないかと思います。Spark とかはその兆しかもしれません。
日本のコミュニティが世界に閉じていないというのも一つの目標ですが、ふとそういう未来も見てみたいと妄想しました。
Scala DB ライブラリ事情(2014/06 版)
最初に調べてから一年経ったので、また GitHub スター数の推移を見てみました*1。というか、こういうの自動化するサービスがあるといい気がしてきたけど。。
DB ライブラリ
順位 | 名前 | 2013/06 | 2013/07 | 2013/08 | 2013/09 | 2013/10 | 2014/06 | 総増減 |
---|---|---|---|---|---|---|---|---|
1 | Slick | 613 | 638 | 658 | 692 | 717 | 922 | +309 |
2 | postgresql-async | 159 | 187 | 213 | 227 | 240 | 421 | +262 |
3 | Squeryl | 322 | 329 | 334 | 338 | 340 | 365 | +43 |
4 | ScalikeJDBC | 112 | 135 | 141 | 150 | 180 | 274 | +162 |
5 | Querulous | 154 | 157 | 158 | 162 | 164 | 170 | +16 |
6 | scala-activerecord | 92 | 101 | 104 | 113 | 130 | 168 | +76 |
7 | Activate | 85 | 94 | 99 | 103 | 112 | 163 | +78 |
8 | sqltyped | 109 | 108 | 110 | 119 | 121 | 162 | +53 |
9 | SORM | 91 | 95 | 97 | 100 | 102 | 127 | +36 |
10 | ScalikeJDBC-Async | - | 14 | 20 | 27 | 28 | 59 | +59 |
11 | Prequel | 35 | 37 | 39 | 41 | 44 | 58 | +23 |
12 | Relate | - | - | - | - | - | 42 | +42 |
13 | DataExpress | 24 | 24 | 27 | 27 | 29 | 37 | +13 |
14 | Scala SQL DSL | 28 | 27 | 27 | 27 | 28 | 28 | 0 |
15 | jdub | - | - | - | - | - | 24 | +24 |
16 | MyBatis Scala | 1 | 3 | 4 | 4 | 7 | 14 | +13 |
17 | ScalaSQL | 12 | 13 | 13 | 13 | 13 | 13 | +1 |
18 | Scweery | 12 | 12 | 12 | 12 | 12 | 11 | -1 |
19 | mirage-scala | 4 | 4 | 4 | 5 | 5 | 7 | +3 |
20 | shirahae-sql | - | - | 2 | 2 | 2 | 2 | +2 |
フレームワークの一部
順位 | 名前 | 2013/06 | 2013/07 | 2013/08 | 2013/09 | 2013/10 | 2014/06 |
---|---|---|---|---|---|---|---|
1 | Anorm | - | - | - | 3697 | 3797 | 4906 |
2 | Lift Mapper | - | - | - | 717 | 722 | 796 |
3 | Spring JDBC Scala | 154 | 168 | 182 | 194 | 212 | 262 |
4 | Skinny ORM | - | - | - | - | 99 | 251 |
5 | Circumflex ORM | 136 | 136 | 137 | 140 | 143 | 155 |
感想など
Slick と postgresql-async のスターがかなり増えていますね。また、手前味噌ですが ScalikeJDBC、Skinny ORM も順調に成長できたといってよいのではないかと思います。
注目すべき点としては Relate というライブラリが出てきてなかなか面白い存在です。Anorm と比較して「パフォーマンス的に生 JDBC と同等」とうたっています。
https://www.lucidchart.com/techblog/2014/06/17/performant-database-access-relate/
提供している Lucid Software というのはデータ分析をビジネスとする会社のようです。元々 Anorm を使っていたものの、かなり大量のデータを扱う必要があって ResultSet からの取り出し部分でのオーバーヘッドが無視できなかったため、独自で開発したとのこと。ここでいうオーバーヘッドは ResultSet からの取り出し部分に限った話なので、通常はそれほど重視する必要はないかなと思いますが、速いのはよいことですね。
また Simple Finance Technology という会社が出している jdub というライブラリを見落としていたようで、こちらを加えました。ただ、この会社が Scala 2.9.2 を使い続けているのか、他のバージョンへのリリースがされておらず、新しい Scala のメジャーバージョンが出る度に issue が登録されている状況のようです。ていうか、なんかビルドが Maven だけど...。
この会社、テストライブラリも自作していたりして、なかなか興味深いです。
https://github.com/SimpleFinance/simplespec
simplespec も Maven でやっているようですが、こちらは 2.11 にもビルドしているようなので jdub は一部の Scala 2.9.2 なレガシープロジェクトだけで使っているということなのかもしれません。
一般的にライブラリの提供元が会社であるということは、半強制的にメンテナンスがチーム体制となるので、継続性という面はプラス要因といえます。それだけでなく、少なくとも何らかの本番環境で使われているということの証明でもあります。最近の OSS では会社をつくるケースが増えている印象もありますが、やはり仕事の一環としてメンテナンスされているというのはよい状況だと思います。
私がやっているようなボランティアベースの開発においても、そのライブラリを使っている会社の開発者を積極的に関係者に巻き込んでいくことで、会社をまたいだ業界内のギルトのようなチーム*2でメンテされる状況をつくることができればいいと思っています。ということで、既に関わってくださっている方々にはとても感謝しております。いつもありがとうございます。
JJUG CCC 2014 Spring で登壇しました #jjug_ccc
JJUG CCC 2014 Spring – R2-4 Skinny Framework で始める Scala
http://www.java-users.jp/?page_id=1048#R2-4
プレゼン資料
感想など
運営の皆様ありがとうございました。懇親会も非常に楽しかったです。
Skinny よかったらぜひ使ってみてください。