seratch's weblog in Japanese

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

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 しましょう、日本からも。

福岡に行ってきた

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 さんのプレゼンにあった「何をやるかももちろん大事だけど、誰とやるのか、どんなチームにしたいのかがもっと大事」というのはとても共感するところがありました。

また「新しい何か」としてこの日にヌーラボさんが提供する各サービスの稼働状況を確認できるサービスがお披露目になりました。

https://status.nulab-inc.com/

こちらの開発合宿でつくられたものが正式にサービスとして公開とのことでした。golang で実装されているそうです。だから、この名前か。 http://nulab-inc.com/ja/blog/nulab/developers-camp-in-fukuoka/

アフターパーティでは、社員の方々の家族も来られていて、小さい子供のためのキッズスペースも考慮されているのがよかったですね。また、皆さん音楽好きのようで、楽器を演奏したり、爆音で音楽を楽しんでいたのが印象的でした。


さて、ここから先は技術と全く関係ないですが。。本当は別のサービスを使おうかと思ったのですが、Tweet の埋め込みなどはてなブログの方がやりやすかったので、、結局ここに書くことにしました。

まず、お昼くらいに博多駅に着いたのですが、昼食は @kaorunix さんのオススメで天神駅前の大砲ラーメンへ。とても美味しかったです。本場のラーメンは美味いし、東京に比べて安いですね。

そして、夜はアフターパーティに少しおじゃました後で、天神駅前の角屋で一人飲みしました。角屋がどんな店かはこちらを見てもらえるとわかると思います。

http://www.ensen24.jp/fukuoka/2010/08/post-134.php

ビールもつまみも全部食券を買って頼むスタイルなんですが、店員さんを呼んだり、料理を持ってきてもらうのを待たなくていいし、とてもよかったです。立ち飲み屋さんだと店員さんがなかなかつかまらなかったり、料理が出るのに時間かかったりとかたまにありますが、このスタイルにすればそういう問題は起きなさそうです。東京でもこういう店が増えてほしいですね。

備え付けのテレビで日本代表サッカー中継が流れていて、一人で来ている人は何となく観てたりしましたが、私はちょうど東京でやっていた Scala 初心者向け勉強会 のタイムラインが面白かったので iPhoneTwitter を見ていました。この店ではそういう客も別に浮かないというか、客はそれぞれ自分の好きなようにしているし、さくっと飲んで帰る人が多くて隣の人もどんどん入れ替わるので居心地よかったです。

もう一カ所くらいどこか、福岡といえばもつ鍋かラーメンかなと思いましたが「一人だし、もつ鍋ということもなかろう」ということで昼に続きもう一軒ラーメンに行くことにしました*1

食べログのランキングを参考に、そこそこ駅から離れているものの泊まっているホテルと同じ方面だったので、元祖長浜屋*2。ここは一杯 500 円。安い。もうおなかいっぱいだったので替え玉できなかったけど、普通に空腹で来ていたら替え玉していたと思います。美味かった。

この長浜屋へ徒歩で往復して一旦ホテルに戻ったところ、アフターパーティと角屋でそこそこ飲んでいたこともあり、そのまま寝てしまいました*3

翌日(今日ですが)は 6 時に目が覚めたので、ホテルの朝食券で少し軽食を食べた後、もう一カ所くらいどこかに行こうと調べていたら、一蘭本社総本店が歩いて 15 分くらいの場所にあり、なんと 24 時間営業で早朝でも開いていたので博多駅に行く前に立ち寄りました。

あのカウンターはちょっと苦手ではあるんですが、味はとても美味しかったです。

ということで、博多・天神はとても楽しかったです。今度来るときは屋台など行ってみたいところです。

最後にヌーラボさん 10 周年おめでとうございました!

*1:屋台でもつ煮込みとかもありえたのかもしれないけど、どこ行けばいいかよくわからなかったので、、

*2:そういえば、一文字変えただけの名前の店がすぐそばにあったりして笑ってしまいました

*3:まだ 22 時前くらいでしたが

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 ?

こんな記事もありましたが

Scalate 1.7を試してみる - Starlight

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: をお願いしたところ

無事マージされました。ありがとうございます!

ということで Scalate の公式サイトはこちらになりました。

Scalate

Skinny Framework からのリンクも更新しておきました。

https://github.com/skinny-framework/skinny-framework.github.io/commit/2c0c7fcfc0c739c761b72433a12db0b5d937cce4

最近は 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 のスターがかなり増えていますね。また、手前味噌ですが ScalikeJDBCSkinny 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でメンテされる状況をつくることができればいいと思っています。ということで、既に関わってくださっている方々にはとても感謝しております。いつもありがとうございます。

*1:なお、もちろんスター数が多いからよいライブラリとは限らないのですが、必要条件ではあるとは思います

*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 よかったらぜひ使ってみてください。