Play 2.3 Anorm の NamedParameter


Play 2.1 から Play 2.3 にアップデートした際の Anorm 関連のメモです。

* [Play 2.3 Migration Guide](http://www.playframework.com/documentation/2.3.x/Migration23)
* [Anorm > Executing SQL queries > Edge cases](https://github.com/playframework/playframework/blob/2.3.x/documentation/manual/scalaGuide/main/sql/ScalaAnorm.md#edge-cases)

## Play 2.1 Anorm

Play 2.1 の Anorm では、[SimpleSql#on](https://github.com/playframework/playframework/blob/2.1.x/framework/src/anorm/src/main/scala/anorm/Anorm.scala#L372) の可変長引数に、変数で渡したい時は、値を [ParameterValue](https://github.com/playframework/playframework/blob/2.1.x/framework/src/anorm/src/main/scala/anorm/Anorm.scala#L366) に変換しておく必要がありました。

```scala
// NG: Seq[(Symbol, Any)]
val params1 = Seq(
  'email -> "foo@example.net",
  'id -> 1
)
SQL("UPDATE users SET email = {email} WHERE id = {id}")
    .on(params1: _*).executeUpdate()
// [error]  found   : Seq[(Symbol, Any)]
// [error]  required: Seq[(Any, anorm.ParameterValue[_])]

// OK: Seq[(Symbol, ParameterValue[_ >: String with Int])]
val params2 = Seq(
  'email -> toParameterValue("bar@example.net"),
  'id -> toParameterValue(2)
)
SQL("UPDATE users SET email = {email} WHERE id = {id}")
    .on(params2: _*).executeUpdate()
```

## Play 2.3 Anorm

Play 2.3 の Anorm では [SimpleSql#on](https://github.com/playframework/playframework/blob/2.3.x/framework/src/anorm/src/main/scala/anorm/Anorm.scala#L201) の引数が [NamedParameter](https://github.com/playframework/playframework/blob/2.3.x/framework/src/anorm/src/main/scala/anorm/Anorm.scala#L158) に変更されていました。これまでの `(Any, ParameterValue[_])` の別名ではなく、以下のように新たにクラス定義されているので、先述の Play 2.1 の方法は使えなくなります。

```scala
case class NamedParameter(name: String, value: ParameterValue)
```

以下のように `Seq` の型パラメータに `NamedParameter` を指定して、暗黙変換するように修正する必要があります。

```scala
val params = Seq[NamedParameter](
  'email -> "foo@example.net",
  'id -> 1
)
SQL("UPDATE users SET email = {email} WHERE id = {id}")
    .on(params: _*).executeUpdate()

// 事前に値を ParameterValue に暗黙変換しておく方法
val values = Seq[ParameterValue]("bar@example.net", 2)
SQL("UPDATE users SET email = {email} WHERE id = {id}")
    .on('email -> values(0), 'id -> values(1)).executeUpdate()
```

注意すべき点として `ParameterValue` 自体も変更されていて `Any` からの暗黙変換を受け付けなくなっています。

```scala
val id: Any = 1
val row = SQL("SELECT * FROM users WHERE id = {id}")
    .on('id -> id).apply().head
// [error] No implicit view available from Any => anorm.ParameterValue
```

上記のように明示的に `Any` で渡すことはないと思いますが、パラメータを `Seq` に入れておいて `map` でまとめて変換しようと考えてしまうと、値の型が混在して `Any` になった時にコンパイルが通りません。

```scala
// NG: Seq[(Symbol, Any)]
val params = Seq('id -> 1, 'name -> "foo") map { t =>
  val np: NamedParameter = t; np
}

// NG: Seq[Any]
val values = Seq(123, "abc") map { v =>
  val pv: ParameterValue = v; pv
}
```

`anorm.features.anyToStatement` をインポートすることで `Any` も暗黙変換されるようになりますが、非推奨です。

[anorm.ToStatement.dateToStatement](https://github.com/playframework/playframework/blob/2.3.x/framework/src/anorm/src/main/scala/anorm/ToStatement.scala#L372) を見ると、パラメータ値が `java.util.Date` の場合、PreparedStatement 値として `java.sql.Timestamp` へ暗黙変換を行なっています。パラメータ値の型が `Any` だと `java.util.Date` のまま `java.sql.PreparedStatement#setObject` でセットされるので、JDBC によってはランタイムエラーが起こってしまうようです。