mongoose는 Schema에 기본적으로 _id
라는 필드명으로 인덱스(type: mongoose.Schema.Types.ObjectId)를 자동으로 추가해줍니다.
따라서 우리가 document를 하나 생성할 때마다 자동으로 인덱스가 생성되어 추가됩니다.
문서를 조회하면 { _id: ObjectId(‘63c8f2a264ef009315acf031’), //…other property }
와 같이 결과가 조회됩니다.
저희는 MySQL과 MongoDB를 동시에 사용하고 있습니다.
MySQL은 인덱스에 대해 id
라는 컬럼명을 갖고, MongoDB는 _id
라는 필드명를 갖고 있습니다.
하지만 클라이언트에게 데이터를 보낼 때 데이터 형식을 통일하고자 하였기에, MongoDB에서 조회한 데이터를 응답할 때 _id
를 id
로 변환하고자 하였습니다.
mongoose의 find() 함수에는 projection
이라는 파라미터가 있습니다.
해당 파라미터는 select
와 같은 역할을 하며 리턴할 필드들을 선택할 수 있습니다.
mongoose의 find() 함수의 projection
파라미터로 { _id: 0 , id: ‘$_id’ } 라는 값을 넘겨주면 _id
라는 필드는 선택하지 않고, _id
필드의 값을 id
라는 필드명으로 받아올 수 있습니다.
// '_id' 프로퍼티를 조회하지 않기
const messages = DMModel.find({}, '-_id').exec();
// '_id' 프로퍼티 대신 'id'라는 프로퍼티명으로 조회하기
const messages = DMModel.find({}, { _id: 0, id: '$_id' }).exec();
하지만 문제가 발생하였습니다.
이렇게 데이터를 받아올 경우 각 인스턴스의 id 값을 호출하는 것이 불가능했습니다.
// 정확한 이유는 아직 찾지 못했습니다..
const messages = this.dmModel.find({}, { _id: 0, id: '$_id' });
// { _id: new ObjectId('63c8f2a264ef009315acf031'), ..., createdAt: 2023-01-19T08:55:34.417Z }
console.log(messages[0].name); // 2023-01-19T08:55:34.417Z
console.log(messages[0].id); // null
console.log(messages[0]._id); // undefined
두번째 해결방법은 바로 map
과 mongoose schema의 instance methods
를 사용하는 것이었습니다.
find() 함수를 통해 데이터를 조회할 때는 _id
라는 필드명 그대로 받아오고, 후에 _id
를 id
로 변환해주는 methods
를 통해 결과값을 변환하였습니다.
아래는 제가 구현한 코드입니다.
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document } from 'mongoose';
@Schema({ timestamps: { createdAt: 'createdAt' } })
export class DM extends Document {
@Prop({ required: true, type: mongoose.Schema.Types.Number })
dmRoomId: number;
@Prop({ required: true, type: mongoose.Schema.Types.Number })
sender: number;
@Prop({ required: true })
content: string;
@Prop({ default: new Date(), type: mongoose.Schema.Types.Date })
createdAt: Date;
toClient(): { id: string; sender: number; content: string; createdAt: Date } {
const obj = {
id: this._id,
sender: this.sender,
content: this.content,
createdAt: this.createdAt,
};
return obj;
}
}
export const DMSchema = SchemaFactory.createForClass(DM);
DMSchema.loadClass(DM);
위에서 정의한 toClient()
라는 methods는 각 인스턴스에서 _id와 sender, content, createdAt 값을 각각 id, sender, content, createdAt이라는 키에 담아서 객체를 반환합니다.
그리고 아래와 같이 toClient()를 map으로 각 인스턴스에 대해 호출해서 그 결과값을 array로 반환합니다.
*@fxts/core는 지연평가가 가능한 함수형 프로그래밍 라이브러리입니다.
import { pipe, map, toArray } from '@fxts/core';
// ...
console.log(messages[0]); // { _id: new ObjectId("63c8f2a264ef009315acf031"), ..., createdAt: 2023-01-19T08:55:34.417Z }
console.log(messages[0]._id) // new ObjectId("63c8f2a264ef009315acf031")
console.log(messages[0].id) // 63c8f2a264ef009315acf031
return {
messages: pipe(
messages,
map((message) => message.toClient()),
toArray,
),
// [ { id: '63c8f2a264ef009315acf031',
// ...
// createdAt: 2023-01-19T08:55:34.417Z
// }, ... ]
}
*참고: id는 _id를 string 타입으로 변환하여 반환
이렇게 하면 messages[0].id
로 각 인스턴스의 id 값도 호출가능하고, 클라이언트에게 _id 필드를 id로 변환하여 데이터를 보낼 수도 있습니다.
map() 함수 내에서 로직을 작성해도 되지 않나? 굳이 toClient()라는 메소드를 스키마에 정의한 이유가 있나?
-> 관심사 분리
아래와 같이 작성할 경우 이 함수가 목적으로 하고 있는 기능과 객체 구조를 변경하는 기능이 혼재됨...
코드도 복잡해짐...
따라서 인스턴스 구조를 변환하는 로직은 toClient()라는 함수 안에 선언해서 호출하는 게 깔끔하다고 생각했다.
return {
messages: pipe(
messages,
map((message) => ({
id: message.id,
sender: message.sender,
content: message.content,
createdAt: message.createdAt,
})),
toArray,
),
// [ { id: '63c8f2a264ef009315acf031',
// ...
// createdAt: 2023-01-19T08:55:34.417Z
// }, ... ]
}
끗