LoginSignup
6
4

More than 1 year has passed since last update.

TypeORM SelectQueryBuilderでJoinとwithDeletedの挙動

Posted at

TypeORMでSelectQueryBuilderをよく使うのですが、論理削除されたレコードを取得するwithDeletedメソッドについてのドキュメントがシンプルすぎてテーブルをJOINしたときの挙動がよくわからなかったので実際に動かして調べてみました。

※ 動かしてみてきっとこうだろうと推測される内容を書いてます。この動作が保証されるわけではないです。

前提

TypeORM

TypeORM 0.2.31 で確認しました。
Changelogを見る限り、これ以降0.3.9までの間で特に変更入ってないようです

SelectQueryBuilderのwithDeletedメソッドについてドキュメントで見つけられたのはここだけでした。他にあるのかなあ?

DB

他のものでもそんなに変わらないと思うけど、手元で試したのはPostgreSQLです。

例として扱うテーブルたち

companyテーブル(会社)
  departmentテーブル(部署)
    memberテーブル(署員)
clientテーブル(顧客)
  • companyとdepartment、departmentとmember、companyとclientはそれぞれ1対多のリレーションを持つことにします。
  • Entityはそれぞれ、Company、Department、Member、Clientとします。
    • Entityの定義はさきほどのドキュメントのように定義されていることにします。

やってみた

1種類のEntity(Company)を取得する場合

この場合はドキュメントに書かれているのと同じなのですが、Companyで論理削除されたものも含めてすべて取得しようとすると

const companies = await dataSource
  .getRepository(Company)
  .createQueryBuilder('company')
  .select('company.id', 'id')
  .withDeleted()
  .getMany();

のようになります。

JOINする場合

ここからが本題です。

CompanyとDepartmentをJOINする場合

前述の通り、CompanyとDepartmentは1対多です。

getMany直前にwithDeletedメソッドを追加

どういう動きをするのか全然わからなかったので、とりあえずこうしてみました。

const companies = await dataSource
  .getRepository(Company)
  .createQueryBuilder('company')
  .select('company.id', 'id')
  .leftJoinAndSelect('company.departments', 'department')
  .withDeleted()
  .getMany();

結果は次のような組み合わせになって、Companyは論理削除に関わらず取得されますが、Departmentは論理削除されたものは取得できませんでした。
お?Department全部取れてこないじゃんと混乱したのはいい思い出。

Company
論理削除
Department
論理削除
Company
取得
Department
取得
- -
- -
-
-

※ 論理削除されたものが、されていないものが-。取得できたものが、されなかったものが-

JOIN直前にwithDeletedメソッドを追加

ここで、「もしかしてCompanyにしかwithDeleted効かない?」と思ったのですが、諦めが悪いので順番を変えて突っ込むことにしました。

const companies = await dataSource
  .getRepository(Company)
  .createQueryBuilder('company')
  .select('company.id', 'id')
  .withDeleted()
  .leftJoinAndSelect('company.departments', 'department')
  .getMany();

結果は次のような組み合わせになって、Departmentも論理削除に関わらず取得されました。

Company
論理削除
Department
論理削除
Company
取得
Department
取得
- -
-
-

どうやらwithDeletedの位置によって論理削除でも取得される・されないが変わりそうというのはわかってきました。

CompanyとDepartmentとMemberをJOINする場合

それじゃあ、3つJOINしたときはどうなるんだ?という疑問が出てくるのでやってみました。
前述の通り、CompanyとDepartmentは1対多、DepartmentとMemberも1対多です。
JOINの位置とwithDeletedの位置が関係しそうだったので、ケース分けしてみました。

すべてのJOIN前にwithDeletedメソッドを追加

とりあえずJOINする前にwithDeletedメソッドを追加しました。

const companies = await dataSource
  .getRepository(Company)
  .createQueryBuilder('company')
  .select('company.id', 'id')
  .withDeleted()
  .leftJoinAndSelect('company.departments', 'department')
  .leftJoinAndSelect('department.members', 'member')
  .getMany();

すべてのEntityが論理削除に関わらず取得されました。

Company
論理削除
Department
論理削除
Member
論理削除
Company
取得
Department
取得
Member
取得
- - -
- -
- -
-
- -
-
-

(項目増えると図がわかりづらいですね)
とりあえずJOIN前に追加しておけばよさそうだというのがわかってきました。

DepartmentをJOINした後にwithDeletedメソッドを追加

順番に検証したかったので、次はDepartmentのJOIN後にしてみました。

const companies = await dataSource
  .getRepository(Company)
  .createQueryBuilder('company')
  .select('company.id', 'id')
  .leftJoinAndSelect('company.departments', 'department')
  .withDeleted()
  .leftJoinAndSelect('department.members', 'member')
  .getMany();

ここまで来ると割と想像できそうですが、Departmentは論理削除されてるものが取得できませんでした。なので自動的に論理削除されたDepartmentに紐付いているMemberも取れてきませんでした。

Company
論理削除
Department
論理削除
Member
論理削除
Company
取得
Department
取得
Member
取得
- - -
- - - -
- -
- - -
- -
- - -
-
- -

MemberをJOINした後にwithDeletedメソッドを追加

最後はMemberのJOIN後にしてみました。

const companies = await dataSource
  .getRepository(Company)
  .createQueryBuilder('company')
  .select('company.id', 'id')
  .leftJoinAndSelect('company.departments', 'department')
  .leftJoinAndSelect('department.members', 'member')
  .withDeleted()
  .getMany();

DepartmentもMemberも論理削除されてるものが取得できませんでした。

Company
論理削除
Department
論理削除
Member
論理削除
Company
取得
Department
取得
Member
取得
- - -
- - - -
- - -
- - -
- -
- - -
- -
- -

結論

どうやらwithDeletedメソッドの後にJOINしたEntityは論理削除に関わらずデータを取得できるようだということが見えてきました。
逆にそれ以外のEntityは論理削除されたものが取得されないようです。

蛇足

前提で定義してたけど使っていなかったClientはDepartmentとかMemberとは直接関係のないリレーションなのですが、こういうEntityが複数ある場合JOINの順番をものすごく考慮しなければなりません。

「こっちのEntityは論理削除されたものも必要だけど、あっちのEntityはいらなくて、そっちは必要で…」
「論理削除されたものも必要な場合はwithDeletedの後にJOINして、いらないものはその前にJOINして…」

これは実装するときも見直すときもレビューするのも辛いです。
なぜJOINする際のメソッド(innerJoinとかleftJoinとか)にオプション定義しなかったのかなぁと勝手なことを思ってしまいました。

一応、簡単にできる対応方法としてはすべてのJOIN前にwithDeletedを追加して、論理削除されたものが必要ないEntityはJOIN時の条件にdeletedAt IS NULLを追加する、のようにして対処できます。

混乱するのを避けるためにも、データの整理とか設計の見直しとかをしてなるべくそういう状況にならないようにしたいですね。

6
4
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
6
4