프로젝트 진행 중 메인 퀘스트 업데이트 시, 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);
}
}