[mongoose] subdocument 이란?
Subdocuments는 도큐멘트 안에 있는 도큐멘트
몽고 공식문서(v5.9.18)를 해석하면서 잘 모르겠는 개념을 덧붙인 글입니다 🙂
목차
- Subdocument는 무엇인가?
- Subdocument 검색하기
- Subdocument를 Array에 추가하기
- Subdocument 삭제하기
- Subdocument의 부모 array schema 만들기 (array 선언 문법의 다른 방법)
- single subdocument 만들기 (single subdocument 선언 문법의 다른 방법)
- Schema.prototype.pre() — 개인적으로 덧붙임
문서 읽으면서 처음 알게 된 내용
- 최상위 부모 document가 저장되어야 subdocument도 저장된다.
- mongoose에서 제공하는 Schema Middleware가 있다.
- schema middleware에 사용한 method명이 같다면, 부모 schema middleware가 먼저 실행되고, subdocument schema middleware가 실행된다.
parent()
과ownerDocument()
를 이용해서 subdocument의 부모 document에 접근할 수 있다.
subdocuments
는 다른 도큐멘트안에 끼워진 도큐멘트를 말합니다. 몽구스로 작업할 때 다른 스키마(Schema
)안에 스키마를 내장시키는 방식으로 subdocuments
를 만들 수 있습니다. 몽구스의 subdocuments
는 array 형태의 subdocuments
와 single nested subdocuments
가 있습니다.
var childSchema = new Schema({ name: 'string' });
var parentSchema = new Schema({
// array 형태의 subdocuments
children: [childSchema],
// Single nested subdocuments.
// 주의: single nested subdocs는 몽구스 버전 4.2.0 이상에서 사용가능합니다.
child: childSchema
});
(🤔 아직 이해를 못한 부분) 코드 재사용성 외에도 subdocuments
를 이용하는 중요한 이유 중 하나는 필드 그룹에 대한 validation(유효성 검사) 경로가 없는 경로를 만드는 것입니다. (뭔말이지? 😱😱😱)
Aside from code reuse, one important reason to use subdocuments is to create a path where there would otherwise not be one to allow for validation over a group of fields (e.g. dateRange.fromDate <= dateRange.toDate).
Subdocument는 무엇인가?
subdocument
는 일반적인 document와 비슷합니다. 중첩된 스키마는 미들웨어, 커스텀 유효성검사 로직, Mongoose virtuals, 가장 최상위 레벨의 스키마가 사용할 수 있는 기능을 갖게 됩니다. 주요 차이점은 subdocument
는 개별적으로 저장되지 않고, subdocument
의 최상위 부모 document가 저장될 때마다 subdocument
가 저장된다는 것입니다.
// 대충 부모 Document가 저장될 때만 subdocument가 저장된다는 내용
var Parent = mongoose.model('Parent', parentSchema);
var parent = new Parent({
children: [
{ name: 'Matt' },
{ name: 'Sarah' }
]
})
parent.children[0].name = 'Matthew';
/*
* `parent.children[0].save()`는 작동하지 않습니다.
* 이 구문이 미들웨어를 호출하지만 실제로 subdocument를 저장하지는 않습니다.
* 부모 document를 저장해야합니다.
*/
parent.save(callback);
subdocument
는 최상위 document처럼 저장(save)
과 유효성검사(validate)
미들웨어를 가지고 있습니다. 부모 document에서 저장(save)
을 호출하면 내부에 있던 모든subdocument
의 저장(save) 미들웨어
를 호출합니다. 유효성 검사도 동일하게 동작합니다.
childSchema.pre('save', function (next) {
if ('invalid' == this.name) {
return next(new Error('#sadpanda'));
}
next();
});
var parent = new Parent({ children: [{ name: 'invalid' }] });
parent.save(function (err) {
console.log(err.message) // #sadpanda
});
subdocument
의 pre('save')
와 pre('validate')
미들웨어는 최상위 document의 pre('save')
전 단계에서, pre('validate')
미들웨어 후에 실행됩니다. 왜냐하면 save() 전 단계에서 유효성검사를 하는 것은 몽고디비 내장 미들웨어의 한 과정이기 때문입니다.
// 아래 코드는 1부터 4까지 순서대로 console.log를 찍을거에요.
var childSchema = new mongoose.Schema({ name: 'string' });
childSchema.pre('validate', function(next) {
console.log('2');
next();
});
childSchema.pre('save', function(next) {
console.log('3');
next();
});
var parentSchema = new mongoose.Schema({
child: childSchema,
});
parentSchema.pre('validate', function(next) {
console.log('1');
next();
});
parentSchema.pre('save', function(next) {
console.log('4');
next();
});
Subdocument 검색하기
각 subdocument
들은 기본적으로 _id
를 가지고 있습니다. Mongoose document array인 경우에는 _id
값을 가지고 document를 찾기 위한 특별한 id method가 있습니다.
var doc = parent.children.id(_id);
Subdocument를 Array에 추가하기
push, unshift, addToSet 같은 MongooseArray method는 적절합 타입으로 argument를 넣을 수 있습니다.
var Parent = mongoose.model('Parent');
var parent = new Parent;
// create a comment
parent.children.push({ name: 'Liesl' });
var subdoc = parent.children[0];
console.log(subdoc) // { _id: '501d86090d371bab2c0341c5', name: 'Liesl' }
subdoc.isNew; // true
parent.save(function (err) {
if (err) return handleError(err)
console.log('Success!');
});
subdocument
는 MongooseArrays의 create method를 사용하면 array에 추가하지 않고도 만들어질 수 있습니다.
var newdoc = parent.children.create({ name: 'Aaron' });
Subdocument 삭제하기
각각의 subdocument
는 고유의 remove method를 가지고 있습니다. array subdocumnet
인 경우에는 subdocument
에서 .pull()
을 호출하는 것과 같고, single nested subdocuments
인 경우에는, remove()
는 subdocument
를 null
로 설정하는 것과 동일합니다.
// `parent.children.pull(_id)`과 같음
parent.children.id(_id).remove();// `parent.child = null`과 같음
parent.child.remove();
parent.save(function (err) {
if (err) return handleError(err);
console.log('the subdocs were removed');
});
Subdocument의 부모
때때로 subdocument
의 부모를 찾아야할 때는 parent()
함수를 이용해서 부모 document에 접근할 수 있습니다.
const schema = new Schema({
docArr: [{ name: String }],
singleNested: new Schema({ name: String })
});
const Model = mongoose.model('Test', schema);
const doc = new Model({
docArr: [{ name: 'foo' }],
singleNested: { name: 'bar' }
});
doc.singleNested.parent() === doc; // true
doc.docArr[0].parent() === doc; // true
만약 subdocument
를 여러번 중첩해서 사용한 경우에는, 최상위 document에는 ownerDocument()
함수를 이용해서 접근할 수 있습니다.
const schema = new Schema({
level1: new Schema({
level2: new Schema({
test: String
})
})
});
const Model = mongoose.model('Test', schema);
const doc = new Model({ level1: { level2: 'test' } });
doc.level1.level2.parent() === doc; // false
doc.level1.level2.parent() === doc.level1; // true
doc.level1.level2.ownerDocument() === doc; // true
array schema 만들기 (array 선언 문법의 다른 방법)
만약 object로 구성되어있는 array로 Schema를 만들고 싶다면, mongoose는 자동으로 object를 schema로 변환해줍니다.
var parentSchema = new Schema({
children: [{ name: 'string' }]
});
// 똑같음
var parentSchema = new Schema({
children: [new Schema({ name: 'string' })]
});
single subdocument 만들기 (single subdocument 선언 문법의 다른 방법)
위와 유사하게, single subdocument도 Schema 인스턴스로 랩핑하는 것을 생략할 수 있습니다. 하지만 history 관리를 위해서 이런 방법(생략하는 방법)은 반드시 option(상위 스키마 인스턴스화 또는 몽구스 인스턴스화)을 이용해서 진행되어야합니다.
var parentSchema = new Schema({
child: { type: { name: 'string' } }
}, { typePojoToMixed: false });
// 동일함
var parentSchema = new Schema({
child: new Schema({ name: 'string' })
});// TODO: 아직 무엇이 다른지 이해못함
// 동일하지 않음! 주의 - a Mixed path is created instead!
var parentSchema = new Schema({
child: { type: { name: 'string' } }
});
typePojoToMixed
: 기본값은 true. POJO(Plain Old JavaScript Objects)로 설정된 유형이 혼합된 path인지 subdocument
인지 판단합니다.
개인적으로 덧붙이는 내용
Schema.prototype.pre()
— 개인적으로 덧붙임
Schema.prototype.pre()
는 mongoose에서 사용하는 미들웨어로 전처리(pre)로 쓰이는 method입니다.
아래 예시 코드로 볼 때, save()함수가 호출되면 콘솔이 찍히는 순서는
save 미들웨어
→ saved!
순서로 콘솔이 찍힙니다.
save()
함수가 실행되기 직전에 미들웨어인 testSchema.pre('save', callback)
부분이 호출되고, callback 함수의 내용을 실행한 후 save() 함수가 호출되게 됩니다. 복잡한 유효성검사를 수행해야하거나 x가 삭제되면 y도 삭제(또는 업데이트)할 때 유용하다고 합니다. 이 부분들이 atomic한 것인지는 아직 저는 모르겠습니다. 😨 알게 되면 추가해두겠습니다.
// create a schema
const testSchema = new mongoose.Schema({
name: String,
description: String,
});testSchema.pre('save', function(next) {
console.log('save 미들웨어')
this.name = this.name.toUpperCase();
next();
});// create the model
const testModel = mongoose.model('Myevent', eventSchema);const doc = new eventModel({
name: 'test8',
description: 'description',
})
await doc.save()
console.log('saved!')