はじめに
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テーブルがあるとします。
create table groups(id int, name varchar(20));
create table members(id int, name varchar(20), group_id int);
members
の group_id
が groups
への外部キーです。
それぞれのテーブルに対応するクラスは以下のように定義します。
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 を記述するためのサポート機能」です)
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 された値にアクセスできるデータです。
使用例
次にこれらを使う側のコードです。
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 は以下のようになります。
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テーブルを自己結合する必要がでてきた場合は
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
に関しては
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 に使う名前と取得時の名前が適切に対応してさえいればよいはずですから
withSQL {
select(m.id, ...) // 原始系で select
...
}.map { rs => ... rs.long(m.id) ... } // 原始系で取得
でもよさそうです。
実際今回はそれでも動きます。しかし 原始系をおすすめしない事情があります。
今回の例では
select(m.result.id, m.result.name, m.result.groupId, g.result.id, g.result.name).from(...) ...
と全カラムを書き並べていましたが、from および join したテーブルの全カラムを result系で自動的に展開してくれる
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 によるカラム名変換を書きました。