SkinnyORMのjoin定義について

## はじめに
[Scala Advent Calendar](http://qiita.com/advent-calendar/2016/scala) 10日目の記事。Skinny ORMという、ScalaのScalikeJDBCをベースにしたORMのjoin定義について、コードを見ながら解説していく。

## joinの基本
SkinnyORMでjoinをするときは、まずそのテーブルの結合が、以下のどのパターンに当てはまるか考える。

- belongsTo
- hasOne
- hasMany(hasManyThrough)

そのうえでCRUDMapperにjoin定義を書けば良い。たとえばStation毎に1つのLineに所属するようなTableを考えると、次のようなものになる。

```scala
case class Line(id: Long, name: String)

object Line extends SkinnyCRUDMapperWithId[Long, Line] { ... }

case class Station(id: Long, name: String, lineId: Long, line: Option[Line] = None)

object Station extends SkinnyCRUDMapperWithId[Long, Station] {
  ...
  belongsTo[Line](
    right = Line,
    merge = (station, line) => station.copy(line = line)
  ).byDefault
}
```

`.byDefault` となっているので、特に明示的になにかをする必要はなく、自動的にStation classのlineにコピーされる。その処理はbelongsToの中のmergeによって指示される。

もしdefaultでjoinされるのが困るのであれば、

```scala
lazy val lineRef = belongsTo[Line](...)
```

などと定義し、

```scala
Station.joins(Station.lineRef).findAll()
```

と呼び出せば明示的にjoinできる。

以上がSkinnyORMのjoinにおける概要である。詳細は[公式ドキュメント](http://skinny-framework.org/documentation/orm.html)を参照。

## withJoinConditionメソッド
多くの場合は上記のようにすれば足りるが、複雑なjoinを行うには、SkinnyORMが自動で行なっていることを理解する必要がある。ソースコードを紐解いてみよう。

joinまわりの実装の殆どは`AssociationsFeature` traitで実装されている。さきほど使った`belongsTo`メソッドもそこにある。見てみよう。

[skinny/orm/feature/AssociationsFeature.scala#L192](https://github.com/skinny-framework/skinny-framework/blob/d2bf1d27d291b8b4606839d86ae7172128b6676c/orm/src/main/scala/skinny/orm/feature/AssociationsFeature.scala#L192)

```scala
def belongsTo[A](right: AssociationsWithIdFeature[_, A], merge: (Entity, Option[A]) => Entity): BelongsToAssociation[Entity] = {
  val fk = toDefaultForeignKeyName[A](right)
  belongsToWithJoinCondition[A](right, sqls.eq(this.defaultAlias.field(fk), right.defaultAlias.field(right.primaryKeyFieldName)), merge)
}
```

少なくとも、返り値が`BelongsToAssociation[Entity]`というcase classであること、それを生成しているのは`belongsToWithJoinCondition`というメソッドであることが分かる。

[skinny/orm/feature/AssociationsFeature.scala#L197](https://github.com/skinny-framework/skinny-framework/blob/d2bf1d27d291b8b4606839d86ae7172128b6676c/orm/src/main/scala/skinny/orm/feature/AssociationsFeature.scala#L197)

```scala
def belongsToWithJoinCondition[A](right: AssociationsWithIdFeature[_, A], on: SQLSyntax, merge: (Entity, Option[A]) => Entity): BelongsToAssociation[Entity] = {
  val joinDef = leftJoinWithDefaults(right, on)
  val extractor = extractBelongsTo[A](right, toDefaultForeignKeyName[A](right), right.defaultAlias, merge)
  new BelongsToAssociation[Entity](this, unshiftJoinDefinition(joinDef, right.defaultJoinDefinitions.filter(_.enabledEvenIfAssociated)), extractor)
}
```

ここで`BelongsToAssociation[Entity]`が実際に生成されていることが分かる。

`belongsToWithJoinCondition`で増えているのは`on: SQLSyntax`という引数のみである。これは結合条件ではないだろうか。逆に言うと`belongsTo`メソッドでは結合条件を自動生成しているのでは。その部分を抜き出すと

```scala
val fk = toDefaultForeignKeyName[A](right)
sqls.eq(this.defaultAlias.field(fk), right.defaultAlias.field(right.primaryKeyFieldName)
```

つまりrightがLineであるので、外部キー名はそのテーブル名を用いてline_idであると推測され、rightはWithIdなAssociationであるのでPrimaryKeyFieldは当然Line.idで、その2つが等価であるような結合条件を自動生成する。

大体の場合はこれで正しいので、最小限書けばあとはSkinnyORMが自動でやってくれるように見える。すばらしい。これではだめなとき(これもまた割と良くある)はWithJoinCondition付きのメソッドを使えば良いのだ。

※実はForeignKeyがdefaultと異なる場合、`belongsTowithJoinCondition`ではだめである。詳しくは後述する。

## withFkメソッド
ソースコードを見ていくと近くには他にもBelongsToAssociationを生成しているメソッドが沢山見つかる。順番に見てみよう。

[skinny/orm/feature/AssociationsFeature.scala#L203](https://github.com/skinny-framework/skinny-framework/blob/d2bf1d27d291b8b4606839d86ae7172128b6676c/orm/src/main/scala/skinny/orm/feature/AssociationsFeature.scala#L203)

```scala
def belongsToWithFk[A](right: AssociationsWithIdFeature[_, A], fk: String, merge: (Entity, Option[A]) => Entity): BelongsToAssociation[Entity] {
    belongsToWithFkAndJoinCondition(right, fk, sqls.eq(this.defaultAlias.field(fk), right.defaultAlias.field(right.primaryKeyFieldName)), merge)
}
```

これは先の`belongsTo`のJoinCondition自動生成におけるfkのみをカスタムしてConditionを生成するものだろうか。呼び出している`belongsToWithFkAndJoinCondition`を見てみる

[skinny/orm/feature/AssociationsFeature.scala#L207](https://github.com/skinny-framework/skinny-framework/blob/d2bf1d27d291b8b4606839d86ae7172128b6676c/orm/src/main/scala/skinny/orm/feature/AssociationsFeature.scala#L207)

```scala
def belongsToWithFkAndJoinCondition[A](right: AssociationsFeature[A], fk: String, on: SQLSyntax, merge: (Entity, Option[A]) => Entity): BelongsToAssociation[Entity] = {
    val joinDef = leftJoinWithDefaults(right, on)
    val extractor = extractBelongsTo[A](right, fk, right.defaultAlias, merge)
    new BelongsToAssociation[Entity](this, unshiftJoinDefinition(joinDef, right.defaultJoinDefinitions.filter(_.enabledEvenIfAssociated)), extractor)
  }
```

この2つのメソッドの関係が、先のwithFk無しの場合とほとんど同じようなものであるというのはすぐに分かるであろう。しかしForeignKeyはjoinConditionの指定をするためだけではなく、Extractorでも指定している。したがって、先に出てきた`belongsToWithJoinCondition`でjoinCondition内のForeignKeyを書き換えてもExtractorでエラーが出る筈で、異なるForeignKeyを使う場合はwithFkの付いたメソッドを用いる必要がある。

なお、Extractorはきちんと追っていないものの、おそらくはjoin結果を取得するまわりの設定になっていると思われる。

## withAliasメソッド
さらに見ていくと`belongsToWithAlias`というメソッドがある。

[skinny/orm/feature/AssociationsFeature.scala#L213](https://github.com/skinny-framework/skinny-framework/blob/d2bf1d27d291b8b4606839d86ae7172128b6676c/orm/src/main/scala/skinny/orm/feature/AssociationsFeature.scala#L213)

```scala
def belongsToWithAlias[A](right: (AssociationsWithIdFeature[_, A], Alias[A]), merge: (Entity, Option[A]) => Entity): BelongsToAssociation[Entity] = {
  val fk = if (right._1.defaultAlias != right._2) {
    val fieldName = right._1.primaryKeyFieldName
    val primaryKeyFieldName = fieldName.head.toString.toUpperCase + fieldName.tail
    right._2.tableAliasName + primaryKeyFieldName
  } else {
    toDefaultForeignKeyName[A](right._1)
  }
  belongsToWithAliasAndFk(right, fk, merge)
}
```

引数`right`がtupleになり、Aliasが追加されている。Alias型はどこかで見たことないだろうか？良く見る場所としては、ボイラーコード的に書いている、

```scala
override val defaultAlias: Alias[Station] = createAlias("s")
```

これがAliasの1つである。`SkinnyORMCURDMapper`はこのように、table毎にdefaultAliasが定義することになっており、selectした結果をextractするときなどはalias名を使って処理する。たとえば

```sql
SELECT ... FROM station s LEFT JOIN line l on ...
```

のようなQueryを生成する。

実際のコードを確認してみると、withAliasの付いていないメソッドでは、joinConditionとextractorでdefaultAliasを使っていた。defaultAliasで困る場合にはwithAliasのメソッドを使う、ということであろう。

ちなみにdefaultで駄目な場合とはどのようなものであろうか。分かりやすい例で言えば、同じテーブルを2回joinする場合などは、片方のAliasをdefaultから書き換えることが必須である(区別付かないので)。SkinnyORMで沢山のjoinを行なっていると、いつのまにかtableが2箇所出てきて突然動かなくなったりすることはある(経験済)。

あとは不幸にも同じAliasを別のtableで使った場合とかもあるだろうか。(もちろんそんなことが無いようにcreateAliasの文字が被らないように気を使ってますよね？)

直下にある`belongsToWithAliasAndFk`や`belongsToWithAliasAndFkAndJoinCondition`などは説明不要であろう。特に後者は最もカスタムの効く形態で自動生成も最小に抑えてあり、デバッグ時などに役立つかもしれない。

## IdのないTableをjoinする
ところでお気付きだろうか。ほぼ全てのメソッドの`right`のtableの型が`AssociationsWithIdFeature`になっていること、そしてまれに`AssociationsFeature`なっていることを。その名のとおり前者は後者に対し、IdをPrimaryKeyとする制約条件を加えたものである。

つまりrightでIdのないTableをjoinしたいときは、まれな`AssociationsFeature`を引数に取るメソッド、

- `belongsToWithFkAndJoinCondition`
- `belongsToWithAliasAndFkAndJoinCondition`

のどちらかを使う必要がある。

逆に言えばそれ以外のメソッドは右辺のprimary keyが暗黙にidであることを仮定している、ということであり、ForeignKeyの生成(line_idは右辺のPrimaryKeyがidであることを仮定する)やJoinConditionの生成(右辺のPrimaryKeyの定義が必要)などがそれに該当する。

最初書いたときは勘違いしていたが、TableにPrimaryKeyを設定することは可能で、ただbelongsToはそれを利用したりはしていない、というのが正しいようだ。hasOneやhasManyにこのような制限は無い。(primaryKeyさえ設定していればNoId等でも動く)

## まとめ
主にbelongs-to関係のjoinを作るメソッドをソースコードを混じえつつ解説を試みた。同様にhas-one関係やhas-many関係なども、勿論いくらか引数は増えて複雑にはなっているものの、基本は同じように作られているので、コードを追えば分かるようになっている筈である。