Merged PR 470: [Sp-19]アカウント削除時に削除するテーブルについて、on delete cascadeではなくコード上で削除を行うよう修正する

## 概要
[Task2783: [Sp-19]アカウント削除時に削除するテーブルについて、on delete cascadeではなくコード上で削除を行うよう修正する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2783)

アカウント削除時に削除するテーブルについて、on delete cascadeではなくコード上で削除を行うよう修正
一部ユニットテスト用にutilityのcreateForeignKeyConstraints: falseに指定
LGTM後、すべてのテーブルのレコード削除の動作確認をDEV環境で実施します。

## レビューポイント
DBマイグレーションファイルが正しく修正されているか

## UIの変更
なし

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
maruyama.t 2023-10-16 09:02:38 +00:00
parent 897bad289b
commit 00f4966aa9
6 changed files with 327 additions and 50 deletions

View File

@ -0,0 +1,33 @@
-- +migrate Up
ALTER TABLE `checkout_permission` DROP FOREIGN KEY `checkout_permission_fk_task_id`;
ALTER TABLE `tasks` DROP FOREIGN KEY `tasks_fk_account_id`;
ALTER TABLE `template_files` DROP FOREIGN KEY `template_files_fk_account_id`;
ALTER TABLE `option_items` DROP FOREIGN KEY `option_items_fk_worktype_id`;
ALTER TABLE `worktypes` DROP FOREIGN KEY `worktypes_fk_account_id`;
ALTER TABLE `audio_option_items` DROP FOREIGN KEY `audio_option_items_fk_audio_file_id`;
ALTER TABLE `audio_files` DROP FOREIGN KEY `audio_files_fk_account_id`;
ALTER TABLE `user_group_member` DROP FOREIGN KEY `user_group_member_fk_user_group_id`;
ALTER TABLE `user_group` DROP FOREIGN KEY `user_group_fk_account_id`;
ALTER TABLE `license_allocation_history` DROP FOREIGN KEY `license_allocation_history_fk_account_id`;
ALTER TABLE `card_licenses` DROP FOREIGN KEY `card_licenses_fk_license_id`;
ALTER TABLE `licenses` DROP FOREIGN KEY `licenses_fk_account_id`;
ALTER TABLE `license_orders` DROP FOREIGN KEY `license_orders_fk_from_account_id`;
ALTER TABLE `sort_criteria` DROP FOREIGN KEY `sort_criteria_fk_user_id`;
ALTER TABLE `users` DROP FOREIGN KEY `users_fk_account_id`;
-- +migrate Down
ALTER TABLE `checkout_permission` ADD CONSTRAINT `checkout_permission_fk_task_id` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `tasks` ADD CONSTRAINT `tasks_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `template_files` ADD CONSTRAINT `template_files_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `option_items` ADD CONSTRAINT `option_items_fk_worktype_id` FOREIGN KEY (`worktype_id`) REFERENCES `worktypes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `worktypes` ADD CONSTRAINT `worktypes_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `audio_option_items` ADD CONSTRAINT `audio_option_items_fk_audio_file_id` FOREIGN KEY (`audio_file_id`) REFERENCES `audio_files` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `audio_files` ADD CONSTRAINT `audio_files_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `user_group_member` ADD CONSTRAINT `user_group_member_fk_user_group_id` FOREIGN KEY (`user_group_id`) REFERENCES `user_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `user_group` ADD CONSTRAINT `user_group_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `license_allocation_history` ADD CONSTRAINT `license_allocation_history_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `card_licenses` ADD CONSTRAINT `card_licenses_fk_license_id` FOREIGN KEY (`license_id`) REFERENCES `licenses` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `licenses` ADD CONSTRAINT `licenses_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `license_orders` ADD CONSTRAINT `license_orders_fk_from_account_id` FOREIGN KEY (`from_account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `sort_criteria` ADD CONSTRAINT `sort_criteria_fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `users` ADD CONSTRAINT `users_fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -3,6 +3,7 @@ import { DataSource } from 'typeorm';
import { User, UserArchive } from '../../repositories/users/entity/user.entity';
import { Account } from '../../repositories/accounts/entity/account.entity';
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
import { License } from '../../repositories/licenses/entity/license.entity';
type InitialTestDBState = {
tier1Accounts: { account: Account; users: User[] }[];
@ -381,3 +382,14 @@ export const getUserArchive = async (
): Promise<UserArchive[]> => {
return await dataSource.getRepository(UserArchive).find();
};
export const getLicenses = async (
datasource: DataSource,
account_id: number,
): Promise<License[]> => {
const licenses = await datasource.getRepository(License).find({
where: {
account_id: account_id,
},
});
return licenses;
};

View File

@ -35,6 +35,7 @@ import {
makeTestUser,
makeHierarchicalAccounts,
getUser,
getLicenses,
getUserArchive,
} from '../../common/test/utility';
import { AccountsService } from './accounts.service';
@ -49,7 +50,11 @@ import {
USER_ROLES,
WORKTYPE_MAX_COUNT,
} from '../../constants';
import { License } from '../../repositories/licenses/entity/license.entity';
import {
License,
LicenseAllocationHistory,
LicenseOrder,
} from '../../repositories/licenses/entity/license.entity';
import {
overrideAccountsRepositoryService,
overrideAdB2cService,
@ -60,7 +65,6 @@ import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
import {
createLicenseAllocationHistory,
createOrder,
getLicenseArchive,
getLicenseAllocationHistoryArchive,
@ -72,7 +76,7 @@ import { AdB2cUser } from '../../gateways/adb2c/types/types';
import { Worktype } from '../../repositories/worktypes/entity/worktype.entity';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { UsersService } from '../users/users.service';
describe('createAccount', () => {
let source: DataSource = null;
beforeEach(async () => {
@ -90,7 +94,6 @@ describe('createAccount', () => {
await source.destroy();
source = null;
});
it('アカウントを作成できる', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
@ -5073,8 +5076,9 @@ describe('アカウント情報更新', () => {
it('アカウント情報を更新する(ディーラーアカウントが未入力)', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const { tier3Accounts: tier3Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
source,
);
const adduser = await makeTestUser(source, {
account_id: tier4Accounts[0].account.id,
external_id: 'typist-user-external-id',
@ -5355,45 +5359,117 @@ describe('deleteAccountAndData', () => {
it('アカウント情報が削除されること', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
// 第階層のアカウント作成
const tier4Accounts = await makeHierarchicalAccounts(source);
const { account: account1, admin: admin1 } = await makeTestAccount(source, {
parent_account_id: tier4Accounts.tier4Accounts[0].account.id,
});
const account = account1;
const admin = admin1;
const context = makeContext(admin.external_id);
// 第五階層のアカウント作成
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: account.id,
// 第一~第四階層のアカウント作成
const {
tier1Accounts: tier1Accounts,
tier2Accounts: tier2Accounts,
tier3Accounts: tier3Accounts,
tier4Accounts: tier4Accounts,
} = await makeHierarchicalAccounts(source);
// 第五階層のアカウント作成A
const tier5AccountsA = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
// ユーザの作成
const user = await makeTestUser(source, {
account_id: tier5Accounts.account.id,
// 第五階層のアカウント作成B
const tier5AccountsB = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
// ライセンス作成
await createLicense(
// ユーザの作成A
const userA = await makeTestUser(source, {
account_id: tier5AccountsA.account.id,
});
// ユーザの作成B
const userB = await makeTestUser(source, {
account_id: tier5AccountsB.account.id,
});
const context = makeContext(tier5AccountsA.admin.external_id);
// 第一階層~第五階層までのライセンス注文を作成
await createLicenseOrder(
source,
1,
new Date(),
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
user.id,
null,
null,
tier2Accounts[0].account.id,
tier1Accounts[0].account.id,
100,
'PO001',
);
await createLicenseAllocationHistory(
await createLicenseOrder(
source,
1,
user.id,
1,
tier5Accounts.account.id,
'NONE',
tier3Accounts[0].account.id,
tier2Accounts[0].account.id,
90,
'PO002',
);
await createLicenseOrder(
source,
tier4Accounts[0].account.id,
tier3Accounts[0].account.id,
80,
'PO003',
);
await createLicenseOrder(
source,
tier5AccountsA.account.id,
tier4Accounts[0].account.id,
40,
'PO004A',
);
await createLicenseOrder(
source,
tier5AccountsB.account.id,
tier4Accounts[0].account.id,
40,
'PO004B',
);
// 第一階層~第五階層までのライセンス注文を発行済みにする
await service.issueLicense(
context,
tier2Accounts[0].account.id,
tier1Accounts[0].users[0].external_id,
1,
'PO001',
);
await service.issueLicense(
context,
tier3Accounts[0].account.id,
tier2Accounts[0].users[0].external_id,
2,
'PO002',
);
await service.issueLicense(
context,
tier4Accounts[0].account.id,
tier3Accounts[0].users[0].external_id,
3,
'PO003',
);
await service.issueLicense(
context,
tier5AccountsA.account.id,
tier4Accounts[0].users[0].external_id,
4,
'PO004A',
);
await service.issueLicense(
context,
tier5AccountsB.account.id,
tier4Accounts[0].users[0].external_id,
4,
'PO004B',
);
// アカウントAのライセンスを取得する
const licensesA = await getLicenses(source, tier5AccountsA.account.id);
// アカウントAのライセンスを取得する
const licensesB = await getLicenses(source, tier5AccountsB.account.id);
const usersService = module.get<UsersService>(UsersService);
// アカウントAのライセンスを割り当てる
await usersService.allocateLicense(context, userA.id, licensesA[0].id);
// アカウントBのライセンスを割り当てる
await usersService.allocateLicense(context, userB.id, licensesB[0].id);
// ADB2Cユーザーの削除成功
overrideAdB2cService(service, {
@ -5403,25 +5479,68 @@ describe('deleteAccountAndData', () => {
overrideBlobstorageService(service, {
deleteContainer: jest.fn(),
});
// アカウント情報の削除
await service.deleteAccountAndData(
context,
tier5Accounts.admin.external_id,
tier5Accounts.account.id,
tier5AccountsA.admin.external_id,
tier5AccountsA.account.id,
);
// DB内が想定通りになっているか確認
const accountRecord = await getAccount(source, tier5Accounts.account.id);
expect(accountRecord).toBe(null);
// 第五階層のアカウントAが削除されていること
const accountRecordA = await getAccount(source, tier5AccountsA.account.id);
expect(accountRecordA).toBe(null);
const userRecordA = await getUser(source, userA.id);
expect(userRecordA).toBe(null);
const userRecord = await getUser(source, user.id);
expect(userRecord).toBe(null);
// 第五階層のアカウントAのライセンスが削除されていること
const licenseRecordA = await source.manager.find(License, {
where: { account_id: tier5AccountsA.account.id },
});
expect(licenseRecordA.length).toBe(0);
// 第五階層のアカウントAのライセンス注文履歴が削除されていること
const licenseOrderRecordA = await source.manager.find(LicenseOrder, {
where: { from_account_id: tier5AccountsA.account.id },
});
expect(licenseOrderRecordA.length).toBe(0);
// 第五階層のアカウントAのライセンス割り当て履歴が削除されていること
const LicenseAllocationHistoryRecordA = await source.manager.find(
LicenseAllocationHistory,
{
where: { account_id: tier5AccountsA.account.id },
},
);
expect(LicenseAllocationHistoryRecordA.length).toBe(0);
// 第五階層のアカウントBは削除されていないこと
const accountRecordB = await getAccount(source, tier5AccountsB.account.id);
expect(accountRecordB.id).not.toBeNull();
const userRecordB = await getUser(source, userB.id);
expect(userRecordB).not.toBeNull();
// 第五階層のアカウントBのライセンスが削除されていないこと
const licenseRecordB = await source.manager.find(License, {
where: { account_id: tier5AccountsB.account.id },
});
expect(licenseRecordB.length).not.toBe(0);
// 第五階層のアカウントBのライセンス注文履歴が削除されていないこと
const licenseOrderRecordB = await source.manager.find(LicenseOrder, {
where: { from_account_id: tier5AccountsB.account.id },
});
expect(licenseOrderRecordB.length).not.toBe(0);
// 第五階層のアカウントBのライセンス割り当て履歴が削除されていないこと
const LicenseAllocationHistoryRecordB = await source.manager.find(
LicenseAllocationHistory,
{
where: { account_id: tier5AccountsB.account.id },
},
);
expect(LicenseAllocationHistoryRecordB.length).not.toBe(0);
const UserArchive = await getUserArchive(source);
expect(UserArchive.length).toBe(2);
const LicenseArchive = await getLicenseArchive(source);
expect(LicenseArchive.length).toBe(1);
expect(LicenseArchive.length).toBe(40);
const LicenseAllocationHistoryArchive =
await getLicenseAllocationHistoryArchive(source);

View File

@ -13,6 +13,7 @@ import {
import { User, UserArchive } from '../users/entity/user.entity';
import { Account } from './entity/account.entity';
import {
CardLicense,
License,
LicenseAllocationHistory,
LicenseAllocationHistoryArchive,
@ -48,6 +49,14 @@ import {
import { DateWithZeroTime } from '../../features/licenses/types/types';
import { Worktype } from '../worktypes/entity/worktype.entity';
import { WorktypeIdNotFoundError } from '../worktypes/errors/types';
import { OptionItem } from '../worktypes/entity/option_item.entity';
import { Task } from '../tasks/entity/task.entity';
import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity';
import { AudioFile } from '../audio_files/entity/audio_file.entity';
import { AudioOptionItem } from '../audio_option_items/entity/audio_option_item.entity';
import { UserGroup } from '../user_groups/entity/user_group.entity';
import { UserGroupMember } from '../user_groups/entity/user_group_member.entity';
import { TemplateFile } from '../template_files/entity/template_file.entity';
@Injectable()
export class AccountsRepositoryService {
@ -966,9 +975,107 @@ export class AccountsRepositoryService {
.execute();
// アカウントを削除
// アカウントを削除することで、外部キー制約がで紐づいている関連テーブルのデータも削除される
const accountRepo = entityManager.getRepository(Account);
await accountRepo.delete({ id: accountId });
// ライセンス系(card_license_issue以外)のテーブルのレコードを削除する
const orderRepo = entityManager.getRepository(LicenseOrder);
await orderRepo.delete({
from_account_id: accountId,
});
const licenseRepo = entityManager.getRepository(License);
const targetLicenses = await licenseRepo.find({
where: {
account_id: accountId,
},
});
const cardLicenseRepo = entityManager.getRepository(CardLicense);
await cardLicenseRepo.delete({
license_id: In(targetLicenses.map((license) => license.id)),
});
await licenseRepo.delete({
account_id: accountId,
});
const LicenseAllocationHistoryRepo = entityManager.getRepository(
LicenseAllocationHistory,
);
await LicenseAllocationHistoryRepo.delete({
account_id: accountId,
});
// ワークタイプ系のテーブルのレコードを削除する
const worktypeRepo = entityManager.getRepository(Worktype);
const taggerWorktypes = await worktypeRepo.find({
where: { account_id: accountId },
});
const optionItemRepo = entityManager.getRepository(OptionItem);
await optionItemRepo.delete({
worktype_id: In(taggerWorktypes.map((worktype) => worktype.id)),
});
await worktypeRepo.delete({ account_id: accountId });
// タスク系のテーブルのレコードを削除する
const taskRepo = entityManager.getRepository(Task);
const targetTasks = await taskRepo.find({
where: {
account_id: accountId,
},
});
const checkoutPermissionRepo =
entityManager.getRepository(CheckoutPermission);
await checkoutPermissionRepo.delete({
task_id: In(targetTasks.map((task) => task.id)),
});
await taskRepo.delete({
account_id: accountId,
});
// オーディオファイル系のテーブルのレコードを削除する
const audioFileRepo = entityManager.getRepository(AudioFile);
const targetaudioFiles = await audioFileRepo.find({
where: {
account_id: accountId,
},
});
const audioOptionItemsRepo = entityManager.getRepository(AudioOptionItem);
await audioOptionItemsRepo.delete({
audio_file_id: In(targetaudioFiles.map((audioFile) => audioFile.id)),
});
await audioFileRepo.delete({
account_id: accountId,
});
// ユーザーグループ系のテーブルのレコードを削除する
const userGroupRepo = entityManager.getRepository(UserGroup);
const targetUserGroup = await userGroupRepo.find({
where: {
account_id: accountId,
},
});
const userGroupMemberRepo = entityManager.getRepository(UserGroupMember);
await userGroupMemberRepo.delete({
user_group_id: In(targetUserGroup.map((userGroup) => userGroup.id)),
});
await userGroupRepo.delete({
account_id: accountId,
});
// テンプレートファイルテーブルのレコードを削除する
const templateFileRepo = entityManager.getRepository(TemplateFile);
await templateFileRepo.delete({ account_id: accountId });
// ユーザテーブルのレコードを削除する
const userRepo = entityManager.getRepository(User);
await userRepo.delete({
account_id: accountId,
});
// ソート条件のテーブルのレコードを削除する
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
await sortCriteriaRepo.delete({
user_id: In(users.map((user) => user.id)),
});
return users;
});
}

View File

@ -94,7 +94,9 @@ export class License {
@UpdateDateColumn()
updated_at: Date;
@OneToOne(() => User, (user) => user.license)
@OneToOne(() => User, (user) => user.license, {
createForeignKeyConstraints: false,
}) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定
@JoinColumn({ name: 'allocated_user_id' })
user?: User;
}
@ -185,7 +187,9 @@ export class LicenseAllocationHistory {
@UpdateDateColumn()
updated_at: Date;
@ManyToOne(() => License, (licenses) => licenses.id)
@ManyToOne(() => License, (licenses) => licenses.id, {
createForeignKeyConstraints: false,
}) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定
@JoinColumn({ name: 'license_id' })
license?: License;
}

View File

@ -73,7 +73,9 @@ export class User {
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@ManyToOne(() => Account, (account) => account.user, { onDelete: 'CASCADE' }) // onDeleteはSQLite用設定値.本番用は別途migrationで設定
@ManyToOne(() => Account, (account) => account.user, {
createForeignKeyConstraints: false,
}) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定
@JoinColumn({ name: 'account_id' })
account?: Account;