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
を追加する、のようにして対処できます。
混乱するのを避けるためにも、データの整理とか設計の見直しとかをしてなるべくそういう状況にならないようにしたいですね。