26
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Opt TechnologiesAdvent Calendar 2018

Day 7

scalikeJDBC でのカラム名

Last updated at Posted at 2018-12-23

はじめに

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);

membersgroup_idgroups への外部キーです。

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

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)

のようにテーブルへの参照として mm2 の2つの SQLSyntaxProvider を使うことになります。

3種のカラム名

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

m.id
m.resultName.id
m.result.id

の3つです。
(このうち m.resultName.id はこの形としては今回直接は出てきていませんが m.resultNameMember.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 の場合は iname の場合は ngroupId の場合は gi になります。
もし短縮名が重なる場合は連番が付いていきます。
例えば name の他に note というカラムがあった場合は namen に、noten2 になります。

便宜のためこの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.groupIdm.result.groupIdm.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 によるカラム名変換を書きました。

26
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?