
프로젝트 진행 중 메인 퀘스트 업데이트 시, 1:N 관계로 설정된 사이드 퀘스트도 함께 업데이트해야만 한다
UPDATE `quest` SET `end_date` = ?, `updated_at` = CURRENT_TIMESTAMP WHERE (`id` = ?) -- PARAMETERS: ["2024-08-24T14:59:59.999Z","3"]
UPDATE `side_quest` SET `quest_id` = ?, `content` = ?, `updated_at` = CURRENT_TIMESTAMP WHERE (`id` = ?) -- PARAMETERS: [3,"사이드 퀘스트 1 수정7","5"]
UPDATE `side_quest` SET `quest_id` = ?, `content` = ?, `updated_at` = CURRENT_TIMESTAMP WHERE (`id` = ?) -- PARAMETERS: [3,"사이드 퀘스트 2 수정7","6"]
UPDATE `side_quest` SET `quest_id` = ?, `updated_at` = CURRENT_TIMESTAMP WHERE `id` = ? -- PARAMETERS: [null,"5"]
UPDATE `side_quest` SET `quest_id` = ?, `updated_at` = CURRENT_TIMESTAMP WHERE `id` = ? -- PARAMETERS: [null,"6"]
그러나 위와 같은 SQL문을 통해 사이드 퀘스트에서 quest_id값이 null로 업데이트되는 문제가 발생했다
디버깅을 통해 원인을 파악한 결과, id값의 타입을 bigint로 설정한 것이 문제였다. JavaScript에서 정확하게 표현할 수 있는 가장 큰 정수는 2^53 - 1이다
TypeORM 문서에 따르면,
Note about bigint type: bigint column type, used in SQL databases, doesn't fit into the regular number type and maps property to a string instead.
위처럼 bigint 타입을 string으로 매핑한다
그래서 데이터베이스 설정 파일에 bigNumberStrings: false 옵션을 추가하여 bigint 값을 number로 가져오도록 설정하였다.
문제는 TypeORM 라이브러리의 EntityPersistExecutor 클래스에서 발생했다
EntityPersistExecutor 클래스는 전달받은 엔티티를 데이터베이스에 저장하거나 업데이트하는 역할을 한다
// EntityPersistExecutor.js
...
await new SubjectDatabaseEntityLoader_1.SubjectDatabaseEntityLoader(queryRunner, subjects).load(this.mode);
// console.timeEnd("loading...");
// console.time("other subjects...");
// build all related subjects and change maps
if (this.mode === "save" ||
this.mode === "soft-remove" ||
this.mode === "recover") {
new OneToManySubjectBuilder_1.OneToManySubjectBuilder(subjects).build();
new OneToOneInverseSideSubjectBuilder_1.OneToOneInverseSideSubjectBuilder(subjects).build();
new ManyToManySubjectBuilder_1.ManyToManySubjectBuilder(subjects).build();
}
그 중,SubjectDatabaseEntityLoader.load() 메서드를 통해 전달받은 subjects(퀘스트 엔티티와 사이드 퀘스트들)를 통해 데이터베이스에서 엔티티 데이터들을 로드해온다.
그러나, 여기서 퀘스트와 연관 설정된 사이드 퀘스트들을 로드해올 때, bigNumberStrings옵션이 적용되지 않아 사이드 퀘스트의 id값이 string타입으로 가져와졌다
그 후, OneToManySubjectBuilder.build()메서드에서 엔티티 간의 관계를 설정할 때, 업데이트를 위해 전달받은 엔티티와 데이터베이스에서 로드해온 엔티티 간의 관계를 비교한다
이 때 id값의 타입 불일치로 인해 비교 결과가 false가 되어 새로운 subject가 생성되었다
결과적으로 불필요한 update로직이 추가로 발생하게 되어, quest_id가 null로 설정되는 문제가 발생했다
수정하고자 하는 퀘스트와 연관된 사이드 퀘스트들을 각각 별도로 업데이트하고, 하나의 트랜잭션으로 묶어줬다
// quest.service.ts
@Transactional()
async updateMainQuest(
userId: number,
questId: number,
request: UpdateMainQuestRequest
): Promise<void> {
try {
const { title, difficulty, startDate, endDate, hidden, sideQuests } = request;
const quest = await this.findById(userId, questId);
quest.updateMainQuest(title, difficulty, hidden, startDate, endDate);
// 퀘스트와 연관된 사이드 퀘스트 수정 후, 저장하는 로직
await this.sideQuestService.updateSideQuests(questId, sideQuests);
await this.questRepository.save(quest);
} catch (error) {
throw new HttpException('퀘스트 업데이트에 실패하였습니다', HttpStatus.CONFLICT);
}
}
여기서 중요한 점은 퀘스트에 대한 데이터를 가져올 때, relations나 eager옵션을 통해 연관된 사이드 퀘스트들을 함께 가져온다면, OneToManySubjectBuilder에서 동일한 오류가 발생하므로, 퀘스트의 데이터만 가져오도록 설정해야한다
// quest.repository.ts
export class QuestRepository extends GenericTypeOrmRepository<Quest> implements IQuestRepository {
...
async findById(userId: number, questId: number): Promise<Quest> {
const findOptions: FindOneOptions = { where: { id: questId, userId } };
return this.getRepository().findOne(findOptions);
}
}