少年易酔學難成

IT/技術的な話題について書きます

Scala開発日記 2019/05/14

5月から1年ぶりにScalaを使った開発が始まりました。

1年ぶりにScalaを触った感想は、「思った以上に忘れてる、ヤバイ」でした。

せっかくまたScala触れたのだから、今度はわすれないよう、日々のScala開発で感じたことやハマったことをメモしていこうと思います。

目次

  • ScalikeJDBCのTypeBinderでハマった話

  • PlayFrameworkの(QueryString|Path)Bindableをもっと便利にしたい

ScalikeJDBCのTypeBinderでハマった話

現在の仕事は、とあるシステムの内部APIをPlayFramework+Scalaで作成するというものです。 DBへのアクセスにScalikeJDBCを使っています。

現象

Scalaではなるべく型安全にしたいということで値オブジェクトを使っています。

一方、DBはレガシーなもので、nullableなカラムが数多くあります。

そのnullableなカラムの値をTypeBinder[Option[A]]を使って受け取るときに問題が起きました。

実際のコードとは少し違う1のですが、最小で問題を再現するTypeBinderは↓の通りです。

import scalikejdbc._

case class UserName(value: String) extends AnyVal

implicit lazy val userNameBinder: Binders[UserName] = Binders.string.xmap(UserName, _.value)

これを使ってカラムのnullを受けようとしたところ、期待していたNoneではなくSome(UserName(null))という大変ざんねんな結果になってしまいました。

原因

値オブジェクトのバインドにLowPriorityTypeBinderImplicits#optionによって生成されたTypeBinder[Option[UserName]]が使われるからです。

implicit def option[A](implicit ev: TypeBinder[A]): TypeBinder[Option[A]] = new TypeBinder[Option[A]] {
  def apply(rs: ResultSet, columnIndex: Int): Option[A] = wrap(ev(rs, columnIndex))
  def apply(rs: ResultSet, columnLabel: String): Option[A] = wrap(ev(rs, columnLabel))
  private def wrap(a: => A): Option[A] =
    try Option(a) catch { case _: NullPointerException | _: UnexpectedNullValueException => None }
}

UserNameの場合、先にTypeBinder[UserName]を使って受けてからOptionでくるんでいるのでSome(UserName(null))のようになります。 もし値オブジェクトの保持する値がLongなら発生しなかったとでしょう。

解決方法

色々アホなことして遠回りしましたが2、ScalikeJDBCがLongなど他のAnyValを処理している方法を真似して、UserNameにバインドする時に文字列がnullだったらNullPointerExceptionを吐くようにしました。

implicit lazy val userNameBinder: Binders[UserName] = Binders.string.xmap(throwExceptionIfNull(UserName), _.value)

// Bindersのコンパニオンオブジェクトで使われているもの
def throwExceptionIfNull[A, B](f: B => A): B => A = {
  a => if (a == null) throw new NullPointerException else f(a)
}

PlayFrameworkの(QueryString|Path)Bindableをもっと便利にしたい

これは未解決というか感想レベルの話なんですけど、(QueryString|Path)Bindableって既存の(QueryString|Path)Bindableからの導出方法がtransformしかなくて、Prismを使って上手く導出出来ないかな?って思っています。 bindの処理を合成するときにflatMapみたいな感じでやりたいです。

まあ、今日は時間もないし、とりあえず良い案でるまで開発進めながら考えようという感じです。


  1. 値オブジェクトの導入によるボイラープレートを軽減するためIsoとか値オブジェクト用のtraitやらを使ってみている。

  2. きっと体調がわるかったせいと自分を慰めている。