2013-04-14
sbt-install 作った
Scala | |
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 するコマンド作った
Python | |
データ処理でヘッダーがついてる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のバージョンを比較するやつ書いた、が
Scala | |
Play はプロジェクトのバージョンと使用している Play コマンドのバージョンが食い違っていると警告が出るんですが、バージョンの比較が同じかどうかしか見ていないのにエラーメッセージが Update しろとかいうやつなので
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 _ =>
}
こういう風に、ん?なエラーメッセージになることがあります。
で、気持ち悪いので直そうと思ってバージョンを比較するのを書いたんですが、
よく考えたらエラーメッセージをちょっと直せばいいだけじゃん。このコード全然いらねー!バージョンパースするのにパーサコンビネータとか持ち出して自分ばっかじゃねーのってことに気づきました。プログラム書く前に必要かどうかちゃんと考えようって話ですね。
せっかく書いたので貼っておきますね。。。
そういえば、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"))
xuwei
2013/04/07 23:03
playコマンド使わずに、sbt直で起動してるから、気にしたことなかった・・・
2013-04-06
scalikejdbc-play-fixture-plugin を作りました
(また 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 プラグインを作りました
Scala | |
https://github.com/tototoshi/play-flyway
Play にはもともと Evolutions というデータベースマイグレーション機能がついていますが、それと同じような機能を Flyway で作りました。
Flyway は Java 製のデータベースマイグレーションライブラリなんですが、Java でコードを書いたり、XML を使ったりではなく、Evolutions と同様に、Plain な SQL ファイルを使うのが基本です。
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 と同じくこんな感じの画面が出てくるのでぽちっとしてください。
テスト
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 で指定することでマイグレーションを適用できます。
あ、ちゃんとバックアップとかしてからにしてくださいね。
まとめ
プルリクお待ちしております。

