TypeORM 엔티티 업데이트 시 발생한 id 타입 불일치 문제 해결

Oneik·2024년 7월 29일
0
post-thumbnail

문제 상황

프로젝트 진행 중 메인 퀘스트 업데이트 시, 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_idnull로 설정되는 문제가 발생했다

해결 방법

수정하고자 하는 퀘스트와 연관된 사이드 퀘스트들을 각각 별도로 업데이트하고, 하나의 트랜잭션으로 묶어줬다

// 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);
    }
  }

여기서 중요한 점은 퀘스트에 대한 데이터를 가져올 때, relationseager옵션을 통해 연관된 사이드 퀘스트들을 함께 가져온다면, 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);
  }
}

참고

TypeORM 공식 문서

profile
초보 개발자의 블로그입니다

0개의 댓글

관련 채용 정보