모든 Mongoose의 시작은 Schema를 정의하면서 나오게 된다. 각각의 Schema들은 MongoDB collection과 매칭되고 collection의 전체적인 구조를 정의하게 된다.
import mongoose from 'mongoose';
const { Schema } = mongoose;
const blogSchema = new Schema({
title: String,
author: String,
body: String,
comments: [{body: String, date: Date}],
date: {type: Date, default: Date.now},
hidden: Boolean,
meta: {
votes: Number,
favs: Number
}
});
위 예시는 블로그를 구성하는 Schema의 구조를 한번 그려봤다. 단순한 1대1 매칭 구조부터, 리스트, 딕셔너리까지 Schema 데이터를 구성하는 것까지 나와있다. 만약 여기서 우리가 새로운 데이터를 넣으고자 할 때 Schema.add()를 활용해 블로그 스키마에 포스팅된 사이트 이름을 넣을 수 있다.
blogSchema.add({site: String})
or
const SiteSchema = new Schema();
SiteSchema.add({site: 'string'})
blogSchema.add(SiteSchema)
정의된 스키마를 사용하기 위해서 blogSchema를 Model로 변경하는 작업을 해야 한다. 기본 구조는 mongoose.model(modelName, schema)의 형태로 되어 있다.
const Blog = mongoose.model('Blog', blogSchema);
기본적으로 Mongoose는 우리가 생성한 스키마에 _id
라는 특성을 추가해준다. Django에서도 모델을 생성하면 자동적으로 index가 붙는 것처럼 Mongoose도 스키마에 번호가 붙여진다.
const schema = new Schema();
schema.path('_id'); // ObjectId { ... }
예시를 들면,
const Model = mongoose.model('Test', schema);
const doc = new Model();
doc._id instanceof mongoose.Types.ObjectId; // true
우리가 doc
에서 doc._id
가 ObjectId의 인스턴스인지 확인하면 True가 나오는 것처럼 자연스럽게 _id
가 추가 된다. 만약, 우리가 자신만의 _id
를 만들고자 한다면, 스키마를 생성할 때 _id: Number
를 사용하면 된다.
const schema = new Schema({_id: Number});
const Model = mongoose.model('Test', schema);
const doc = new Model();
await doc.save();
doc._id = 1;
await doc.save();
첫번째 await doc.save()
는 _id
가 정의되지 않아 저장되지 않지만 두번째에는저장이 된다. 여기서 알 수 있는 점은 Mongoose에서 모든 데이터에는 _id
가 붙어야 한다는 것이다.
Instance Methods는 간단히 설명하자면, 모델에 함수를 붙이는 것이다.
const animalSchema = new Schema({name: String, type: String});
animalSchema.methods.findSimilarTypes = function(cb) {
return mongoose.model('Animal').find({type: this.type}, cb);
};
const Animal = mongoose.model('Animal', animalSchema);
const dog = new Animal({type: 'dog'});
dog.findSimilarTypes((err, dogs) => {
console.log(dogs);
});
여기서 animalSchema에다가 findSimilarTypes 함수를 생성하고 새로 생성된 dob instance는 이 함수를 사용할 수 있지만 ES6 arrow function
=>
이this.type
로 묶어지는 것을 막기 때문에 여기서는 작동이 되지 않는다.
앞서 단순한 Methods를 생성한 것처럼 Mongoose에다가 정적인 함수를 생성할 수 있다.
animalSchema.statics.findByName = function(name) {
return this.find({name: new RegExp(name, 'i')});
};
or
animalSchema.static('findByBreed', function(breed){return this.find({breed});});
const Animal = mongoose.model('Animal', animalSchema);
let animals = awati Animal.findByName('fido');
animals = animals.concat(await Animal.findByBreed('Poodel'));
여기서도 마찬가지로 ES6 arrow function
=>
를 사용하지 않도록 한다.
animalSchema.query.byName = function(name) {
return this.where({name: new RegExp(name, 'i')})
};
const Animal = mongoose.model('Animal', animalSchema);
Animal.find().byName('fido').exec((err, animals) => {
console.log(animals);
});
Animal.findOne().byNme('fido').exec((err, animal) => {
console.log(animal);
});
여기서 find()
과 findOne()
는 기본적으로 Mongoose에서 제공해주는 Query 함수이다.
MongoDB에서는 secondary indexes를 지원함으로써 우리가 Schema level을 설정하는데 index를 사용할 수 있다.
const animalSchema = new Schema({
name: String,
type: String,
tags: {type: [String], index: true}
});
animalSchema.index({name: 1, type: -1});
여기 나와있는 index를 설명하자면 먼저, name을 기준으로 오름차순(1)으로 정렬을 하고, type을 기준으로 내림차순(-1)으로 정렬을 하는 방식이다. 이외에 우리가 자동적으로 생성(createIndex
)되는 index를 처음부터 정의하거나 막을 수 있다. 정의하는 방식을 막는 방법은 아래와 같다.
1. mongoose.connect('mongodb://user:pass@localhost:port/database', {autoIndex: false});
2. mongoose.createConnection('mongodb://user:pass@localhost:port/database', {autoIndex: false});
3. animalSchema.set('autoIndex', false);
4. new Schema({}, {autoIndex: false});
Mongoose는 에러가 발생하거나 인덱스가 이미 생성되었을 때 모델에서 index 이벤트가 생성된다.
// sparse가 false일 때 자동적으로 _id가 생성되기에 그 반대의 경우 오류 가 발생한다.
animalSchema.index({_id: 1}, {sparse: true});
const Animal = mongoose.model('Animal', animalSchema);
Animal.on('index', error => {
// _id index cannot be sparse가 발생된다.
console.log(error.message);
})
Virtuals는 Spring Boot에서 자동으로 생성해주는 Getter와 Setter의 역할과 같다.
const personSchema = new Schema({
name: {
first: String,
last: String
}
});
const Person = mongoose.model('Person', personSchema);
const axl = new Person({
name: {first: 'Axl', last: 'Rose'}
})
console.log(axl.name.first + ' ' + axl.name.last); // Axl Rose
여기서 우리가 axl의 데이터를 출력하기 위해서는 형식에 맞춰서 코딩을 해줘야 하는 불편함이 존재한다. 이러한 불편함을 Virtual 함수를 만들어 정의를 하고 어느 때에나 가져와서 출력하거나 새롭게 정의할 수 있다.
personSchema.virtual('fullName').
get(function() {
return this.name.first + ' ' + this.name.last;
}).
set(function(v) {
this.name.first = v.substr(0, v.indexOf(' '));
this.name.last = v.substr(v.indexOf(' ') + 1);
});
axl.fullName = 'William Rose';
console.log(axl.fullName); // William Rose
여기서 toJSON()/toObject() 같은 경우 virtual를 가지고 있지 않고 JSON.stringfy
()를 기본적으로 포함하고 있다. 만약 Virtual를 사용할 경우 {virtuals: true}로 정의해야 한다.
Aliases는 Virtual의 Set 함수처럼 우리가 Schema를 정의할 때 타입과 명칭을 분류하는 방식을 말한다.
const personSchema = new Schema({
n: {
type: String,
alias: 'name'
}
});
const person = new Person({name: 'Val'});
console.log(person); // { n: 'Val' }
console.log(person.toObject({ virtuals: true })); // { n: 'Val', name: 'Val' }
console.log(person.name); // "Val"
person.name = 'Not Val';
console.log(person); // { n: 'Not Val' }
여기서 나온 것처럼 alias는 n
이 또다른 명칭인 name을 가지고 있음을 알 수 있다. 이 뿐만 아니라 Schema 안쪽에 또다른 Schema에다가 alias 방식을 적용할 수 있다.
const childSchema = new Schema({
n: {
type: String,
alias: 'name'
}
}, {_id: false});
const parentSchema = new Schema({
c: childSchema,
name: {
f: {
type: String,
alias: 'name.first' // 안쪽에 선언된 path는 전체적인 경로를 포함하는 것을 권장한다.
}
}
})
Schema에는 이외에 다양한 함수 및 변수가 존재함으로 추가적인 공부를 하고자 할때는 아래의 링크를 확인하면 된다.