fix: bug save posMasterHistory, tuning performance script
This commit is contained in:
parent
9927c73547
commit
ef17236eb0
3 changed files with 186 additions and 18 deletions
|
|
@ -53,6 +53,7 @@ import {
|
||||||
} from "../keycloak";
|
} from "../keycloak";
|
||||||
// import { getPositionCounts, getCounts, getRootCounts } from "../services/OrganizationService";
|
// import { getPositionCounts, getCounts, getRootCounts } from "../services/OrganizationService";
|
||||||
import {
|
import {
|
||||||
|
BatchSavePosMasterHistoryOfficer,
|
||||||
CreatePosMasterHistoryEmployee,
|
CreatePosMasterHistoryEmployee,
|
||||||
CreatePosMasterHistoryOfficer,
|
CreatePosMasterHistoryOfficer,
|
||||||
SavePosMasterHistoryOfficer,
|
SavePosMasterHistoryOfficer,
|
||||||
|
|
@ -8062,9 +8063,26 @@ export class OrganizationController extends Controller {
|
||||||
// Clear current_holderId for positions that will have new holders
|
// Clear current_holderId for positions that will have new holders
|
||||||
const nextHolderIds = posMasterDraft
|
const nextHolderIds = posMasterDraft
|
||||||
.filter((x) => x.next_holderId != null)
|
.filter((x) => x.next_holderId != null)
|
||||||
.map((x) => x.next_holderId);
|
.map((x) => x.next_holderId) as string[];
|
||||||
|
|
||||||
if (nextHolderIds.length > 0) {
|
if (nextHolderIds.length > 0) {
|
||||||
|
// FIX: Fetch positions first before updating (to avoid race condition)
|
||||||
|
const posMastersToUpdate = await queryRunner.manager.find(PosMaster, {
|
||||||
|
where: {
|
||||||
|
orgRevisionId: currentRevisionId,
|
||||||
|
current_holderId: In(nextHolderIds),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save history BEFORE clearing current_holderId
|
||||||
|
const historyOps = posMastersToUpdate.map((pos) => ({
|
||||||
|
posMasterDnaId: pos.ancestorDNA,
|
||||||
|
profileId: null,
|
||||||
|
pm: null,
|
||||||
|
}));
|
||||||
|
await BatchSavePosMasterHistoryOfficer(queryRunner, historyOps);
|
||||||
|
|
||||||
|
// Now clear current_holderId
|
||||||
await queryRunner.manager.update(
|
await queryRunner.manager.update(
|
||||||
PosMaster,
|
PosMaster,
|
||||||
{
|
{
|
||||||
|
|
@ -8112,11 +8130,12 @@ export class OrganizationController extends Controller {
|
||||||
// Then delete posMaster records
|
// Then delete posMaster records
|
||||||
await queryRunner.manager.delete(PosMaster, toDeleteIds);
|
await queryRunner.manager.delete(PosMaster, toDeleteIds);
|
||||||
|
|
||||||
await Promise.all(
|
const deleteHistoryOps = toDelete.map((pos) => ({
|
||||||
toDelete.map(async (pos) => {
|
posMasterDnaId: pos.ancestorDNA,
|
||||||
await SavePosMasterHistoryOfficer(queryRunner, pos.ancestorDNA, null, null);
|
profileId: null,
|
||||||
}),
|
pm: null,
|
||||||
);
|
}));
|
||||||
|
await BatchSavePosMasterHistoryOfficer(queryRunner, deleteHistoryOps);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2.4 Process draft positions (UPDATE or INSERT)
|
// 2.4 Process draft positions (UPDATE or INSERT)
|
||||||
|
|
@ -8705,17 +8724,18 @@ export class OrganizationController extends Controller {
|
||||||
// Bulk DELETE
|
// Bulk DELETE
|
||||||
if (allToDelete.length > 0) {
|
if (allToDelete.length > 0) {
|
||||||
await queryRunner.manager.delete(Position, allToDelete);
|
await queryRunner.manager.delete(Position, allToDelete);
|
||||||
await Promise.all(
|
const deleteOps = allToDeleteHistory.map((ancestorDNA) => ({
|
||||||
allToDeleteHistory.map(async (ancestorDNA) => {
|
posMasterDnaId: ancestorDNA,
|
||||||
await SavePosMasterHistoryOfficer(queryRunner, ancestorDNA, null, null);
|
profileId: null,
|
||||||
}),
|
pm: null,
|
||||||
);
|
}));
|
||||||
|
await BatchSavePosMasterHistoryOfficer(queryRunner, deleteOps);
|
||||||
deletedCount = allToDelete.length;
|
deletedCount = allToDelete.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bulk UPDATE (batch by 500 to avoid query size limits)
|
// Bulk UPDATE (batch by 100 to avoid query size limits)
|
||||||
if (allToUpdate.length > 0) {
|
if (allToUpdate.length > 0) {
|
||||||
const batchSize = 500;
|
const batchSize = 100;
|
||||||
for (let i = 0; i < allToUpdate.length; i += batchSize) {
|
for (let i = 0; i < allToUpdate.length; i += batchSize) {
|
||||||
const batch = allToUpdate.slice(i, i + batchSize);
|
const batch = allToUpdate.slice(i, i + batchSize);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
|
@ -8727,7 +8747,7 @@ export class OrganizationController extends Controller {
|
||||||
|
|
||||||
// Bulk INSERT
|
// Bulk INSERT
|
||||||
if (allToInsert.length > 0) {
|
if (allToInsert.length > 0) {
|
||||||
const batchSize = 500;
|
const batchSize = 100;
|
||||||
for (let i = 0; i < allToInsert.length; i += batchSize) {
|
for (let i = 0; i < allToInsert.length; i += batchSize) {
|
||||||
const batch = allToInsert.slice(i, i + batchSize);
|
const batch = allToInsert.slice(i, i + batchSize);
|
||||||
await queryRunner.manager.save(Position, batch);
|
await queryRunner.manager.save(Position, batch);
|
||||||
|
|
@ -8747,10 +8767,13 @@ export class OrganizationController extends Controller {
|
||||||
|
|
||||||
// Save PosMasterHistory for updated positions
|
// Save PosMasterHistory for updated positions
|
||||||
if (historyCalls.length > 0) {
|
if (historyCalls.length > 0) {
|
||||||
await Promise.all(
|
await BatchSavePosMasterHistoryOfficer(
|
||||||
historyCalls.map(({ ancestorDNA, profileId, historyData }) =>
|
queryRunner,
|
||||||
SavePosMasterHistoryOfficer(queryRunner, ancestorDNA, profileId, historyData),
|
historyCalls.map(({ ancestorDNA, profileId, historyData }) => ({
|
||||||
),
|
posMasterDnaId: ancestorDNA,
|
||||||
|
profileId,
|
||||||
|
pm: historyData,
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
62
src/migration/1770875727560-add_indexes_for_performance.ts
Normal file
62
src/migration/1770875727560-add_indexes_for_performance.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddIndexesForPerformance1770875727560 implements MigrationInterface {
|
||||||
|
name = "AddIndexesForPerformance1770875727560";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// Index for posMasterHistory lookups
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX IDX_posMasterHistory_ancestorDNA
|
||||||
|
ON posMasterHistory(ancestorDNA, createdAt DESC)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Index for org tables lookups
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX IDX_orgRoot_ancestorDNA_revision
|
||||||
|
ON orgRoot(ancestorDNA, orgRevisionId)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX IDX_orgChild1_ancestorDNA_revision
|
||||||
|
ON orgChild1(ancestorDNA, orgRevisionId)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX IDX_orgChild2_ancestorDNA_revision
|
||||||
|
ON orgChild2(ancestorDNA, orgRevisionId)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX IDX_orgChild3_ancestorDNA_revision
|
||||||
|
ON orgChild3(ancestorDNA, orgRevisionId)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX IDX_orgChild4_ancestorDNA_revision
|
||||||
|
ON orgChild4(ancestorDNA, orgRevisionId)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Index for posMaster lookups
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX IDX_posMaster_revision_org
|
||||||
|
ON posMaster(orgRevisionId, orgRootId, orgChild1Id, orgChild2Id, orgChild3Id, orgChild4Id)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE INDEX IDX_posMaster_ancestorDNA
|
||||||
|
ON posMaster(ancestorDNA)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX IDX_position_posMasterId ON position`);
|
||||||
|
await queryRunner.query(`DROP INDEX IDX_posMaster_ancestorDNA ON posMaster`);
|
||||||
|
await queryRunner.query(`DROP INDEX IDX_posMaster_revision_org ON posMaster`);
|
||||||
|
await queryRunner.query(`DROP INDEX IDX_orgChild4_ancestorDNA_revision ON orgChild4`);
|
||||||
|
await queryRunner.query(`DROP INDEX IDX_orgChild3_ancestorDNA_revision ON orgChild3`);
|
||||||
|
await queryRunner.query(`DROP INDEX IDX_orgChild2_ancestorDNA_revision ON orgChild2`);
|
||||||
|
await queryRunner.query(`DROP INDEX IDX_orgChild1_ancestorDNA_revision ON orgChild1`);
|
||||||
|
await queryRunner.query(`DROP INDEX IDX_orgRoot_ancestorDNA_revision ON orgRoot`);
|
||||||
|
await queryRunner.query(`DROP INDEX IDX_posMasterHistory_ancestorDNA ON posMasterHistory`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { In } from "typeorm";
|
||||||
import { SavePosMasterHistory } from "./../interfaces/OrgMapping";
|
import { SavePosMasterHistory } from "./../interfaces/OrgMapping";
|
||||||
import { AppDataSource } from "../database/data-source";
|
import { AppDataSource } from "../database/data-source";
|
||||||
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
|
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
|
||||||
|
|
@ -320,3 +321,85 @@ export async function SavePosMasterHistoryOfficer(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BatchPosMasterHistoryOperation {
|
||||||
|
posMasterDnaId: string;
|
||||||
|
profileId: string | null;
|
||||||
|
pm: SavePosMasterHistory | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function BatchSavePosMasterHistoryOfficer(
|
||||||
|
queryRunner: any,
|
||||||
|
operations: BatchPosMasterHistoryOperation[],
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (operations.length === 0) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const repoPosMasterHistory = queryRunner.manager.getRepository(PosMasterHistory);
|
||||||
|
const dnaIds = operations.map((op) => op.posMasterDnaId);
|
||||||
|
|
||||||
|
// Fetch all existing history records in ONE query
|
||||||
|
const existingHistory = await repoPosMasterHistory.find({
|
||||||
|
where: { ancestorDNA: In(dnaIds) },
|
||||||
|
order: { createdAt: "DESC" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build lookup map
|
||||||
|
const historyByDna = new Map<string, PosMasterHistory[]>();
|
||||||
|
for (const h of existingHistory) {
|
||||||
|
if (!historyByDna.has(h.ancestorDNA)) {
|
||||||
|
historyByDna.set(h.ancestorDNA, []);
|
||||||
|
}
|
||||||
|
historyByDna.get(h.ancestorDNA)!.push(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process operations and collect new records
|
||||||
|
const newRecords: PosMasterHistory[] = [];
|
||||||
|
const _null: any = null;
|
||||||
|
|
||||||
|
for (const op of operations) {
|
||||||
|
const existing = historyByDna.get(op.posMasterDnaId)?.[0];
|
||||||
|
const shouldInsert = !existing && op.profileId && op.pm;
|
||||||
|
const profileChanged = existing && existing.profileId !== op.profileId;
|
||||||
|
|
||||||
|
if (shouldInsert || profileChanged) {
|
||||||
|
const newPmh = new PosMasterHistory();
|
||||||
|
newPmh.ancestorDNA = op.posMasterDnaId;
|
||||||
|
newPmh.prefix = op.pm?.prefix ?? _null;
|
||||||
|
newPmh.firstName = op.pm?.firstName ?? _null;
|
||||||
|
newPmh.lastName = op.pm?.lastName ?? _null;
|
||||||
|
newPmh.position = op.pm?.position ?? _null;
|
||||||
|
newPmh.posType = op.pm?.posType ?? _null;
|
||||||
|
newPmh.posLevel = op.pm?.posLevel ?? _null;
|
||||||
|
newPmh.posExecutive = op.pm?.posExecutive ?? _null;
|
||||||
|
newPmh.profileId = op.profileId ?? _null;
|
||||||
|
newPmh.rootDnaId = op.pm?.rootDnaId ?? _null;
|
||||||
|
newPmh.child1DnaId = op.pm?.child1DnaId ?? _null;
|
||||||
|
newPmh.child2DnaId = op.pm?.child2DnaId ?? _null;
|
||||||
|
newPmh.child3DnaId = op.pm?.child3DnaId ?? _null;
|
||||||
|
newPmh.child4DnaId = op.pm?.child4DnaId ?? _null;
|
||||||
|
newPmh.shortName = op.pm?.shortName ?? _null;
|
||||||
|
newPmh.posMasterNoPrefix = op.pm?.posMasterNoPrefix ?? _null;
|
||||||
|
newPmh.posMasterNo = op.pm?.posMasterNo ?? _null;
|
||||||
|
newPmh.posMasterNoSuffix = op.pm?.posMasterNoSuffix ?? _null;
|
||||||
|
newPmh.createdUserId = "system";
|
||||||
|
newPmh.createdFullName = "system";
|
||||||
|
newPmh.lastUpdateUserId = "system";
|
||||||
|
newPmh.lastUpdateFullName = "system";
|
||||||
|
newPmh.createdAt = new Date();
|
||||||
|
newPmh.lastUpdatedAt = new Date();
|
||||||
|
newRecords.push(newPmh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch insert all new records
|
||||||
|
if (newRecords.length > 0) {
|
||||||
|
await queryRunner.manager.save(PosMasterHistory, newRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("BatchSavePosMasterHistoryOfficer error:", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue