mongoose
는 MongoDB
의 ODM(Object Data Modeling)
라이브러리이다.
MongoDB
는 유연한 데이터 모델(NoSQL)이어서 컬럼을 추가하거나 삭제하거나,
저장할 수 있는 데이터의 형식이 정말 자유롭기 때문에 그만큼 부작용이 많다.
mongoose
는 스키마와 모델을 정의해야하기 때문에 조금은 형식적으로 DB를 관리할 수 있다.
mongoose
의 개요는 대략 이렇고, 나는 이 중 timestamp
옵션과 lean()
함수에 대해 적으려고 한다.
timestamp
스키마 정의 시 timestamp
옵션을 사용하면 createdAt(생성 시각)과 updatedAt(갱신 시각) 값이 자동으로 저장된다.
데이터가 언제 생성되었고 언제 수정되었는지에 대한 정보는 프로젝트를 하다보면 빈번히 필요로 하는 정보인데 일일히 createdAt을 정의하는 것보다 옵션 설정으로 자동으로 저장할 수 있게 하는 것이 좋은 것 같다.
(스키마 변수 명).set('timestamps', true);
//또는
const (스키마 변수 명) = new Schema({
// 속성 정의
}, {
timestamps: true
});
//로 옵션 설정이 가능하다.
(스키마 변수 명).set('timestamps', { createdAt: false, updatedAt: true });
//둘 중 하나의 값만 저장하는 것도 가능하고
(스키마 변수 명).set('timestamps', { createdAt: "createDate", updatedAt: "updateDate" });
//이름을 변경하여 저장하는 것도 가능하다.
lean()
lean
을 사용하면 mongoose
쿼리 실행 시 mongoose document
로 반환하지 않고 바로 JavaScript Object
값으로 변환시켜준다. 이를 통해 쿼리 수행을 더 빨라지고 메모리를 집약적으로 사용할 수 있게 한다. mongoose document
는 JavaScript Object
보다 무겁지만 상태 추적 등 여러 내부 기능이 존재하는데, 데이터의 수정 없이 쿼리를 전송한다면 lean을 사용하는 것이 좋다.
const schema = new mongoose.Schema({ name: String });
const MyModel = mongoose.model('Test', schema);
await MyModel.create({ name: 'test' });
const normalDoc = await MyModel.findOne();
// `lean` 옵션을 사용할 수 있다면 `lean()`을 사용하자
const leanDoc = await MyModel.findOne().lean();
//v8Serialize는 모든 타입의 데이터를 buffer로 변환한다.
v8Serialize(normalDoc).length; // 180
v8Serialize(leanDoc).length; // 32, 5배 작다!
// mongoose document와 lean 옵션을 사용해 반환 받은 object의
// JSON 형태는 둘 다 크기가 같다.
// 이 추가적인 메모리는 Node.js 프로세스가 얼마나 많은 메모리를 사용하냐에 영향을 준다.
// (네트워크에 보내지는 데이터의 양X)
JSON.stringify(normalDoc).length === JSON.stringify(leanDoc).length; // true
다만 lean
을 사용하면 다음과 같은 기능을 사용할 수 없다.
- 변화 추적 (Change Tracking)
- 캐스팅, 검증 (Casting and validation)
- Getter, Setter
- 가상 필드 (Virtuals)
- save()
다음은 lean
을 사용하지 않았을 때/사용했을 때의 getter와 setter 결과이다.
// `Person` 모델을 정의한다. 2개의 직접 만든 getter과 가상 속성인 `fullName`을 가진다.
// lean 옵션이 설정되었다면 getter과 가상 속성에 접근할 수 없다.
const personSchema = new mongoose.Schema({
firstName: {
type: String,
get: capitalizeFirstLetter
},
lastName: {
type: String,
get: capitalizeFirstLetter
}
});
personSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
function capitalizeFirstLetter(v) {
// 'bob' -> 'Bob'
return v.charAt(0).toUpperCase() + v.substring(1);
}
const Person = mongoose.model('Person', personSchema);
// document로 생성하고 lean document로 가져온다.
await Person.create({ firstName: 'benjamin', lastName: 'sisko' });
const normalDoc = await Person.findOne();
const leanDoc = await Person.findOne().lean();
normalDoc.fullName; // 'Benjamin Sisko'
normalDoc.firstName; // `capitalizeFirstLetter()` getter의 영향으로 'Benjamin'가 된다.
normalDoc.lastName; // `capitalizeFirstLetter()` getter의 영향으로 'Sisko'가 된다.
leanDoc.fullName; // undefined
leanDoc.firstName; // 직접 지정한 getter가 작동하지 않아 'benjamin'가 된다.
leanDoc.lastName; // 직접 지정한 getter가 작동하지 않아 'sisko'가 된다.
populate
와 virtual populate
는 가능하다.
populate
는 밑의 코드를 예시로 들어 설명하자면
Group에서 쿼리를 실행하면 원래 members 필드의 값은 ObjectId
로만 나오는데
populate
를 실행하면 members 필드의 값을 Object
로 바꿔서 데이터를 읽을 수 있게 한다.
// 모델 생성
const Group = mongoose.model('Group', new mongoose.Schema({
name: String,
members: [{ type: mongoose.ObjectId, ref: 'Person' }]
}));
const Person = mongoose.model('Person', new mongoose.Schema({
name: String
}));
// 데이터 초기화
const people = await Person.create([
{ name: 'Benjamin Sisko' },
{ name: 'Kira Nerys' }
]);
await Group.create({
name: 'Star Trek: Deep Space Nine Characters',
members: people.map(p => p._id)
});
// lean 쿼리 실행
const group = await Group.findOne().lean().populate('members');
group.members[0].name; // 'Benjamin Sisko'
group.members[1].name; // 'Kira Nerys'
// `group`과 populate된 `member` 둘 다 lean이다.
group instanceof mongoose.Document; // false
group.members[0] instanceof mongoose.Document; // false
group.members[1] instanceof mongoose.Document; // false
virtual populate
는 실제로 MongoDB
에 저장되지는 않지만 어떤 계산된 속성을 사용해야할 때 사용하는 것 같다.
groupSchema.virtual('members', {
ref: 'Person', // members 필드는 Person 스키마를 참조한다.
localField: '_id', // Group 스키마에 저장된 _id를 기준으로 비교하여 가져온다.
foreignField: 'groupId' // Person 스키마의 groupId가 _id와 비교된다.
});
// 모델 생성
const groupSchema = new mongoose.Schema({ name: String });
// `members`라는 가상 속성을 생성
groupSchema.virtual('members', {
ref: 'Person', //참조 모델
localField: '_id', //비교할 필드 (Group)
foreignField: 'groupId' //비교 당할 필드(Person)
});
const Group = mongoose.model('Group', groupSchema);
const Person = mongoose.model('Person', new mongoose.Schema({
name: String,
groupId: mongoose.ObjectId
}));
// 데이터 초기화
const g = await Group.create({ name: 'DS9 Characters' });
await Person.create([
{ name: 'Benjamin Sisko', groupId: g._id },
{ name: 'Kira Nerys', groupId: g._id }
]);
// lean 쿼리 실행
const group = await Group.findOne().lean().populate({
path: 'members',
options: { sort: { name: 1 } }
});
group.members[0].name; // 'Benjamin Sisko'
group.members[1].name; // 'Kira Nerys'
// `group`과 populate된 `members` 모두 mongoose doc이 아닌 lean이다.
group instanceof mongoose.Document; // false
group.members[0] instanceof mongoose.Document; // false
group.members[1] instanceof mongoose.Document; // false