-
Notifications
You must be signed in to change notification settings - Fork 1
MongoDB 다중 Sub Document 제어
Sub Document는 하나의 Document 내부에 존재하는 Embedded Document이다. Mongoose의 경우 Schema를 다른 Schema에 중첩하는 것과 동일하다.
Mongoose는 이러한 Embedded Document를 표현하기 위해 Array를 이용하여 다수의 Sub Document를 담을 수도 있고, 단 하나의 Sub Document를 하나의 field에 지정할 수도 있다.
const childSchema = new Schema({ name: 'string' });
const parentSchema = new Schema({
// Array of subdocuments
children: [childSchema],
// Single nested subdocuments
child: childSchema
});
주의할 점은 Mongoose에서 제공하는 populate
는 Sub Document를 표현하기 위한 수단이 아니며, 단순히 하나의 Document에 다른 Document를 참조할 수 있는 값을 남기는 것임에 유의하자. Sub Document의 차별점은 Top-level Document에 자기 자신을 포함시키는 것이다. 이에 반해 populate
는 Top-level Document에 자신의 참조값을 남길 뿐이다.
또 하나 유념할 점은, Mongoose는 save
메서드를 이용하는 것이 Mongoose의 기능을 온전히 사용하는 것이라고 주장하기 때문에 하단의 Query Building을 통한 Sub Document 조작이 마음에 들지 않는다면 find
로 Document를 가져와 save
를 수행해도 무방하다. (어차피 Mongoose에서 알아서 Query를 작성해준다.) 아래의 설명들은 처리를 모두 MongoDB에 전가하고자 작성한 코드이다.
Sub Document도 일반적인 Document와 비슷하며, 이들도 Middleware, Custom Validation 로직과 Virtual 등 Top-level Document가 쓸 수 있는 대부분의 기능을 사용할 수 있다. 주요한 차이점은 Sub Document는 절대로 독자적으로 저장될 수 없다는 것이며, 오로지 다른 Document의 일부로써 저장된다는 것이다.
이러한 특성으로 모든 Sub Document들 또한 save
와 validate
미들웨어를 지니며, 자신이 속한 Top-level Document의 미들웨어가 동작은 Sub Document의 미들웨어의 트리거가 되어 함께 동작하게 된다. 특히 pre('save')
의 경우 Sub Document의 것은 Top-level Document의 것보다 먼저 동작하며, 반대로 pre('validate')
는 Top-level Document의 것이 실행된 뒤에 Sub Document의 것이 나중에 동작한다.
Sub Document의 경우 각 Prop에 unique
옵션을 줄 수는 있고, 실제로 이를 위한 Index가 DB에 생성되기는 하지만, 실질적으로 이를 사용할 방법이 없다. 즉, Sub Document의 unique 옵션은 동작하지 않는다. ([참고자료](https://stackoverflow.com/questions/25914973/mongoose-unique-index-on-subdocument))
우리가 Nest를 이용하여 Mongoose를 사용할 경우 일반적으로 사용할 모든 Schema를 MongooseModule
에 추가해야만 한다. 하지만 예외적으로 Sub Document들을 위한 Schema들은 해당 모듈에 굳이 등록할 필요가 없다. 위에서 언급했듯이, Sub Document들은 Top-level Document들에게 종속적이기 때문에, 독자적인 Collection을 가질 필요도 없거니와, 이들의 관리는 Top-level Document들이 알아서 해준다.
Sub Document를 다루기 위해서는 MongoDB의 Operator들, 특히 Update Operator와 Query / Projection Operator에 대해 잘 알고 있어야 하며, Array를 이용하여 다중 Sub Document를 관리할 경우 Sub Document를 수정하는 것은 특정 Document의 Field를 Update하는 것과 동일하므로 모델의 update
계열 메서드 또한 잘 활용할 수 있어야 한다.
하단의 설명은 중첩 Array는 없다고 가정한다. 즉, Document에서 Array는 오로지 Sub Document를 관리하는 Array만 존재함을 전제한다.
Sub Document 정의하기
-
참고자료
https://stackoverflow.com/questions/62762492/nestjs-how-to-create-nested-schema-with-decorators
Sub Document들 또한 일반적인 Document들 처럼 Schema를 이용하여 정의하게 된다. 차이점은 굳이
_id
를 가질 필요가 없다는 것, 굳이 모듈에 등록할 필요가 없다는 것이다. 여기서 정의한validate
는 Top-level Document의 유효성 검증 시에 자동으로 이루어진다. 그냥 다른 스키마를 정의하듯 동일하게 추가하면 된다.
// Nested Schema
@Schema()
export class BodyApi extends Document {
@Prop({ required: true })
type: string;
@Prop()
content: string;
}
export const BodySchema = SchemaFactory.createForClass(BodyApi);
// Parent Schema
@Schema()
export class ChaptersApi extends Document {
// Array example
@Prop({ type: [BodySchema], default: [] })
body: BodyContentInterface[];
// Single example
@Prop({ type: BodySchema })
body: BodyContentInterface;
}
export const ChaptersSchema = SchemaFactory.createForClass(ChaptersApi);
위의 경우 Interface를 정의하여 Sub Document를 받을 타입을 정의해주었지만, 굳이 Interface를 정의하지 않고 직접 class를 박아넣어도 사용하는 데에는 지장이 없다.
우리는 이를 활용하여 다음과 같이 Sub Document를 정의하였다.
// Sub Document로 쓰일 것. Sub Document는 id를 쓰지 않아도 된다.
@Schema({ _id: false, collection: 'object-management' })
export class WorkspaceObject {...}
// Top level document
@Schema({ collection: 'object-management', timestamps: true })
export class ObjectBucket {
@Prop({
type: String,
unique: true,
validate: {
validator: (v) => isUUID(v),
},
})
workspaceId: string;
@Prop({
type: [WorkspaceObjectSchema],
default: [],
})
objects: WorkspaceObject[];
}
그리고 Mongoose에게 스키마를 전달할 때에는 오로지 Top-level Document의 Schema만 전달하면 되므로, 다음과 같이 사용할 수 있다.
@Module({
imports: [
MongooseModule.forFeature([
{
name: ObjectBucket.name,
schema: ObjectBucketSchema,
},
]),
],
providers: [TestService],
})
export class TestModule {}
Sub Document CREATE 하기
-
참고자료
https://stackoverflow.com/questions/62762492/nestjs-how-to-create-nested-schema-with-decorators
Sub Document를 추가하고자 이를 생성할 때, 굳이 주입받은 Model을 이용하여 객체를 생성할 필요가 없다는 것을 명심하자. 상위 Document에 추가할 때에는 그저 Schema에 정의한 바에 부합하는 객체를 만들어서 만들어주기만 하면 된다.
이렇게 만들어진 객체를 Sub-Document를 저장하는 Array에 삽입하기 위해서는 Array Update Operator 중
$addToSet
이나$push
를 이용하면 된다. 두 Operator 모두 Array에 새로운 object를 추가한다는 것은 동일하나,$addToSet
은 Array에 동일한 요소가 존재하지 않을 경우에만 새로운 요소를 추가하며,$push
는 단순히 전달받은 요소를 Array에 추가한다.하나의 Document에 새로운 Sub Document를 추가할 경우 다음과 같이 작성한다. ID 중복을 방지하기 위해 Filter 옵션에
$ne
Operator를 추가하여 있을 경우 업데이트를 하지 않도록 방지한다. (workspaceId가 unique하므로 Document를 단 1개만 탐색할 수 있지만,$ne
로 인하여 해당 ObjectId가 존재하면 Filter에서 걸러져 아무런 Document도 안걸리게 됨.)
const ret = await this.bucketModel
.findOneAndUpdate(
{
workspaceId: TOP_LEVEL_ID,
'object.objectId': { $ne: newObject.objectId }
},
{ $addToSet: { objects: newObject } },
{ runValidators: true, returnDocument: 'after' },
)
.exec();
단일 Sub Document READ 하기
-
참고자료
https://www.mongodb.com/docs/v6.0/reference/operator/projection/elemMatch/
만약 특정 Document의 특정 SubDocument를 가져오고자 할 경우, 다음과 같이 작성할 수 있다. 먼저 원하는 Sub Document를 포함하는 Top level Document를 검색하고, projection으로 원하는 문서만 가져오는 것이다.
const { objects } = await this.bucketModel.findOne(
{
workspaceId: TOP_LEVEL_ID,
},
{
objects: {
$elemMatch: { objectId: 'f38c566d-71db-4210-a303-ae34986ea090' },
},
},
{ lean: true },
);
위의 findOne
의 경우 첫 인자로 Document를 찾아가는 방법(filter)을, 두 번째 인자로 해당 Document 내에서 어떤 값만 가져올 것인지(projection)를 정의한다. 그렇기에, 첫 번째 인자에 원하는 Sub Document에 대한 조건을 집어넣고 두 번째에 어떠한 값도 넣지 않는다면 해당 요청으로 문서 전체가 반환된다는 것을 유의하자.
그래서 위의 코드의 경우 첫 인자에는 오로지 Top-level 문서에 찾아가는 방법만을 정의하였고, Projection을 통해 원하는 데이터만 뽑아오는 것이다. 위의 경우처럼 $elemMatch
를 이용해도 좋고, 혹은 $
[projection operator](https://www.mongodb.com/docs/v6.0/reference/operator/projection/positional/)를 사용해도 적절할 것이다.
다중 Sub Document READ 하기
-
참고자료
위의 단일 Sub Document 조회의 경우, Operator의 조합으로 조회가 가능했지만 다중을 조회하고자 할 경우
aggretation
을 이용하여야 한다. (현재 미구현. 연구 중)
Sub Document Update하기
-
참고자료
https://stackoverflow.com/questions/15691224/mongoose-update-values-in-array-of-objects
Sub Document Array인 경우, MongoDB의
$
Update Operator를 활용한다. Model의update
메서드를 이용하면 MongoDB 형식의 Update Query를 작성할 수 있다. 이를 이용하여 Sub Document를 수정할 수 있다. 다만 이 방식을 사용할 경우, 수정된 값을 얻어올 수 없다.
const result = await this.bucketModel
.updateOne(
{
workspaceId: '4082dc7b-480f-4fca-85e1-5b056fb158',
'objects.objectId': 'type1235',
},
{
$set: {
'objects.$.text': 'hello world',
},
},
)
.exec();
혹은 모델의 findOneAndUpdate
메서드를 이용하여 업데이트를 진행할 수 있다. 하단의 코드는 Sub Document의 업데이트를 위해 작성한 코드이다. 이 메서드를 활용할 경우 반환 결과를 얻어올 수 있다.
const updateVal = {
left: 987,
xScale: 1.5,
};
// $set에 쓰기 위해 맞는 형식으로 전환.
const newValue = Object.fromEntries(
Object.entries(updateVal).map(([key, val]) => [`objects.$.${key}`, val]),
);
const ret = await this.bucketModel.findOneAndUpdate(
{
workspaceId: TOP_LEVEL_ID,
'objects.objectId': 'f38c566d-71db-4210-a303-ae34986ea088',
},
{ $set: newValue },
{ runValidators: true, returnDocument: 'after', lean: true },
);
console.log(ret);
}
findOneAndUpdate
메서드는 filter
(1번째 인자)에 부합하는 Document들을 탐색하며, 이 중 조건에 부합하는 field들만 update
(2번째 인자)에 정의한 바에 따라 갱신을 한다. (동작에 따라 추측. 뭔가 정확하게 알려주지는 않더라.)
여기서 주의할 점은, 우리가 업데이트 하는 것은 배열이기 때문에 update
(2번째 인자)에 전달해야하는 객체의 key들은 모두 $
update operator를 사용하여 표현하여야 한다는 것이다. 이는 Update 시에 조건에 부합하는 값들의 Index라는 뜻으로 쓰이며, 따라서 update 작성 시에 모든 Key들은 <array>.$.<field>
형식으로 작성되어야 한다. 이러한 형식에 맞추고자 위의 Object.fromEntries(...)
이라는 코드가 작성된 것이다. (다만, 이 코드는 원본 Object에 담긴 key들이 Sub Document의 field와 일치한다는 것을 전제로 둔다.)
단일 Sub Document DELETE하기
Sub Document가 Array로 관리된다면 $pull
Operator를 이용하여 Array의 특정 요소를 제거할 수 있다.
const ret = await this.bucketModel.findOneAndUpdate(
{ workspaceId: TOP_LEVEL_ID },
{
$pull: {
objects: { objectId: 'f38c566d-71db-4210-a303-ae34986ea084' },
},
},
{ runValidators: true, returnDocument: 'after' },
);
데일리 스크럼
- [Week1-Day1] 팀 빌딩
- [Week1-Day2] 데일리 스크럼
- [Week1-Day3] 데일리 스크럼
- [Week1-Day4] 데일리 스크럼
- [Week1-Day5] 데일리 스크럼
- [Week2-Day1] 스프린트 계획 회의
- [Week2-Day2] 데일리 스크럼
- [Week2-Day3] 데일리 스크럼
- [Week2-Day4] 데일리 스크럼
- [Week3-Day1] 스프린트 계획 회의
- [Week3-Day2] 데일리 스크럼
- [Week3-Day3] 데일리 스크럼
- [Week3-Day4] 데일리 스크럼
- [Week4-Day1] 스프린트 계획 회의
- [Week4-Day2] 데일리 스크럼
- [Week4-Day3] 데일리 스크럼
- [Week4-Day4] 데일리 스크럼
- [Week5-Day1] 스프린트 계획 회의
- [Week5-Day2] 데일리 스크럼
- [Week5-Day3] 데일리 스크럼
- [Week5-Day4] 데일리 스크럼
- [Week6-Day1] 스프린트 계획 회의
- [Week6-Day2] 데일리 스크럼
- [Week6 Day3] 데일리 스크럼
- [Week6 Day4] 데일리 스크럼