JavaScript

TypeORM의 save() 사용 시 데이터 변경 감지 원리 이해하기

MIRACLE LIFE 2024. 12. 15. 22:59

TypeORM의 Repository API를 사용하여 데이터를 변경하는 방법에는 update()save()등이 있다.

 

update()

update - Partially updates entity by a given update options or entity id.

await repository.update({ age: 18 }, { category: "ADULT" })
// executes UPDATE user SET category = ADULT WHERE age = 18

await repository.update(1, { firstName: "Rizzrak" })
// executes UPDATE user SET firstName = Rizzrak WHERE id = 1

 

save()

save - Saves a given entity or array of entities. If the entity already exist in the database, it is updated. If the entity does not exist in the database, it is inserted. It saves all given entities in a single transaction (in the case of entity, manager is not transactional). Also supports partial updating since all undefined properties are skipped. Returns the saved entity/entities.

await repository.save(user)
await repository.save([category1, category2, category3])

 

출처: https://typeorm.io/repository-api

 


 

이때 save()를 사용하여 연관 관계가 있는 외래 키 컬럼의 데이터를 변경할 때 주의해야 할 점이 있다.

예제를 통해 더 자세히 알아보자.

@Entity()
export class User {
	@PrimaryGeneratedColumn()
	id: number

	@Column()
	name: string
    
	@Column({ name: "team_id" })
	teamId: string;

	@ManyToOne(() => Team, (team) => team.users)
	@JoinColumn({ name: "team_id" })
	team: Team; // 유저는 하나의 팀에만 속할 수 있다.
}

 

유저 데이터를 다룰 때, 팀의 전체 정보가 아닌 팀 ID만 필요할 때가 있다. 이러한 경우, 위 엔티티 코드와 같이 team과는 별도로 teamId를 추가로 정의하여 사용하곤 한다.

 

 

이런 상황에서, 유저의 속성을 변경하기 위해 다음과 같은 코드를 작성할 수 있다.

user.name = req.newName; // 이름 변경
user.teamId = req.newTeamId; // 팀 변경

await repository.save(user);

이제 코드를 실행해보면 name은 데이터베이스에 정상적으로 반영되지만, teamId는 변경되지 않은 것을 확인할 수 있다.

 

코드만 봐서는 name이 변경된 것처럼 teamId도 변경되어야 할 것 같은데 그렇지 않은 이유가 뭘까?

 

내부 라이브러리 코드 파악

save()의 코드 내부 동작을 더 살펴보자.

save(entityOrEntities, options) {
	return this.manager.save(this.metadata.target, entityOrEntities, options);
}

 

 

compute()

class SubjectChangedColumnsComputer {
	// -------------------------------------------------------------------------
	// Public Methods
	// -------------------------------------------------------------------------
	/**
	* Finds what columns are changed in the subject entities.
	*/
	compute(subjects) {
		subjects.forEach((subject) => {
		this.computeDiffColumns(subject);
		this.computeDiffRelationalColumns(subjects, subject);
	});
}

save() 호출 스택을 계속 따라가다 보면, 위 코드에서 보이는 compute 메서드가 실행되는 것을 알 수 있다.

 

이 메서드는 설명에서 알 수 있듯이 변경된 컬럼을 찾는 역할을 한다.

 

다음으로, compute()가 호출하는 computeDiffColumns()computeDiffRelationalColumns()의 코드를 살펴 보자.

subject.diffColumns.push(column); // computeDiffColumns() 내부

subject.diffRelations.push(relation); // computeDiffRelationalColumns() 내부

코드를 보면, 변경된 컬럼이 있는 경우 diffColumnsdiffRelations에 저장되고, 이후 업데이트가 처리된다는 것을 알 수 있다. 이제 변경 여부가 어떻게 결정되는지에 대한 기준만 알게 되면 궁금증이 풀릴 것이다.

 

subject 객체

우선, computeDiffColumns()computeDiffRelationalColumns()에서 사용하는 subject에 대해 알아보자.

두 메서드 모두 subject 내부의 databaseEntityentity를 비교하여 다를 경우, diffColumnsdiffRelations에 추가한다.

각 값은 다음과 같다

  • databaseEntity: 데이터베이스에서 현재 저장된 실제 엔티티의 상태
  • entity: 변경된 엔티티, 즉 사용자 코드에서 수정된 엔티티의 상태 (await repository.save(user)user)

현재 entity의 상태는 다음과 같다.

{
	...
	name: "변경한 이름";
	team_id: "변경한 teamId";
	team: { id: "기존 teamId" };
}

 

 

computeDiffColumns()

computeDiffColumns()는 컬럼들을 순회하면서 subjectdatabaseEntityentity 간 값이 다른 컬럼을 찾는다. 이 과정에서, 반복문 내의 다음 코드를 살펴보자.

if (column.relationMetadata) {
	const value = column.relationMetadata.getEntityValue(subject.entity);
	if (value !== null && value !== undefined)
		return;
}

 

이 코드는 column.relationMetadata에 값이 존재하면 코드 실행을 중단하고, 다음 컬럼의 비교로 넘어간다.

즉, team_id 컬럼의 경우 databaseEntityentity의 값이 다르더라도 relationMetadata가 존재하기 때문에 diffColumns에 추가되지 않는다. 반면, name과 같은 일반 컬럼은 끝까지 코드가 실행되어 diffColumns에 포함된다.

 

(엔티티 코드에서 연관 관계가 설정된 경우, 메타데이터가 생성될 때 relationMetadata에 값이 할당된다.)

 

 

computeDiffRelationalColumns()

computeDiffRelationalColumns()는 일반 필드가 아닌 team과 같은 연관 관계 필드만 비교한다.

하지만 현재 entity(User 객체)에서는 team_id만 변경되었을 뿐, team 객체는 변경되지 않았기 때문에 아무 작업도 수행하지 않으며, diffRelations에도 추가되지 않는다.

이러한 이유로 team_id는 기대와 달리 데이터베이스에서 업데이트되지 않은 것이다.

 

여기서 전체 코드를 확인할 수 있다.

https://github.com/typeorm/typeorm/blob/master/src/persistence/SubjectChangedColumnsComputer.ts

 

올바른 코드

따라서, 실제 DB에서 유저의 team_id가 정상적으로 변경되도록 하려면 다음과 같이 코드를 작성해야 한다.

user.team.id = req.newTeamId;
// 또는
user.team = newTeam;

await repository.save(user);

 

 

마무리.

TypeORM에서 save()를 사용할 때 주의해야 할 점과, 업데이트 시 변경 사항을 감지하는 원리에 대해 이해하게 되었다.