비관계형 데이터베이스 중 몽고DB는 제가 비관계형 데이터베이스에서 가장 좋아하는(?) 데이터 베이스 입니다.
무료에다 성능 또한 훌륭하기 때문 입니다!
얼마전 데이터베이스 인기순위에서도 1등(비관계 중)을 한 것을 볼 수 있었습니다. * 기준 : 2022.09
Nestjs에서 Mongodb를 사용하기 위해서는 역시나 모듈을 설치해야 합니다.
Express 프레임워크에서부터 널리 사용 하는 몽구스(mongoose)를 설치 해 줍니다.
npm install @nestjs/mongoose mongoose
#1. 도큐먼트(모델, 스키마) 생성
도큐먼트란 데이터를 매핑하기 위한 ORM의 첫번째 단계 입니다.
시퀄라이즈에서의 모델을 만들어 준 것 처럼 동일한 방식으로 클래스를 만들어야 합니다.
특히 몽고db같은 경우에는 데이터에 대한 제약조건이 없으므로 도큐먼트를 만들때 제약조건(not null, type 등)또한 고려를 해 주어야 합니다.
* 파일이름 : 내도큐먼트.ts
import { Prop, Schema, SchemaOptions, SchemaFactory } from '@nestjs/mongoose';
import { Document, Schema as sch } from 'mongoose';
const options: SchemaOptions = {
timestamps: true, //자동으로 등록일, 수정일을 넣어줍니다.
collection : 'testCollection',
_id : true //기본 인덱스인 id값을 매핑하여 줍니다.
};
@Schema(options)
export class 내도큐먼트 extends Document {
@Prop({type : sch.Types.String})
text : string;
@Prop({type : sch.Types.Number})
num : number;
@Prop({type : sch.Types.Array})
arr : Array<any>;
}
export const 내스키마 = SchemaFactory.createForClass(내도큐먼트);
export type 내타입 = 내도큐먼트 & Document;
몽구스(mongoose)의 Document 라는 클래스를 상속받는 "내도큐먼트" 라는 클래스 입니다.
해당 클래스는 testCollection 컬렉션을 매핑하게 되어 있습니다.
testCollection 이라는 이름의 컬렉션에 3개의 속성을 가진 도큐먼트를 정해주었습니다.
옵션을 자세히 살펴보면 _id와 timestamps 라는 항목의 값이 true 인 것을 볼 수 있습니다.
해당 값에 의해서 자동으로 _id값과 등록(createdAt), 수정(updatedAt)과 관련된 속성값이 생성/매핑 될 예정 입니다.
여기서 export 부분을 주의해야 합니다!
"내스키마" 는 몽구스 모듈이 사용할 스키마를 만드는 객체이고,
"내타입"은 나중에 사용할 서비스에서의 모델의 제네릭에 사용될 타입 입니다.
* 코드로 내용을 이해하는 게 좋을 수도 있겠습니다....
#2. 모듈 추가
도큐먼트를 등록 하였다면 이제 모듈을 추가하여 줍니다.
* 파일이름 : app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';
import {내도큐먼트, 내스키마} from './내도큐먼트';
@Module({
imports: [
//아이디(id), 비밀번호(pwd)가 있는 경우 : mongodb://id:pwd@localhost:27017/디비명
MongooseModule.forRoot('mongodb://localhost:27017/디비명', { }),
MongooseModule.forFeature([{name : 내도큐먼트.name, schema : 내스키마}])
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
forRoot 함수가 사용되는 부분은 몽고DB에 접속하기 위한 기능입니다.
만약 커넥션과 관련된 오류가 난 다면 해당 몽고db가 외부접속이 가능한지 확인해야 하며,
권한과 관련된 오류가 나면 아이디와 비밀번호를 확인 후 주석처리된 방법을 통해서 요청해야 합니다.
다음으로는 사용할 도큐먼트를 몽구스(mongoose)모듈에 알려주는 것 입니다.
forFeature라는 함수를 통해서 사용 할 도큐먼트를 배열 형태로 등록하여 줍니다.
이때 "내도큐먼트" 클래스에서 export 한 "내스키마"를 사용해야 합니다.
"내도큐먼트" 클래스에서 name이라는 값은 몽구스의 Document를 상속 받았기 때 문에 사용한 값 입니다.
여기 까지 오류가 나지 않았다면 이제 다음단계로 넘어 갑니다!
#3. 의존성 주입
컨트롤러에서 바로 사용하는 것 보다는 역시나 서비스에서 만들어진 도큐먼트를 주입받아서 사용하게 만듭니다.
* 파일이름 : app.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import {내도큐먼트, 내타입} from './내도큐먼트';
@Injectable()
export class AppService {
constructor(@InjectModel(내도큐먼트.name) private doc : Model<내타입>) {
}
}
"내도큐먼트" 클래스에서 export 한 "내타입"을 몽구스의 Model 클래스의 제네릭에 매핑하여 주었습니다.
몽구스의 Model 클래스는 데이터를 관리 할 때 "내도큐먼트" 클래스에서 선언한 데이터를 기반으로 동작하게 됩니다.
#3-1. 저장
먼저 저장관련 기능 입니다.
Model 클래스를 new 연산자를 통해서 인스턴스화 하여 줍니다.
이때 "내도큐먼트" 클래스에서 정의한 속성을 따라야 합니다.
그리고 나서 해당 객체에 save 함수를 통해서 해당 도큐먼트를 몽고db에 등록 할 수 있습니다.
일반적인 데이터 등록하는 방법 입니다.
내가 정의한 도큐먼트의 모습 그대로 데이터를 만들어서 넣어주면 됩니다.
* 파일이름 : app.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import {내도큐먼트, 내타입} from './내도큐먼트';
@Injectable()
export class AppService {
constructor(@InjectModel(내도큐먼트.name) private doc : Model<내타입>) {
this.insertType1();
}
insertType1(){
const data1 = new this.doc({text:'abcd', num : 1234, arr : [1,2,3,4,5]});
data1.save();
}
}
만약 데이터를 여러개 한번에 등록 한 다면 insertMany 함수를 사용할 수 있습니다.
* 파일이름 : app.service.ts
//생략..
insertType2(){
const docs = [
new this.doc({text:'abcd', num : 5678, arr : [1,2,3,4,5]}),
new this.doc({text:'ggqe', num : 8923, arr : [11,22]}),
new this.doc({text:'refw', num : 5123, arr : ['aaa', 'fff', 12121]})
];
this.doc.insertMany(docs);
}
배열에 도큐먼트를 담아둔 뒤에 전역객체인 Model 클래스에서 insertMany 함수를 통해 저장하는 모습 입니다.
데이터를 한번에 등록 한다면 insertMany를 사용하는 것도 나쁘지 않습니다.
#3-2. 수정, 삭제
수정과 삭제하는 기능은 어렵지 않습니다.
수정과 삭제에서는 대상을 찾기위한 조건(where)이 필수 입니다.
여기서는 기본으로 생성되는 인덱스인 _id 값을 가지고 수정과 삭제에 사용하였습니다.
* 파일이름 : app.service.ts
//생략..
update (_id : string = '631992be602911b2c3b92474'){
//upsert가 true 면 insert 행위를 함
this.doc.updateOne({ _id }, {$set: {text:'bbbbbb'}}, {upsert : true}, err=>{
console.log(err)
});
}
delete (_id : string = '631992be602911b2c3b92474'){
this.doc.remove({ _id }, err=>{
console.log(err)
});
}
updateOne 함수는 1개의 데이터를 업데이트 할 때 사용되며, 여러개의 도큐먼트를 업데이트 하려면 updateMany 함수를 사용해야 합니다.
첫번째 값은 수정하기 위한 조건(where)을 입력 해 주도록 합니다.
두번째 값은 적용할 데이터를 의미 합니다.
세번째 옵션에서의 upsert는 없는 경우 등록(insert) 옵션입니다.
만약 upsert를 사용하는 데 도큐먼트의 속성이 없는 상태라면 데이터 수정간 오류가 발생하므로 주의하여야 합니다.
#3-3. 읽기
데이터를 읽는 것 또한 쉽습니다.
* 파일이름 : app.service.ts
//생략..
select(){
this.doc.find({}, (err, arg : Array<any>)=>{
if(err) return;
arg.forEach( (element : 내도큐먼트) => {
console.log(element)
});
})
}
첫번째 find 함수 뒤에 객체에는 조건을 붙여줄 수 있습니다.
가령 문자값을 가지고 있는 text 속성에서 ab라는 값이 있는 데이터를 조회 하려면 아래와 같이 작성 합니다.
find({text :{$regex : 'ab'}}, (err, arg : Array<any>)=>{
if(err) return;
arg.forEach( (element : 내도큐먼트) => {
console.log(element)
});
})
조회에 사용되는 연산자는 몽고db 매뉴얼 홈페이지에서 확인가능 합니다.
https://www.mongodb.com/docs/manual/
#3-4. 집계(aggregate, group)
몽고DB의 성능을 체감하려면 역시나 등록(insert) 행위 보다도 데이터를 그룹(aggregate, 집계)핑을 해 보아야 합니다.
몽구스 모듈은 몽고DB 쿼리를 거의 대부분 그대로 재현하고 있기 때 문에 같은 방식으로 사용 할 수 있습니다.
* 파일이름 : app.service.ts
//생략..
groupBy(){
this.doc.aggregate([
{
$project: { //가져올 데이터 선언
text: 1, //컬렉션에 저장된 도큐먼트의 text속성입니다.
num: 1, //컬렉션에 저장된 도큐먼트의 num속성입니다.
createdAt:1, //컬렉션에 저장된 도큐먼트의 createdAt속성입니다.
arr : 1, //컬렉션에 저장된 도큐먼트의 arr속성입니다.
//여기부터는 저장되어 있는 데이터를 변경하여 가져오는 부분 입니다.
dateToStr: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } }, //날짜->문자1
dateToStrMonth: { $dateToString: { format: "%Y-%m", date: "$createdAt" } }, //날짜->문자2
dateToStrYear: { $dateToString: { format: "%Y", date: "$createdAt" } }, //날짜->문자3
ArrayCnt : { $cond: { if: { $isArray: "$arr" }, then: { $size: "$arr" }, else: 0} }, //배열의 갯수를 측정 후 숫자
ArraySum : { $cond: { if: { $isArray: "$arr" }, then: { $sum: "$arr" }, else: 0} } //배열의 숫자 원소의 총 합
}
},
{ $match: { num: { $gte: 5 }}}, //조건
{
$group: {
_id : "$text", //그룹할 대상
firstCreateDate : { $first: "$createdAt" }, //가장 처음값만 가져오기
lastCreateDate : { $last: "$createdAt" }, //가장 나중값만 가져오기
dataSum : { $sum: "$num" }, //sum 속성의 합계 구하기
dataCount : { $sum: 1 }, //그룹한 text의 총 갯수
ArraySum : { $sum: "$ArraySum" }, //ArraySum 값의 총 합
ArrayCount : { $sum: "$ArrayCnt" }, //ArrayCnt 값의 총 합
pushStyle: { $push: "$dateToStr" } //dateToStr 값을 배열형태로 밀어넣기
}
},
{ $skip: 0 }, //페이징을 위한 시작지점 커서
{ $limit: 50 } //페이징을 위한 갯수
]).allowDiskUse(true).then( arg=>{
console.log(arg)
}).catch(err=>{
console.log(err)
})
}
주로 사용되는, 사용 할 만한 내용을 바탕으로 작성하여본 집계관련 내용 입니다.
몽고DB의 특성상 JOIN 같은 기능은 지양하기 때 문에 조회하는 컬렉션의 도큐먼트의 속성값만 가지고 기능을 만드는 것을 추천합니다.
* 만약 lookup과 unwind 연산자를 통해 다른 컬렉션을 join 하여 집계(aggregate)다면, 데이터가 몇십만건으로 늘어난 경우 조회가 불가능할 정도의 시간이 걸리는 것을 볼 수 있습니다.
* 통계가 필요한 컬렉션에 join이 없도록 설계하는 것이 몽고DB의 기초라 생각 합니다.
facet이라는 함수를 활용하면 다른종류의 집계 행위를 병렬로 처리하도록 할 수 있습니다.
* 파일이름 : app.service.ts
//생략..
groupByWithFacet(){
this.doc.aggregate().facet({
result1 : [{$group : {_id : "$text"}}],
result2 : [{$group : {_id : "$num"}}]
}).then( arg=>{
console.log(arg[0])
}).catch(err=>{
console.log(err)
})
}
result1 이라는 키 값으로 1개의 집계동작을 하게 하였고, result2 라는 키 값으로 또 다른 집계 동작을 하게 하였습니다.
이렇게 동작한 결과는 사용자가 만든 키 값을 가지고 확인가능 합니다.
여기까지 Nestjs에서 몽고db와 관련된 내용이였습니다.
위 내용에 사용된 코드는 아래 제 깃허브에서 받아볼 수 있습니다.
https://github.com/TaeSeungRyu/NestProject/tree/main/step13
공부하면 할 수록 재미있고 즐거운 nestjs!!!
궁금한점 또는 틀린부분은 언제든 연락 주세요
'Node.js > Nestjs (Nest.js)' 카테고리의 다른 글
Nestjs 프레임워크 서버(log, winston) -16 (2) | 2022.10.04 |
---|---|
Nestjs 프레임워크 서버(emitter, rxjs, schedule) -15 (0) | 2022.09.22 |
Nestjs 프레임워크 서버(sequelize, database) -13 (1) | 2022.09.11 |
Nestjs 프레임워크 서버(websocket) -12 (0) | 2022.09.07 |
Nestjs 프레임워크 서버(XSS 방지, Cross Site Scripting) -11 (0) | 2022.08.29 |
댓글