scalikeJDBC One-to-X APIを使いこなす（基本編）

## なんの記事だい？
scalikeJDBCの [One-to-X API]((http://scalikejdbc.org/documentation/one-to-x.html)) を使いこなすためのまとめ記事です。
scalikeJDBCでselect文相当のコードを書くこと自体はそんなに難しくないのですが、
欲しいDBモデルに変換していく作業（ .one().toMany().map()とかそのあたりのコードです）をわりと色々な感じでかけるため、
改めて基礎を抑えたく記事にしてみました。
リレーショナルモデルにおける「結合」の基本と照らし合わせながら、scalikeJDBCの書き方をまとめてみたつもりです👧

## 内部結合の場合
![IMG_5569.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137322/556dd276-3b3d-c314-77b2-f76a864e393e.jpeg)

Corporatesの一要素は複数のuserを持つかもしれない。
また、Usersの一要素は一つのcorporateを持つような関係のテーブルがあるとします。

（リレーショナルモデルの考え方に基づき、集合論としてイメージを持ちたいので、一行と言わず一要素と言うことにします！）

![iOS の画像 (14).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137322/8cfa0528-6c9a-d80e-6fca-b40fbfd27379.jpeg)

*そもそも「結合」とは、こういう新しい集合を返す操作ですね！！！*

これをscalikeJDBCで書くならばどうなるのか？というところですが、
まず結合した結果としてどんな値が欲しいか考えます。
それによって、select文の後にする処理が変わってきます。

1. 起点にしたテーブルの値が欲しい
2. 結合したテーブルの値も欲しい

それぞれのパターンについて、実装例を記します。

### 1. 起点にしたテーブルの値が欲しい

例えば、ユーザーが一人でもいる企業のリストが欲しいとします。
Corporatesを起点にしてUsersをjoinすれば、userが一人も紐づいていないcorporate要素は結合されずに結果から落とされ、欲しかった企業のリストが取得できます。

この場合、以下のコードのようにselect文後にmapの中で
`rs: WrappedResultSet (ある集合の一要素に値する型。のちに定義記載。)`
を `Corporates型` （DBモデルの型）に変換してあげればOkです。

```scala
withSQL[Corporates] {
  select
    .from(Corporates.as(corporatesTable))
    .join(Users.as(usersTable))
    .on(corporatesTable.corporateId, usersTable.corporateId)
}.map {
  rs => Corporates(corporatesTable)(rs)
}.list()
  .apply()
```

list()はselect文で抽出できた複数の要素をリストで返します。（最初の一つの要素だけ返すメソッドとかもある）

ログを見てみると、以下のようなクエリになっています。
ちなみに実際にクエリが発行されるのは、apply()が実行された時です。

```sql
select *(省略) from corporates corporatesTable
inner join users usersTable
on corporatesTable.corporate_id = usersTable.corporate_id;
```

また、select文の結果として得られた各要素を利用し、オリジナルのクラスとして結果を返すこともできます。

```scala
{ select文 }
.map { rs =>
   val corporate = Corporates(corporatesTable)(rs)
   CorporateNameModel(s"株式会社 ${corporate.name}")
}.list()
  .apply()
```

### 2. 結合したテーブルの値も欲しい
ユーザーに所属企業情報も含めたリストが欲しいとします。
Usersを起点としてCorporatesをjoinすれば、Usersの一要素は必ず一つのcorporateを持ちます。
つまり、select文の結果として、必ずユーザーと企業が1:1で取得できているでしょう。
そう仮定した上で、select文の後は以下のように書いていきます。

```scala
withSQL[Users] {
  select
    .from(Users.as(usersTable))
    .join(Corporates.as(corporatesTable))
    .on(corporatesTable.corporateId, usersTable.corporateId)
}.one(Users(usersTable))
  .toOne(Corporates(corporatesTable))
  .map((user, corporate) => // userとcorporateが一要素ずつ詰まったタプル、これはとてもリレーショナルモデル的...
    CorporateUserModel(corporate.id, user.id, user.name...)
  )
  .list()
  .apply()
```

1:1なので、.one().toOne()と書けばOkなのが、感覚的に書けてとてもグッドですね👍

## 外部結合の場合

![iOS の画像 (13).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137322/2a60670d-08e9-69e1-687f-bc2e94b7b99c.jpeg)

外部結合する場合は、Corporatesが起点ですね。

Corporatesの一要素は、userを持っているかもしれないし、持っていないかもしれません。
なので、 corporate:userが `1:[0 ~ n]` の関係ですね。

select文を書く動機として、主に2種類考えられると思います。
1. 結合したテーブルの値がlistで欲しい （1:[0 ~ n]）
2. 結合したテーブルの値がOptionで欲しい (1:[0 ~ 1])

### 1. 結合したテーブルの値がlistで欲しい （1:[0 ~ n]）
アプリケーションの要件的に言い換えると、「会社とそれにひもづくユーザーの一覧が欲しい、ユーザーが一人もいない場合も一覧に入れて欲しい。」という感じですね。
こういう場合は以下のようなコードになります。

```scala
withSQL[Corporates] {
  select
    .from(Corporates.as(corporatesTable))
    .leftJoin(Users.as(usersTable))
    .on(corporatesTable.corporateId, usersTable.corporateId)
}.one(Corporates(corporatesTable))
  .toMany(
    rs => rs.longOpt(usersTable.resultName.corporateId).map(_ => Users(usersTable)(rs))
  )
  .map((corporate, users) => (corporate, users)) // 一つのcorporateに対して、userがlistで付いてくる
  .list()
  .apply()
```

one-to-manyの関係なので、 `one().toMany()` とそのままコードに落とせば欲しい形になります！

toManyの中では、 `rs: WrappedResultSet =>` で結合したUsersの要素が一要素ずつ渡されます。

`WrappedResultSet` の定義は以下で、 `java.sql.ResultSet` をラップしたものになります。

```scala
case class WrappedResultSet(underlying: ResultSet, cursor: ResultSetCursor, index: Int)
// indexにはその要素の"行数"が確保されていたりする
```

leftJoinなので、もしかするとそのUsers一要素の全カラムの値が `NULL` かもしれないですね。（😇）
なので、 `longOpt("カラム名")` で値にアクセスしてみて、
もし「これはNULLで結合()されちゃった要素ですねえ」となればNoーを返し、
そうでなければその要素をSome(Users型)に変換して返しています。

one().toMany()を通過するとon句でつなげた部分がよしなに束ねられ、
mapしたときには `(corporate: Corporates, users: Seq[Users)` というタプルになっています👏

![IMG_5570.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137322/034e616c-2d25-ac48-91e6-c3cbe600ef18.jpeg)


図で表すとこんな感じです。

また、外部結合する集合が二つ以上あるときは、 `toManies()` （複数形）を使うべきなので注意です。

ちなみに、toManies()に渡すことのできる引数の上限は `9個まで` です😂
私のチームが開発しているアプリケーションで9個以上leftJoinしているところがあり、その制約を頑張って回避したことがある（leftJoinだからといってむやみにtoManiesに渡しちゃダメだよ、というだけの話だが・・・）のでそれも後々記事にしようと思います。

### 2. 結合したテーブルの値がOptionで欲しい (1:[0 ~ 1])
仮に、一つの会社はユーザーを一人までしか持たない（アドミンユーザーならあり得そう）設計だったとします。
その場合、上記の .one().toMany() でかいてしまうと、mapの中でusersに対してheadOptionしなければならない気がしてきますね・・・。
そういう時は、 `toOptionalOne` を使います。

```scala
withSQL[Corporates] {
  select
    .from(Corporates.as(corporatesTable))
    .leftJoin(Users.as(usersTable))
    .on(corporatesTable.corporateId, usersTable.corporateId)
}.one(Corporates(corporatesTable))
  .toOptionalOne(
    rs => rs.longOpt(usersTable.resultName.corporateId).map(_ => Users(usersTable)(rs))
  .map((corporate, userOpt) => (corporate, userOpt))
  .list()
  .apply()
```

toOptionalOne()に渡している部分のコードはは、先ほどのtoMany()のときと同じです。
万が一、「一つの会社にユーザー二人いるやないかい・・・！」（DB設計的にはあり得てしまうと思うので）となったときには、以下のような実行時例外がはかれます。

```
scalikejdbc.IllegalRelationshipException: one-to-one relation is expected but it seems to be a one-to-many relationship.
```

便利なようで、ちょっと怖い。
one-to-manyがアプリケーション仕様的にあり得ないのであれば、例外処理をしておきたいですね。

## さいごに
[one-to-one-to-one]や[one-to-many-to-one]や[one-to-many-to-many]を簡潔に書くなり、同じテーブルを2回joinするなり、サブクエリを書くなり、ひとくせあったな〜と感じた例を紹介しきれなかったので、また次回 [応用編] ということで記事にしたいと思います。