scalikeJDBC One-to-X APIを使いこなす(基本編)
なんの記事だい?
scalikeJDBCの One-to-X API を使いこなすためのまとめ記事です。
scalikeJDBCでselect文相当のコードを書くこと自体はそんなに難しくないのですが、
欲しいDBモデルに変換していく作業( .one().toMany().map()とかそのあたりのコードです)をわりと色々な感じでかけるため、
改めて基礎を抑えたく記事にしてみました。
リレーショナルモデルにおける「結合」の基本と照らし合わせながら、scalikeJDBCの書き方をまとめてみたつもりです👧
内部結合の場合
Corporatesの一要素は複数のuserを持つかもしれない。
また、Usersの一要素は一つのcorporateを持つような関係のテーブルがあるとします。
(リレーショナルモデルの考え方に基づき、集合論としてイメージを持ちたいので、一行と言わず一要素と言うことにします!)
そもそも「結合」とは、こういう新しい集合を返す操作ですね!!!
これをscalikeJDBCで書くならばどうなるのか?というところですが、
まず結合した結果としてどんな値が欲しいか考えます。
それによって、select文の後にする処理が変わってきます。
- 起点にしたテーブルの値が欲しい
- 結合したテーブルの値も欲しい
それぞれのパターンについて、実装例を記します。
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なのが、感覚的に書けてとてもグッドですね👍
外部結合の場合
外部結合する場合は、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)
というタプルになっています👏
図で表すとこんな感じです。
また、外部結合する集合が二つ以上あるときは、 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するなり、サブクエリを書くなり、ひとくせあったな〜と感じた例を紹介しきれなかったので、また次回 [応用編] ということで記事にしたいと思います。
Author And Source
この問題について(scalikeJDBC One-to-X APIを使いこなす(基本編)), 我々は、より多くの情報をここで見つけました https://qiita.com/natsumisawa/items/c644fdb4043d63a6629d著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .