일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- sequelize
- 리버싱
- node.js
- 스프링 배치
- 보안
- 사이버보안
- Baekjoon
- docker
- programmers
- 회고
- gcp ci/cd
- cloud run
- Batch
- gcp
- 네트워크
- 시스템 해킹
- nodejs
- 백준
- hackctf
- 프로그래머스
- spring Batch
- gcp cloud build
- Python
- webhacking.kr
- 포너블
- 웹보안
- 웹해킹
- 파이썬
- kotest
- pwnable.xyz
uju's Tech
여러 개의 row가 unique인 칼럼이 변경된 후 save 할 경우 주의해야 하는 점 본문
이슈
TypeORM 에서 여러 개의 row가 업데이트 되거나 생성될 때 save 메서드를 사용할 수 있다.
변경되는 여러 개의 row에서 unique한 칼럼이 업데이트되거나 생성되는 경우도 있을 것이다.
여기서 unique constraint violate 에러가 발생할 수 있다.
이 에러는 어떤 경우에, 왜 발생하는 것일까? 그리고 어떻게 해결할 수 있을까?
예제
예를 들어서 설명해보겠다.
Post가 있고 Post를 저장할 때 Label을 지정할 수 있다고 하자. 그리고 Label이라는 테이블은 id, name, post_id로 구성되어 있으며 name에는 unique constraint가 걸려있다고 하자.
현재 하나의 Post가 저장되어 있고 그에 대한 Label은 다음과 같이 저장되어 있다.
Post를 저장할 때 Label도 함께 저장되는데 저장 로직은 다음과 같다.
1. Request에 id가 존재하는 Label의 경우 update.
2. Reqeust에 id가 없는 Label의 경우 insert.
요청
다음과 같이 요청이 들어왔다고 해보자.
비즈니스 로직
id 가 1이었던 name 을 '안녕'으로 , id 2는 그대로, 새로운 name을 기존에 id 1의 name과 동일한 '우주' 로 추가하자고 요청이 왔다.
그리고 아래와 같은 로직을 가지고 저장을 하고있다.
1. 이미 존재하는 라벨을 뽑아 요청온 라벨 목록과 비교하여 존재하지 않으면 삭제한다.
2. 삭제 후 업데이트 혹은 생성이므로 이를 모두 처리할 수 있는 save를 사용하여 처리한다.
에러와 원인
하지만 우리의 기대와 다르게 duplicate key value violates unique constraint ... 에러가 발생함을 확인할 수 있다.
id가 1인 Label의 name을 '우주'로 변경하고 새로운 name을 추가하는 시점에 아직 데이터베이스에 저장되어 있는 id가 1인 Label의 name은 여전히 '우주'이기 때문이다.
TypeORM save의 동작
save가 내부적으로 어떻게 동작하는지 살펴보자. save 메서드 내부에서는 identifier의 존재 여부에 따라 insert를 할지, update를 할지, 아무런 동작도 하지 않을지 결정한다.
EntityManager의 save를 호출할 때 아래와 같은 로직을 걸친다. 주목해야 할 부분은 빨간 네모 박스의 EntityPersistExecutor다. 이름에서 알 수 있듯이 엔티티를 데이터베이스에 영속화하는 로직으로 보인다.
그럼 실행하고 있는 EntityPersistExecutor를 살펴보자. 굉장히 복잡하고 긴 파일을 마주할 수 있다.
여기서 살펴봐야할 부분만 빠르게 살펴보자. subjects 배열에 엔티티, metadata 등 여러가지 정보를 담는 것을 볼 수 있다.
여기서의 subject는 영속성을 관리할 대상이다.
그리고 이 subjects를 인수로 받아 뭔가 실행하는 SubjectExecutor를 볼 수 있다.
(console이 왜이렇게 많은지는...모르겠다ㅣ..)
주어진 subjects에 대해 SubjectExecutor에서 데이터베이스 연산을 수행한다. 여기도 굉장히 길고 복잡한 코드로 구성되어있지만 딱 필요한 부분만 살펴보자.
보이는 것과 같이 insert, update, remove, softRemove, recover 를 각각의 리스트에 담아 수행한다.
위 코드에서 가장 먼저 insert를 수행하는 것을 확인할 수 있다. save를 호출할 때 우리는 엔티티의 순서대로 동작할 것이라고 생각할 수 있지만 실제로는 insert 가 필요한 엔티티들 부터 수행되고 그 이후에 identifier 가 있는 엔티티들이 update 가 수행된다.
결론
예시를 보았을 때는 update 하고 insert 하면 되는거아니야? 라고 생각할 수도 있다.
두 가지 이유로 사용하지 못하는 방법인데,
첫 번째, 다음과 같은 케이스에서는 동일한 에러가 발생할 것 이다. 새로운 값이 추가되는 것이 아닌 id 1의 name을 블로그로 id 2의 name을 우주로 변경하면 모두 update가 발생하기 때문에 동일한 에러가 발생할 것이다.
두 번째, TypeORM 의 save는 항상 insert부터 수행된다.
그럼 우리가 고려할 수 있는 방법은 다음과 같다.
첫 번째, 수행 전 대상 엔티티들을 모두 삭제 후 수행한다.
두 번째, save 실행 전 업데이트 되어야하는 row들의 name을 모두 null로 셋팅한다.
첫 번째 방법으로 하면 변경사항이 없는데 저장 요청이 들어올 때 마다 데이터를 삭제하고 다시 쓰는 과정이 반복될 것이다. 그러면 매번 데이터베이스 삭제 연산이 수행되어 id가 빠르게 소비될 것이다.
두 번째 방법으로 진행하면 label 칼럼이 string 타입이고 nullable true로 설정되어있어 name 을 null로 할당할 때 타입 단언을 사용하게 된다. 나의 경우에는 두 번째 방법으로 진행했다.
끝 -
'Node' 카테고리의 다른 글
TypeORM Replication Mode 지정 (1) | 2023.02.11 |
---|---|
Alpine Linux, Debian, CentOS Linux cp 차이를 통한 리눅스 삽질 과정 (2) | 2022.05.18 |
[PostgreSQL] 데이터가 존재할 때만 INSERT (2) | 2022.03.24 |
[Sequelize: 기록용]sequelize migration 시 ERROR: Cannot read property 'toString' of undefined 에러 (0) | 2021.07.30 |
[Node:기록용] Date 를 '월-일-요일' 포맷팅 (0) | 2021.07.16 |