8
2

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.

scalikeJDBC One-to-X APIを使いこなす(基本編)

Last updated at Posted at 2020-01-13

なんの記事だい?

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

内部結合の場合

IMG_5569.jpg

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

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

iOS の画像 (14).jpg

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

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

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

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

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

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

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

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()が実行された時です。

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

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

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

2. 結合したテーブルの値も欲しい

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

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

外部結合する場合は、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])

アプリケーションの要件的に言い換えると、「会社とそれにひもづくユーザーの一覧が欲しい、ユーザーが一人もいない場合も一覧に入れて欲しい。」という感じですね。
こういう場合は以下のようなコードになります。

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 をラップしたものになります。

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

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

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

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

2. 結合したテーブルの値がOptionで欲しい (1:[0 ~ 1])

仮に、一つの会社はユーザーを一人までしか持たない(アドミンユーザーならあり得そう)設計だったとします。
その場合、上記の .one().toMany() でかいてしまうと、mapの中でusersに対してheadOptionしなければならない気がしてきますね・・・。
そういう時は、 toOptionalOne を使います。

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するなり、サブクエリを書くなり、ひとくせあったな〜と感じた例を紹介しきれなかったので、また次回 [応用編] ということで記事にしたいと思います。

8
2
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
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?