scalikeJDBC の nameConverters によるカラム名変換

# はじめに


前回の記事に引き続き scalikeJDBC のカラム名について取り上げます。
今回はカラム名の変換機能 nameConverters について説明します。
前回の記事[「scalikeJDBC でのカラム名」](https://qiita.com/saikyouyaki/items/f9e3e4f36615e26f9c17)のコードや説明の理解を前提としますので読んでいない方は一度目を通しておいてください。

# 前回の DBテーブルとの違い

前回の例での DBテーブルは次のようになっていました。

```sql
-- 前回の例
create table groups(id int, name varchar(20));
create table members(id int, name varchar(20), group_id int);
```

今回説明する例では groupsテーブルの id と membersテーブルの id のところがそれぞれ以下のように group_id と member_id になっていたとします。

```sql
-- 今回の例
create table groups(group_id int, name varchar(20));
create table members(member_id int, name varchar(20), group_id int);
```

# カラム名の変換 nameConverters

ただし scala のコードでは前回と同じく `group_id` と `member_id` のどちらも `id` という名前として扱いたいとします。

```scala
case class Group(id: Long, name: String)
case class Member(id: Long, name: String, groupId: Option[Long] = None, group: Option[Group] = None)
```

このまま前回と同じコードで動かすと

`m.id` や `m.result.id` や `m.resultName.id` は以下のように処理されていきます。

コンパイル時
=> `selectDynamic("id")` // ここで `Member`クラスのメンバ変数かのチェックも行う
=> `field("id")` に置き換わる
コンパイル時はここまで。

実行時
`field("id")` // ここでスネークケース化などの名前変換する
=> `c("id")`
=> `column("id")` // ここで実際のテーブルに該当カラムが存在しているかのチェックも行う

selectDynamicメソッドでのチェックでは `id` は `Member`クラスのメンバ変数ですからエラーになりませんが、columnメソッドのチェックで `members`テーブルに `id`カラムはないのでエラーになります。
columnメソッドにたどり着く前に "id" を "member_id" に変換したいのですが、それを実現できるのが nameConverters の機能です。

そのためにはテーブルに対応する SQLSyntaxSupport を以下のように定義します。

```scala
object Group extends SQLSyntaxSupport[Group] {
  override val tableName = "groups"
  override val nameConverters = Map("^id$" -> "group_id") // 前記事のコードとの違いはこの行だけ
  def apply(g: ResultName[Group])(rs: WrappedResultSet): Group = Group(rs.long(g.id), rs.string(g.name))
}

object Member extends SQLSyntaxSupport[Member] {
  override val tableName = "members"
  override val nameConverters = Map("^id$" -> "member_id") // 前記事のコードとの違いはこの行だけ
  def apply(rn: ResultName[Member], grn: ResultName[Group])(rs: WrappedResultSet): Member = {
    val gId = rs.longOpt(grn.id)
    Member(rs.long(rn.id), rs.string(rn.name), groupId = gId, group = gId.map(_ => Group(grn)(rs)))
  }
}
```

コードのコメントに書いたとおり前記事のコードとの違いは `nameConverters` を override しているところだけです。
`nameConverters` は `Map[String, String]` 型で、部分一致置換に使う正規表現用文字列と置換後文字列のペアからなる Map です。

`nameConverters` は fieldメソッドで使われカラム名を変換します。
例えば上の `Group` でのカラム名 `id` は

```scala
  override val nameConverters = Map("^id$" -> "group_id")
```
の正規表現 `^id$` にマッチするので `group_id` に置き換わります。

つまり今回は

実行時
`field("id")` // ここで nameConverters でカラム名変換する。その後スネークケース化もする
=> `c("member_id")`
=> `column("member_id")` // ここで実際のテーブルに該当カラムが存在しているかのチェックも行う

という処理の流れになります。


`nameConverters` の正規表現は部分一致変換に使われることに注意してください。
完全一致させたいときは `^id$` のように `^` （先頭一致） と `$` （末尾一致）で囲む必要があります。

これらを使う方は前回の記事のコードを変更することなくそのまま使えます。

```scala
// 前回の記事のコードとまったく同じ

val m = Member.syntax("me") // テーブル別名として "me" を使う
val g = Group.syntax("gr") //  テーブル別名として "gr" を使う

// 指定した groupId を持つ Member の一覧を取得
def memberList(groupId: Long)(implicit session: DBSession): Seq[Member] = {
  withSQL {
    select(m.result.id, m.result.name, m.result.groupId, g.result.id, g.result.name)
      .from(Member as m)
      .leftJoin(Group as g).on(sqls.eq(m.groupId, g.id))
      .where.eq(g.id, groupId)
      .orderBy(m.id)
  }.map { rs =>
    Member(m.resultName, g.resultName)(rs)
  }.list.apply()
}
```

この withSQL の中のコードに対応する SQL は以下のようになります。

```sql
select me.member_id as mi_on_me, me.name as n_on_me, me.group_id as gi_on_me, gr.group_id as gi_on_gr, gr.name as n_on_gr
  from members me
  left join groups gr on me.group_id = gr.group_id
  where  gr.group_id = ?
  order by me.member_id
```

`g` の `id` と `m` の `id` のところが短縮名も含めそれぞれカラム名変換された `group_id` と `member_id` に対応していることが確認できます。

# SQLSyntaxSupport の nameConverters と SQLSyntaxProvider  の nameConverters

SQLSyntaxSupport と SQLSyntaxProvider はぱっと見ただけで区別しづらいので以降それぞれ Support と Provider と略称します。

上の例では Support （`members` や `groups`）にだけ nameConverters を持たせたように見えますが実は Provider （`m` や `g`）も nameConverters を持っています。
カラム名の変換に使われるのは Provider の nameConverters の方です。上の例では Support の方にだけ設定しているように見えるのにちゃんとカラム名の変換ができています。これはどういうことでしょう。それは

```scala
val m = Member.syntax("me")
```
の `syntax` メソッドで Provider を作るときに Support である `Member` の `nameConverters` が Provider の `nameConverters` としても共有されるようになっているからです。

Support自身は nameConverters を何に使っているかというと DBテーブル名変換に使っています。
例えば

```scala
object Group extends SQLSyntaxSupport[Group] {
  // 今回は tableName を使ったテーブル名設定はしないとする
  // override val tableName = "groups"

  override val nameConverters = Map("^Group$" -> "groups", "^id$" -> "group_id")
  def apply(g: ResultName[Group])(rs: WrappedResultSet): Group = Group(rs.long(g.id), rs.string(g.name))
}
```
とするとクラス名 "Group" が `nameConverters` の `"^Group$" -> "groups"` によって "groups" に変換されてそれがテーブル名として設定されます。

Provider の nameConverters に Support の nameConverters とは異なるものを設定したり、同じテーブルを参照する複数の Provider にそれぞれ異なる nameConverters を設定したりすることも構造上可能と思われますが、それを使ったうまい使い道があるんでしょうか。
今のところ寡聞にして知りませんがあれば是非教えて欲しいです。


# 続く（つもり）

続編ではサブクエリのカラム名について書きたいと思ってます。