scalikeJDBC でのカラム名

# はじめに

scalikeJDBC を使っていて

```
"Failed to retrieve value because Column 'member.name' not found..
If you're using SQLInterpolation, you may mistake u.id for u.resultName.id."
```
みたいなエラーで怒られ

*「u なんていうテーブル使ってない memberテーブルだし、id なんてカラム使ってない nameカラムだし、お前は何を言っているんだ？」*

という思いをしたことありませんか。
scalikeJDBC の生成するカラム名の仕組みがわかればエラーメッセージの言いたいこともわかってあげれるようになります。

その仕組みと基本的なカラム名の使い方について説明します。

ちなみに

```
"If you're using SQLInterpolation, you may mistake u.id for u.resultName.id." 
```
の部分は scalikeJDBC のコードに固定で書かれている文字列なのでいつも u とid が出てくるのはしょうがないのです。

以下にあげるコードは ScalikeJDBC 3.3.1 で確認しました。

# 今回の例に使うDBテーブルとクラスと SQLSyntaxSupport

DB には以下の groupsテーブルと membersテーブルがあるとします。

```sql
create table groups(id int, name varchar(20));
create table members(id int, name varchar(20), group_id int);
```

`members` の `group_id` が `groups` への外部キーです。

それぞれのテーブルに対応するクラスは以下のように定義します。

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

`Member` には関連する `Group` の情報ものせるようにしました。
「groupId は group.map(_.id) として取れるからいらないんじゃないの？」と思われるかもしれません。ごもっともです。
でも事情があるんです。それについては後ほど説明します。

それぞれのテーブルに対応する SQLSyntaxSupport は以下のように定義します。
（SQLSyntaxSupport は「ORM の一歩手前、DRY な SQL を記述するためのサポート機能」です）

```scala
import scalikejdbc._

object Group extends SQLSyntaxSupport[Group] {
  override val tableName = "groups"
  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"
  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)))
  }
}
```

`tableName` が対応するテーブル名です。
この `tableName` を override しなかった場合は SQLSyntaxSupport に与えられたクラス名を基にした名前をテーブル名とします。
例えば `SQLSyntaxSupport[Group]` に対応するデフォルトのテーブル名は `group` になります。

`rs: WrappedResultSet` が select された値にアクセスできるデータです。

# 使用例

次にこれらを使う側のコードです。

```scala
import scalikejdbc._

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()
}
```

m と g がテーブルに対応して適切なカラム名を提供してくれる SQLSyntaxProvider です。

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

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

`from(Member as m)` が `from members me` に、
`leftJoin(Group as g)` が `left join groups gr` に
対応していることがわかります。

SQLSyntaxSupport とか SQLSyntaxProvider とか出てきてややっこしいですが、
SQLSyntaxSupport はテーブルそのものに対応して、SQLSyntaxProvider はテーブルへの参照に対応しているというところでしょうか。
例えば membersテーブルを自己結合する必要がでてきた場合は

```scala
val m = Member.syntax("me")
val m2 = Member.syntax("me2")
...
  select(...).from(Member as m)
  .leftJoin(Member as m2)
```
のようにテーブルへの参照として `m` と `m2` の2つの SQLSyntaxProvider を使うことになります。

# 3種のカラム名

これらのコードでは各カラムに対応する3種類のコードが出てきています。
例えば `Member` のカラム `id` に関しては

```scala
m.id
m.resultName.id
m.result.id
```
の3つです。
（このうち `m.resultName.id` はこの形としては今回直接は出てきていませんが `m.resultName` が `Member.apply` の引数の `rn` として渡ってそこで `rn.id` として使っていますから 結局 `m.resultName.id` として使っていることになります)


この3種がそれぞれどういう文字列に変換されるかまとめると次のようになります。

| コード           | SQL中の対応する文字列   | 解説                                         |
|------------------|------------------|----------------------------------------------|
| m.id             | me.id            | テーブル別名.カラム名                    |
| m.resultName.id  | i_on_me          | カラム名の短縮名\_on\_テーブル別名         |
| m.result.id      | me.id as i_on_me | m.id対応文字列 as m.resultName.id対応文字列 | 

カラム名の短縮名はキャメルケースの各先頭文字をくっつけたものになります。
例えば `id` の場合は `i`、`name` の場合は `n`、`groupId` の場合は `gi` になります。
もし短縮名が重なる場合は連番が付いていきます。
例えば `name` の他に `note` というカラムがあった場合は `name` は `n` に、`note` は `n2` になります。

便宜のためこの3種の各系統に呼び名を付けておきましょう。

- `m.id`系を「原始系」
- `m.resultName.id`系を「resultName系」
- `m.result.id`系を「result系」

と呼ぶことにします。

今回の例では select では `m.result.id` のような result系を使って（別名部分が resultName系になって）、値を取得するときは `m.resultName.id` のような resultName系を使っていますが、select に使う名前と取得時の名前が適切に対応してさえいればよいはずですから 

```scala
withSQL {
  select(m.id, ...) // 原始系で select
  ...
}.map { rs => ... rs.long(m.id) ... } // 原始系で取得
```
でもよさそうです。
実際今回はそれでも動きます。しかし 原始系をおすすめしない事情があります。
今回の例では

```scala
select(m.result.id, m.result.name, m.result.groupId, g.result.id, g.result.name).from(...) ...
```
と全カラムを書き並べていましたが、from および join したテーブルの全カラムを result系で自動的に展開してくれる

```scala
select.from(...) ...
// または
selectFrom(...) ...
```
という書き方があります。
またサブクエリとして使うときのことを考えても別名が付いていた方がよいので result系に統一した方がよいでしょう。

# カラム名の使い方のまとめ

結論として基本的に次のように使います。

- select では result系を使う（select.from や selectFrom も result系）
- WrappedResultSet から値を取得するときは resultName系を使う
- where や on や orderBy では 原始系を使う


# カラム名生成の仕組み

ところで、m や g に `id` などのメソッドを定義した覚えもないのにどうして `m.id` などがコンパイルエラーにもならずに使えるのでしょう。

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

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

実行時
`field("groupId")` // ここでスネークケース化などの名前変換する
=> `c("group_id")`
=> `column("group_id")`
この `column`メソッドはそれぞれの系統毎に定義されていて

- `m.column` は 原始系のカラム名を、
- `m.resultName.column` は resultName系のカラム名を、
- `m.result.column` は result系のカラム名を

生成します。
また `column`メソッドは対応するカラム名が実際のテーブルに存在しているかのチェックも行います。

# カラム名の妥当性チェック

上で説明したようにカラム名の妥当性チェックは `selectDynamic`メソッドと `column`メソッドの2カ所で行われ、例えば

- `m.foo` と書いたときは `Member`クラスに `foo` はないためコンパイル時の selectDynamicメソッドでエラーになります。
- `m.group` と書いたときは `Member`クラスに `group` はあるのでコンパイル時エラーになりませんが `members`テーブルには `group` というカラムはないので実行時の columnメソッドでエラーになります。

さて、前に書いた「Memberクラスに groupId はいらないんじゃないの」の件ですが、
もし `Member`クラスに `groupId` がないとすると、テーブル的には問題ないにも関わらず `m.groupId` はコンパイル時の selectDynamicメソッドのチェックでエラーになってしまいます。それを避けるため冗長ではありますが `groupId` も持たせました。

# 他にも書きたかったこと

カラム名変換(nameConverters)やサブクエリでのカラム名についても書きたかったのですが準備不足で今回は間に合いませんでした。
準備ができたら続編として書きたいと思います。

2019-02-10 補足： カラム名変換については[scalikeJDBC の nameConverters によるカラム名変換](https://qiita.com/saikyouyaki/items/ad11ac1d646cf0b20783)を書きました。