MongoURI がいい加減すぎワロエナイ

Heroku + MongoLab 環境に Casbah を使って MongoDB に接続する Play 2.0 で作成したアプリケーションをデプロイしようとしました。

それで Heroku 上で MONGOLAB_URI の設定値として提供される MongoDB の接続 URI を com.mongodb.casbah.MongoURI(com.mongodb.MongoURI)に食わせたらまったくもってでたらめなパースをするので自分でなんとかするコードを書きました。使い方は以下のコード内のコメント参照。MongoDB の接続 URI は conf/application.conf 内で mongodb.default.uri で設定するか、起動時に -D オプションで与えるようにしてください。

package mongodb

import play.api._
import Play.current
import com.mongodb.casbah.Imports._

/**
 * MongoDB をサクッと使うためのオブジェクトです。
 */
object Mongo {
  /**
   * MongoDB 接続用 URI をパースするための正規表現
   */
  val uriPattern = "mongodb://(?:([^:^@]+)(?::([^@]+))?@)?([^:^/]+)(?::(\\d+))?/(.+)".r

  /**
   * MongoDB に接続されたブロックを提供します。
   * 接続 URI は mongodb.default.uri の設定値を使用します。
   *
   * 使い方の例
   * {{{
   *   MongoDB.withDB { db =>
   *     db("employee").findAll().toSeq
   *   }
   * }}}
   */
  def withDB[A](f: MongoDB => A): A = withDB("default")(f)

  /**
   * MongoDB に接続されたブロックを提供します。
   *
   * 使い方の例
   * {{{
   *   MongoDB.withDB("mydb") { db =>
   *     db("employee").findAll().toSeq
   *   }
   * }}}
   *
   * @param name 設定名
   */
  def withDB[A](name: String)(f: MongoDB => A): A = {
    // Play を使わない場合は↓このあたりを変えるといい
    val uri = Play.configuration.getString("mongodb." + name + ".uri").getOrElse("mongodb://localhost/test")

    // URI のパース
    val (usernameOpt, passwordOpt, host, portOpt, database) = parseURI(uri)

    // MongoDB に接続
    val conn = portOpt map { port =>
      MongoConnection(host, port.toInt)
    } getOrElse {
      MongoConnection(host)
    }

    try {
      // DB 取得
      val db = conn(database)
      // 必要とあれば認証処理を行う
      (usernameOpt, passwordOpt) match {
        case (Some(username), Some(password)) => db.authenticate(username, password)
        case _ =>
      }
      // 引数で指定された処理を実行
      f(db)
    } finally {
      conn.close()
    }
  }

  /**
   * MongoDB 接続 URI をパースします。
   */
  def parseURI(uri: String) = {
    uri match {
      case uriPattern(username, password, host, port, database) =>
        (Option(username), Option(password), host, Option(port), database)
    }
  }
}

Play 2.0 でカスタムフォームデータマッピング

前回の記事で、「あれ? Double(Float)型使えないの? 少数とかどうするの?」と思った方もいると思います。そうでないにしても、他の型にマッピングしたいというのは当然の要望でしょう。

そこで今回は、任意の型にフォームデータをマッピングする方法を紹介します。

まず必要なのは play.api.data.format.Formatter トレイトの実装クラスです。これを提供するメソッドを作成します。作成する場所は object であればどこでもかまいませんが、どうせ controllers でしか使用しないというのであれば、controllers のパッケージオブジェクトだと何かと楽です(controllers のパッケージオブジェクトでは上手くいかないようでした)。なぜ object かというと、以下のように implicit def なので import が必要だからです。

まず、作成するメソッドに必要なクラス等を使用する import 宣言は以下のとおりです。

import play.api.data._
import Forms._
import play.api.data.format._
import Formats._

そして作成するメソッドですが、以下は Double 型へマッピングする場合の例です。

  /**
   * リクエストデータを Double 型に変換するフォーマッタを取得します。
   */
  implicit def doubleFormat = new Formatter[Double] {

    /**
     * このフォーマットについて画面に表示する際に使用するメッセージ
     */
    override val format: Option[(String, Seq[Any])] = Some("format.double", Nil)

    /**
     * リクエストデータを Double 型に変換します。
     *
     * @param key リクエストデータを取り出す際に使用するキー値
     * @param data リクエストデータ
     * @return Double 型への変換が失敗した場合フォームエラー。成功した場合変換した値
     */
    def bind(key: String, data: Map[String, String]): Either[Seq[FormError], Double] = {
      stringFormat.bind(key, data).right.flatMap { s =>
        scala.util.control.Exception.allCatch[Double]
          .either(s.toDouble) // 文字列から Double に変換する処理
          .left.map(e => Seq(FormError(key, "error.double", Nil))) // それが失敗した場合
      }
    }

    /**
     * Double 型の値から、リクエストデータ用の文字列に変換します。
     */
    def unbind(key: String, value: Double): Map[String, String] = Map(key -> value.toString)
  }

ここまでやればもう使い物になるのですが、もう一声、上記のメソッドの直下あたりで以下のようにやっておくとより格好がつきます。

  val double = of[Double]

このようにフォームデータマッピングで使用できます。

  val helthForm = Form(
    tuple(
      "tall" -> double,
      "weight" -> optional(double)))

Play 2.0 でのフォーム定義

Play 2.0 が正式リリースされました。Play 1.x が Java 用フレームワークであとから Scala モジュールを加えることで Scala でも利用できていたのに対して Play 2.0 は最初から Scala に対応しています。というよりも、Scala メインと言ってもいいでしょう。

Play 2.0 はとても使いやすい Web アプリケーションフレームワークですが、まだまだ情報が不足しています。今回は公式ドキュメントの Handling form submission の補足を行います。

どんなフォームデータマッピングが使用きるのか?

公式ドキュメントではフォームデータのマッピングは text と number についてだけ少しだけ述べているに過ぎません。ですが、デフォルトでもたくさんのマッピングが利用できます。

text

フォームデータを String 型にマッピングします。文字数を制限したい場合は以下のように書きます(省略可)。minLength で最小文字数、maxLength で最大文字数を指定します。

"username" -> text(minLength = 3, maxLength = 100)

nonEmptyText

文字数0を許容しない text です。text に minLegnth = 1 と指定した場合と同じ動きになりますが、制約メッセージを表示した場合、text の場合だと Minimum Value: 1 と表示されるのに対してこれは Required と表示されます。nonEmptyText も text 同様に文字数を制限することができます。

 number

フォームデータを Int 型にマッピングします。以下のように値の範囲を制限することもできます(省略可)。

"size" -> number(min = 10, max = 20)

longNumber

フォームデータを Long 型にマッピングします。なぜか number のような指定の仕方で範囲の制限はできません。

date

フォームデータを java.util.Date 型にマッピングします。引数でフォーマットを指定できます(省略可)。フォーマット形式は java.text.SimpleDateFormat に準じます。

"birthday" -> number("yyyyMMdd")

sqlDate

フォームデータを java.sql.Date 型にマッピングします。date 同様フォーマットを引数で指定できます。

email

フォームデータを String 型にマッピングしますが、メールアドレスとして妥当な形式でなければなりません。といっても、ものすごく厳密にメールアドレスの形式を検証しているわけではありません(必要十分なレベル)。

boolean

フォームデータを Boolean 型にマッピングします。フォームデータが “true” であれば true、”false” であれば false になります。必ず小文字でなければならないようです。

checked

フォームデータを Boolean 型にマッピングします(boolean と同様の性質)が、必ず true にならなければなりません。true でない場合、引数で指定したエラーメッセージを表示します。

"accepted" -> checked("使用許諾に同意してください。")

ignored

フォームデータをマッピングしません。代わりに引数で指定したデフォルト値を設定します。これは O/R マッパ等のエンティティにフォームデータをマッピングするときにあるフィールドはフォームデータの値とひもづけない等といった場合に使用します。

"id" -> ignored(0)

optional

フォームデータを Option[A] 型にマップします。省略可能な値である場合に使用します。引数として、型パラメータ A に対応するマッピングを指定します。

"age" -> optional(number(0, 120))

list

フォームデータを List[A] 型にマップします。複数選択値を扱う際に使用します。引数として、型パラメータ A に対応するマッピングを指定します。

"favorite" -> list(text)

seq

フォームデータを Seq[A] 型にマップします。使い方は list と同じです。

Play 2.0 RC3 における i18n の勘所

Play 2.0 RC3 で i18n を行う場合、他の Web アプリケーションフレームワークに比べてちょっと違うかなと思ったところがあったのでメモ。

まず、自分のアプリケーションの conf/messages でメッセージ定義するのは多分ナシ

なぜかといいますと、Play そのもの(play_2.9.1.jar)が持っている messages のほうが優先されるので、たとえば、

contraint.required=必須

みたいなことをやっても「Required」というもともとのメッセージが表示されてしまいます。

ではどうすれば良いかといいますと、明示的にロケールを指定してやればOK。ただしこれにも癖があります。

リソースバンドルを使い慣れた Java プログラマは、ロケールが ja_JP ならば messages_ja_JP または messages_ja が読み込まれるのではないかと当たりをつけることでしょう。Github 上の Wiki によればファイル名とロケールのセパレータはアンダースコアではなくピリオドなのでロケールを指定するときは messages.ja のように書けばいいことがわかります。

ところがここがハマりポイント! ロケールが ja_JP のとき、このファイルは読み込まれません!なんと、ja と ja_JP は Play では別物なのです!

しかも、Java では ja_JP とロケールを表記するのは当たり前ですが、Play では ja-JP と書き表します。

ということで、メッセージの定義は messages.ja-JP で行うのが無難そうです。

アナリスト、ストラテジストが足りない

我々の業界(ソフトハウス、SIer)には、圧倒的に「アナリスト」や「ストラテジスト」が足りないと思います。

「私はカンナ掛けが最高に上手いです」「寸分の狂いなく木組みします」という腕のいい大工がやってきて「さあ、どんな家を作りましょうか?」といったところで家を建てようと思う人からは仕事を取れるはずもないでしょう。

では最高に上手い設計をする建築士ではどうでしょうか? たぶん、それでもダメです。

お客さんの夢、これからの人生をどうあゆんでいきたいかを汲み取った上で「こんな家はどうでしょう?」と言えるようでなければなりません。

SEには提案力が求められると簡単に言われますが、業務的、経営的分析力と戦略を立てる能力がなければカンナ掛けの上手さのアピールの延長線上から抜け出すことはできないと思います。