Hatena::ブログ(Diary)

tototoshiの日記 このページをアンテナに追加 RSSフィード Twitter

2013-04-14

sbt-install 作った

| 22:10 |

https://github.com/tototoshi/sbt-install

sbt は manual install するのが好みなんですが、

あっ、このマシン sbt 入ってない

とか

あっ、sbt が古い

ってときに例の java -jar sbt-launch.jar するスクリプト書くのがいい加減めんどくさくなったので、インストールスクリプト作っときました。

0.12.x 系は launcher は共通なのでそんな必須でもないんですが、そうは言っても sbt ってすぐアップデートされちゃうし。。。


$ curl https://raw.github.com/tototoshi/sbt-install/master/sbt-install > ~/bin/sbt-install && chmod 0755 !#:3 

インストールスクリプトインストールできます。

そんで、

$ sbt-install 0.12.3

で sbt 0.12.3 をインストールできます。

プロジェクトによってはプロジェクトのリポジトリに sbt-launch.jar に入れてしまうこともあると思うので

-d でインストールするディレクトリを選べるようにしました。

$ sbt-install -d . 0.12.3

地味に便利だと思います。

sbt version manager みたいなのにしようかとか思ったけどすぐにそれはやりすぎだと思いました。

2013-04-13

ヘッダーを見て cut するコマンド作った

| 00:45 |

データ処理でヘッダーがついてるtsv形式のデータファルを扱うことがあるんですが、フィールドが何番目か数えてから cut コマンドをするのがめんどいし、cut -f 1,3,12 みたいなのが暗号めいてていやなので、ヘッダーを指定して cut できるコマンドを作りました。地味っすね。


こんな感じ。

$ cat a.txt
USER_ID NAME    AGE
1       Sato    10
2       Suzuki  30
3       Abe     20

$ hcut -f USER_ID -f NAME a.txt
1       Sato
2       Suzuki
3       Abe

PyPI に挙げたので easy_install hcut で入ります。

2013-04-07

Playのバージョンを比較するやつ書いた、が

| 22:57 |

Play はプロジェクトのバージョンと使用している Play コマンドのバージョンが食い違っていると警告が出るんですが、バージョンの比較が同じかどうかしか見ていないのにエラーメッセージが Update しろとかいうやつなので

https://github.com/playframework/Play20/blob/2.1.1/framework/src/sbt-plugin/src/main/scala/play/Project.scala#L23

  Option(System.getProperty("play.version")).map {
    case badVersion if badVersion != play.core.PlayVersion.current => {
      println(
        Colors.red("""
|This project uses Play %s!
|Update the Play sbt-plugin version to %s (usually in project/plugins.sbt)
""".stripMargin.format(play.core.PlayVersion.current, badVersion))
      )
    }
    case _ =>
  }

f:id:tototoshi:20130407223937p:image

こういう風に、ん?なエラーメッセージになることがあります。


で、気持ち悪いので直そうと思ってバージョンを比較するのを書いたんですが、

よく考えたらエラーメッセージをちょっと直せばいいだけじゃん。このコード全然いらねー!バージョンパースするのにパーサコンビネータとか持ち出して自分ばっかじゃねーのってことに気づきました。プログラム書く前に必要かどうかちゃんと考えようって話ですね。


せっかく書いたので貼っておきますね。。。

そういえば、Ordering はモノイドだからうんぬんって話がありましたけど、Scalaz ではなく Scala の場合はまあ Ordering.by で Tuple の比較とかに落とすのがシンプルでいいのかなと思いました。


import scala.util.parsing.combinator._

class PlayVersionException(val message: String) extends Exception(message)


sealed abstract class VersionMilestone(val value: Int)
case object Alpha extends VersionMilestone(0)
case object Beta extends VersionMilestone(1)
case class RC(number: Int) extends VersionMilestone(2)
case object Regular extends VersionMilestone(3)

case class PlayVersion(major: Int, middle: Int, minor: Int, versionMilestone: VersionMilestone) {

  def >(x: PlayVersion)(implicit ord: Ordering[PlayVersion]): Boolean = {
    if (implicitly[Ordering[PlayVersion]].lteq(this, x)) false else true
  }

  def <(x: PlayVersion)(implicit ord: Ordering[PlayVersion]): Boolean = {
    ! this.>(x)(ord)
  }

}

object PlayVersionParser extends RegexParsers {
  def number = """[0-9]+""".r
  def major = number ^^ { _.toInt }
  def middle = number ^^ { _.toInt }
  def minor = number ^^ { _.toInt }
  def rc: Parser[VersionMilestone] = "-RC" ~> number ^^ { n => RC(n.toInt) }
  def beta: Parser[VersionMilestone] = "-beta" ^^ { _ => Beta }
  def alpha: Parser[VersionMilestone] = "-alpha" ^^ { _ => Alpha }
  def earlyVersion: Parser[VersionMilestone] = rc | beta | alpha

  def version: Parser[PlayVersion] = major ~ ("." ~> middle) ~ opt("." ~> minor) ~ opt(earlyVersion) ^^ {
    case major ~ middle ~ minor ~ earlyVersion => PlayVersion(major, middle, minor.getOrElse(0), earlyVersion.getOrElse(Regular))
  }
  def parse(in: String): PlayVersion = parseAll(version, in) match {
    case Success(result, _) => result
    case failure : NoSuccess => throw new PlayVersionException("Unexpected version naming convention.")
  }

}

implicit val versionMilestoneOrdering = new Ordering[VersionMilestone] {
  def compare(x: VersionMilestone, y: VersionMilestone): Int = {
    (x, y) match {
      case (x, y) if x == y => 0
      case (RC(i), RC(j)) => i - j
      case (x, y) => x.value - y.value
    }
  }
}

implicit val playVersionOrdering: Ordering[PlayVersion] = {
  Ordering.by[PlayVersion, (Int, Int, Int, VersionMilestone)] { v: PlayVersion =>
    (v.major, v.middle, v.minor, v.versionMilestone)
  }
}

object PlayVersion {
  def apply(versionString: String): PlayVersion = {
    PlayVersionParser.parse(versionString)
  }
}

assert(PlayVersion("2.1.1") > PlayVersion("2.0"))
assert(PlayVersion("2.1.1") > PlayVersion("2.1.0"))
assert(PlayVersion("2.1.1") > PlayVersion("2.1.1-RC1"))
assert(PlayVersion("2.1.1-RC1") == PlayVersion("2.1.1-RC1"))
assert(PlayVersion("2.1.1-RC2") > PlayVersion("2.1.1-RC1"))
assert(PlayVersion("2.1.1-RC2") > PlayVersion("2.1.1-beta"))
assert(PlayVersion("2.1.1-alpha") < PlayVersion("2.1.1-beta"))

xuweixuwei 2013/04/07 23:03 playコマンド使わずに、sbt直で起動してるから、気にしたことなかった・・・

2013-04-06

scalikejdbc-play-fixture-plugin を作りました

| 14:23 |

(また Play プラグイン作ったのかよと言われそうですが、、、)



おしごと的な Play アプリでは Fixture 機能を自前実装して、それでテストをしていました。

ただ、scalikejdbc に依存していたため、そこだけ切り出して Play プラグインにするとかはしてませんでした。でもよく考えたら scalikejdbc-play-plugin 自体に Fixture 機能つけちゃえばいいんじゃね?ってことで実装し、scalikejdbc 1.5.2 から入りました。

http://notes.implicit.ly/post/47241249711/scalikejdbc-1-5-2

scalikejdbc-play-plugin に組み込もうと思ったんですが、実装してから Play 2.0 にはない play.api.Configuration#getStringList を使っていることがわかりました。なのでこの機能は scalikejdbc-play-fixture-plugin として scalikejdbc-play-plugin とは別のものになり、 Play 2.1 からのサポートです。


それ evolutions でできるよ、と思う方がいるかもしれませんが、evolutions はデータベースマイグレーションの仕組みであり、データの投入ツールではないです。あと、この前のエントリに書いたようなプラグイン同士の依存関係の問題があり、scalikejdbc と evolutions の併用はあまりしたくないです。


インストール

build.sbt はこんな感じ

val appDependencies = Seq(
  "com.github.seratch" %% "scalikejdbc"                     % "[1.5,)",
  "com.github.seratch" %% "scalikejdbc-play-plugin"         % "[1.5,)",
  "com.github.seratch" %% "scalikejdbc-play-fixture-plugin" % "[1.5,)"
)

conf/play.plugins はこう。PlayFixturePlugin を使うには PlayPlugin がすでにロードされている必要があることに注意してください。

10000:scalikejdbc.PlayPlugin
11000:scalikejdbc.PlayFixturePlugin

使い方

Fixture はよく yaml とかのフォーマットで用意したりしますが、Play は evolutions がああですから、Plain SQL をそのまま使えばいいだろうってことにしました。

ファイルの中身も evolutions をパクって、Ups と Downs に分けました。Upsアプリの起動時に読み込まれ、Downs は終了時に実行されます。


#!Ups
INSERT INTO message (name, message) VALUES ('toshi', 'hello');
...

#!Downs
DELETE FROM message;

このファイルを conf/db/fixtures/${dbname}/message.sql として保存します。

default データベースなら conf/db/fixtures/default/message.sql になります。

conf/db/fixtures 以下に置いたファイルのうちどれを使うかは、application.conf で設定します。

設定キーは開発モードとテストモードで別の fixture を使えるように分けました。


開発用は

     db.default.fixtures.dev="insert_data.sql"

テスト用は

     db.default.fixtures.test="insert_data.sql"

fixture スクリプトは複数指定することもできます。

     db.default.fixtures.dev=[ "insert_data.sql", "insert_additional_data.sql" ]

複数指定した場合は、指定した順番で SQL が実行されます。(Downs は逆に実行されます。)


テスト

テストのときは FakeApplication を使えば application.conf の設定を上書きして Fixture を切り替えることも可能です。


  running (FakeApplication(
    additionalConfiguration =
      Map("db.default.fixtures.test" -> 
        List("insert_test_data.sql", "insert_test_data2.sql").asJava) ++
      inMemoryDatabase(name = "default",
                       options = Map("DB_CLOSE_DELAY" -> "-1")))) {
                         
      // test ....

  }

asJava しなきゃいけないのがいけてないですね。Play が typesafe-config をラップしきれてないせいです。

これは pull request を送って取り込まれたのでそのうち書かなくてもよくなります。

https://github.com/playframework/Play20/pull/938


サンプルアプリ

https://github.com/seratch/scalikejdbc/tree/1.5.2/scalikejdbc-play-plugin/test/zentasks

Play 付属のサンプル、zentasks を scalikejdbc に変えたものです。

(ちなみに マイグレーションには evolutions ではなく play-flyway を使っています。)



scalikejdbc は最近いろいろ機能が増えてきて、なんかすごいことになってるのですが、

あまり伝わってなさげなので、そろそろドキュメントとかに力入れたほうがいいのかもですね。

2013-03-29

Play flyway プラグインを作りました

| 00:33 |

https://github.com/tototoshi/play-flyway

Play にはもともと Evolutions というデータベースマイグレーション機能がついていますが、それと同じような機能を Flyway で作りました。

Flyway は Java 製のデータベースマイグレーションライブラリなんですが、Java でコードを書いたり、XML を使ったりではなく、Evolutions と同様に、Plain な SQL ファイルを使うのが基本です。

http://flywaydb.org/


Motivation 1 (重要)

Play の Evolutions Plugin って実は DBPlugin に依存しているので、DBPlugin を使わない人にとってはちょっともどかしいところがあります。DBPlugin を使わない人ってのは例えば


  • Mongo とか使ってる人
  • Scalikejdbc の PlayPlugin など、DBPlugin に依存しない形で RDB を使っている人

などです。そういう人は不要でも Evolutions を使うためには DBPlugin を無効にできません。

さらに、Evolutions は実はインターフェースとしての DBPlugin と DBPlugin の実装である BoneCPPlugin の両方に依存しているため、

も Evolutions を使いたければ BoneCPPlugin をオフにできません。


というわけでなんでもいいから DBPlugin に依存しない Evolutions の代替が欲しかった。


Motivation 2

Evolutions は Ups と Downs 書きますが、DB マイグレーション派閥には 「Downs いらないんじゃね? 派」もいるらしく Evolutions の feature request にもたしかそんなのがありました。あと Evolutions の場合は Ups と Downs を両方とも同じファイルに書くので怖い、という意見もあります。


Flyway は Ups のみで Downs はありません。これはまあ好みだと思います。


インストール

Build.scala/build.sbt に dependency を追加して

libraryDependencies += "com.github.tototoshi" %% "play-flyway" % "0.1.0"

play.plugins にこれを書く。

1000:com.github.tototoshi.play2.flyway.Plugin

使い方

SQL スクリプトの配置の仕方

conf/db/migration/${dbName} 以下に置いてください。

default なら conf/db/migration/default です。

ファイル名は flyway の規約に従って下さい。V1__create_nantoka_table.sql のようになります。詳しくは Flyway のドキュメントを見てください。


Dev

Evolutions と同じくこんな感じの画面が出てくるのでぽちっとしてください。

f:id:tototoshi:20130329002536p:image

テスト

テスト時はマイグレーションは自動で適用されます。


Play 2 では h2インメモリデータベースを使っている人も多いかと思いますが、そのときは注意が必要です。インメモリデータベースは接続が切れるとデータが消えるので、それを防ぐために DB_CLOSE_DELAY=-1 を設定します。Play の inMemoryDatabase ヘルパーを使えば以下のようにして DB_CLOSE_DELAY オプションを設定できます。

running(FakeApplication(
  additionalConfiguration =
    inMemoryDatabase(name = "default", options = Map("DB_CLOSE_DELAY" -> "-1"))
)) {
  test()
}

ちなみにこのオプション指定機能をつけたのは私です。ドヤ

https://github.com/playframework/Play20/commit/f99b03d35f843c8e536435e6c597f8383c51dee0


Production

Production モードだとマイグレーションが必要な状態では起動できません。開発時のようにボタンを押してぼちっとかできないので。Evolutions と同じです。

db.${dbName}.migration.auto を application.conf で指定することでマイグレーション適用できます。

あ、ちゃんとバックアップとかしてからにしてくださいね。


まとめ

プルリクお待ちしております。