Search
🙄

typeorm 0.3 + mongodb find 쿼리 where절이 안먹히는 오류 해결

subtitle
킹받는다 진짜
Tags
오픈소스
트러블슈팅
Created
2022/08/06
2 more properties

TL;DR

아래와 같은 상황이라면 해당 이슈일 가능성이 크다
typeorm@^0.3.0 - 0.3.7 버전
mongodb 사용
find 쿼리를 사용하는 entity에 DeletedDateColumn이 있다.
해결 방법
typeorm - 0.2.41 버전으로 내린다
또는 (typeorm ^0.3.0 버전을 굳이굳이 사용해야 한다면) 임시 방편으로 findXXX 메서드에 withDeleted: true 옵션을 주면 해당 버그를 피할 수 있다. 하지만 삭제된 데이터까지 모두 불러올 수 있다는 점에 주의하자…
어떤 게 원인이었는지 궁금하면 계속 읽어보시길

Typeorm: mongodb 버그 PR (2022.08)

회사에서 typeorm@0.2.41 버전을 사용하다가 peer dependency를 맞추기 위해 ^0.3.0버전으로 올렸다. db는 mongodb를 사용하고 있었는데 나중에 데이터를 확인해보니 쿼리가 엉뚱하게 들어가고 있었다. 분명 typeorm findXXX 메서드의 인터페이스에 맞게 넣어줬는데 where절이 왜 안먹히는 걸까 고민했다. nestjs와 typeorm 문서에 있는 샘플 코드를 보아도 똑같이 따라해보았는데 데이터는 이상하게 나왔다.
컴파일 된 typeorm 코드에서 직접 콘솔을 찍어가며 확인해보았다.
typeorm에서 mongodb는 다른 RDB와는 달리 MongoRepository를 사용하고, MongoRepository에서는 MongoEntityManager를 사용해 데이터를 query 한다.
/** * Finds entities that match given find options or conditions. */ find(options?: MongoFindManyOptions<Entity>): Promise<Entity[]> { return this.manager.find(this.metadata.target, options) }
TypeScript
엔티티에 DeletedDateColumn이 있으면 filterSoftDeleted 메서드를 타는 분기가 있다.
if (FindOptionsUtils.isFindOneOptions(findOneOptionsOrConditions)) { // ...생략 if (deleteDateColumn && !findOneOptionsOrConditions.withDeleted) { this.filterSoftDeleted(cursor, deleteDateColumn) } } else if (deleteDateColumn) { this.filterSoftDeleted(cursor, deleteDateColumn) }
TypeScript
이 분기 이전에 cursor 데이터를 찍어보면 파라미터로 들어온 where절에 맞게 데이터를 가져온다.
이 분기 이후에 다시 데이터를 찍어보면 DeletedDateColumn이 null인, 그러니까 삭제되지 않은 데이터를 모두 가져온다.
문제가 있었던 filterSoftDeleted의 코드를 보면 cursor.filter를 사용하고 있다.
protected filterSoftDeleted<Entity>( cursor: Cursor<Entity>, deleteDateColumn: ColumnMetadata, ) { cursor.filter({ $where: `this.${deleteDateColumn.propertyName}==null` }) }
JavaScript
cursor는 node-mongodb-native 코드를 보면 확인할 수 있는데, cursor의 filter 메서드는 기존 쿼리 필터에 새로운 필터를 추가하는 게 아니라, 새로운 필터로 기존 필터를 덮어쓴다.
/** Set the cursor query */ filter(filter: Document): this { assertUninitialized(this); this[kFilter] = filter; return this; }
JavaScript
filter 메서드에 DeletedDateColumn가 null인 필터를 파라미터로 전달해 해당 필터만 데이터에 적용이 되었고 따라서 삭제되지 않은 데이터만 전부 가져온 것이다.
임시방편으로 findXXXBy 메서드 대신 findXXX 메서드를 사용하고 withDeleted: true 옵션을 추가하면 해당 버그를 피할 수 있지만, 이는 삭제된 데이터를 전부가져온다는 점에 주의해야 한다.
해당 PR에서는 filterSoftDeleted에 query 파라미터를 추가하여, find 메서드를 통해 전달된 쿼리 필터가 함께 적용되도록 변경했다.
protected filterSoftDeleted<Entity>( cursor: Cursor<Entity>, deleteDateColumn: ColumnMetadata, query?: ObjectLiteral, // find메서드를 통해 전달되는 쿼리 ) { // query.$or 절이 덮어씌워지지 않도록 따로 분리한다. const { $or, ...restQuery } = query ?? {} cursor.filter({ $or: [ { [deleteDateColumn.propertyName]: { $eq: null } }, ...(Array.isArray($or) ? $or : []), ], ...restQuery, }) }
JavaScript
그리고 해당 코드의 테스트를 위해 DeletedDateColumn이 붙은 entity를 추가하여 find 메서드 쿼리 테스트를 작성했다.
typeorm 테스트를 할 때는 변경 사항이 생길 때마다 전체 코드를 빌드한 후 테스트를 돌려야 한다. 만약 변경사항이 자주 발생한다면 그냥 컴파일된 코드에서 바로바로 고쳐서 npm run test-fast로 실행하는 게 빠르다. 그리고 테스트가 너무 많으니 describe.only나 it.only를 사용해 특정 테스트만 실행하자. (grep 옵션으로도 테스트 필터링이 가능하다) → typeorm CONTIBUTING.md 참고
mongodb는 repository나 entity manager 코드가 따로 작성되어있어서 RDB (Mysql, postgresql, …) 코드를 전부 테스트하지 않아도 되서 편했던 것 같다. 스택오버플로우나 다른 블로그들을 보니 기존에 많은 사람들이 겪었던 문제 같은데 얼른 머지되서 해결 되었으면 좋겠다.