-
MongoDB를 활용한 API - Read(Populate,Vitual,Nesting)카테고리 없음 2024. 7. 21. 11:02
1.데이더 구조
Collection 관계
요구되는 반환 JSON
{ "_id": "669761e5df7f8f03fbd7d201", "title": "impedit molestiae magni", "content": "Perspiciatis et ab earum. Porro vel ad ipsam debitis qui et laboriosam optio. Culpa minus est laboriosam est temporibus. Libero enim ratione asperiores qui asperiores blanditiis quo sed iusto. "isLive": true, "user": { "name": { "first": "Brisa", "last": "Reilly" }, "_id": "669761e5df7f8f03fbd7d1ed", "username": "Grayce.Weissnat83", "age": 29, "email": "Hailey_Haley@hotmail.com", "__v": 0, "createdAt": "2024-07-17T06:17:09.056Z", "updatedAt": "2024-07-17T06:17:09.056Z" }, "__v": 0, "createdAt": "2024-07-17T06:17:09.177Z", "updatedAt": "2024-07-17T06:17:09.177Z", "comments": [ { "_id": "669761e5df7f8f03fbd7d318", "content": "Rerum omnis possimus dolorem quis rerum.", "user": { "name": { "first": "Kyler", "last": "Lockman" }, "_id": "669761e5df7f8f03fbd7d1f2", "username": "Kennith5679", "age": 10, "email": "Jada_Schultz@hotmail.com", "__v": 0, "createdAt": "2024-07-17T06:17:09.057Z", "updatedAt": "2024-07-17T06:17:09.057Z" }, "blog": "669761e5df7f8f03fbd7d201", "__v": 0 }, { "_id": "669761e5df7f8f03fbd7d3d6", "content": "Molestias animi et adipisci accusamus.", "user": { "name": { "first": "Connor", "last": "Kuphal" }, "_id": "669761e5df7f8f03fbd7d1f9", "username": "Mohammed_Fritsch30", "age": 39, "email": "Ludwig75@gmail.com", "__v": 0, "createdAt": "2024-07-17T06:17:09.058Z", "updatedAt": "2024-07-17T06:17:09.058Z" }, "blog": "669761e5df7f8f03fbd7d201", "__v": 0 } ], "id": "669761e5df7f8f03fbd7d201" },
2. Populate
populate로 부모document 데이터 불러오기
- 호출된 blog를 작성한 user의 정보도 포함해서 호출
GET - /blog
blogRouter.get('/', async (req, res) => { try { const blogs = await Blog.find({}) .limit(10) .populate([{ path: 'user' }]); return res.send({ blogs }); } catch (error) { console.error(error); return res.status(500).send({ err: error.message }); } });
Blog.js
import { model, Schema, Types } from 'mongoose'; const BlogSchema = new Schema( { title: { type: String, required: true }, content: { type: String, required: true }, isLive: { type: Boolean, require: true, default: false }, user: { type: Types.ObjectId, required: true, ref: 'user' }, }, { timestamps: true }, ); export const Blog = model('blog', BlogSchema);
Virtual Populate로 자식document 정보 불러오기
- 호출된 blog 하위에 작성된 comment 데이터도 포함해서 호출
GET - /blog
blogRouter.get('/', async (req, res) => { try { const blogs = await Blog.find({}) .limit(10) .populate([{ path: 'user' }, { path: 'comments' }]); return res.send({ blogs }); } catch (error) { console.error(error); return res.status(500).send({ err: error.message }); } });
Blog.js
import { model, Schema, Types } from 'mongoose'; const BlogSchema = new Schema( { title: { type: String, required: true }, content: { type: String, required: true }, isLive: { type: Boolean, require: true, default: false }, user: { type: Types.ObjectId, required: true, ref: 'user' }, }, { timestamps: true }, ); BlogSchema.virtual('comments', { ref: 'comment', localField: '_id', foreignField: 'blog', }); //설정 BlogSchema.set('toObject', { virtuals: true }); BlogSchema.set('toJSON', { virtuals: true }); export const Blog = model('blog', BlogSchema);
Populate로 자식document 의 부모 document불러오기
- 호출된 blog 하위의 comment의 각각 작성자 user데이터 불러오기
GET - /blog
blogRouter.get('/', async (req, res) => { try { const blogs = await Blog.find({}) .limit(10) .populate([ { path: 'user' }, { path: 'comments', populate: { path: 'user' } }, ]); return res.send({ blogs }); } catch (error) { console.error(error); return res.status(500).send({ err: error.message }); } });
Comment.js
import { Schema, model, Types } from 'mongoose'; const CommentSchema = new Schema( { content: { type: String, require: true }, user: { type: Types.ObjectId, require: true, ref: 'user' }, blog: { type: Types.ObjectId, require: true, ref: 'blog' }, }, { timestamp: true }, ); export const Comment = model('comment', CommentSchema);
comment.js는 user의 자식 document이므로 이미 user를 참조하고 있기 때문에 virtual 정보를 설정해주지 않아도 된다.
정리
max 3 Request
population을 통해서 최대 3번의 request로 원하는 데이터를 담은 JSON을 반환할 수 있다.
3. Denormalize
1 request
MongoDB는 자식document를 부모document에 내장하므로써 한번의 request로 document의 내장된 정보 모두 호출 할수 있게 해준다. 즉 Comment가 생성되 었을때, Comment도 저장을 하지만 Comment를 Blog안에 아예 포함해서 같이 저장하는 것이다.
다만, 이런 방식을 사용하면 읽기(read)를 사용하는 API는 매우 단순해지고 속도도 매우 빨라지지만, C)생성(insert), U)수정(update), D)삭제(delete) 와 관련한 작업이 더 늘어 날 수 있다.
Blog스키마 변경
blog스키마에 user의 정보를 ObjectId만 저장하는 것이 아니라 blog를 조회할때 필요한 user의 정보도 저장시에 같이 저장이되어 수월할게 불러올수 있게 blog스키마를 변경해준다.
변경전
Blog.js
import { model, Schema, Types } from 'mongoose'; const BlogSchema = new Schema( { title: { type: String, required: true }, content: { type: String, required: true }, isLive: { type: Boolean, require: true, default: false }, user: { type: Types.ObjectId, require: true, ref: 'user' }, }, { timestamps: true }, ); export const Blog = model('blog', BlogSchema);
저장된 데이터
변경 후
Blog.js
import { model, Schema, Types } from 'mongoose'; const BlogSchema = new Schema( { title: { type: String, required: true }, content: { type: String, required: true }, isLive: { type: Boolean, require: true, default: false }, user: { _id: { type: Types.ObjectId, required: true, ref: 'user' }, //user에서 username unique는 true지만 //comment에서는 user가 여러 comment를 쓸수 //있으므로 username이 unique하지 않다. username: { type: String, require: true }, name: { first: { type: String, require: true }, last: { type: String, require: true }, }, }, }, { timestamps: true }, ); export const Blog = model('blog', BlogSchema);
저장된 데이터
Comment 내장 데이터를 위한 Blog.js 설정
Blog.js
import { model, Schema, Types } from 'mongoose'; import { CommentSchema } from './Comment.js'; const BlogSchema = new Schema( { title: { type: String, required: true }, content: { type: String, required: true }, isLive: { type: Boolean, require: true, default: false }, user: { _id: { type: Types.ObjectId, required: true, ref: 'user' }, username: { type: String, require: true }, name: { first: { type: String, require: true }, last: { type: String, require: true }, }, }, comments: [CommentSchema], }, { timestamps: true }, ); // BlogSchema.virtual('comments', { // ref: 'comment', // localField: '_id', // foreignField: 'blog', // }); //설정 // BlogSchema.set('toObject', { virtuals: true }); // BlogSchema.set('toJSON', { virtuals: true }); export const Blog = model('blog', BlogSchema);
가상 필드로 comments 정보를 불러오지 않을 것이기 때문에 virtual에 관한 설정을 주석처리해준다. Blog에 comments에 대한 정보를 여러개 일수 있으므로 배열이다. 그리고 CommentSchema를 그대로 불러오면 comment에 해당 스키마에 정의된 모든 정보를 내장한다.
내장 데이터를 위한 API수정
comment를 생성할때 comment document만 생성하는 것이 아니라 Blog에 내장된 comments 정보도 함께 update해 주어야한다.
commentController.js
export const createComment = async (req, res) => { const validationResult = await checkSchema( { content: { notEmpty: { errorMessage: 'content이 값이 오지 않았습니다.' }, isString: { errorMessage: 'content는 문자열이어야 합니다.' }, }, userId: { notEmpty: { errorMessage: 'user이 값이 오지 않았습니다.' }, }, }, ['body'], ).run(req); const validationError = validationResult .filter((err) => err.errors.length > 0) .map((err) => err.errors[0]); if (validationError.length > 0) { return res.status(400).send({ err: validationError }); } const { content, userId } = req.body; if (!isValidObjectId(userId)) { return res.status(400).send({ err: '유효하지 않은 userId 타입 입니다.' }); } const { blogId } = req.params; if (!isValidObjectId(blogId)) { return res.status(400).send({ err: '유효하지 않은 blogId 타입 입니다.' }); } try { const [user, blog] = await Promise.all([ User.findById(userId), Blog.findById(blogId), ]); if (!user || !blog) { return res.status(400).send({ err: '존재하지 않는 user or blog입니다.' }); } if (!blog.isLive) { return res.status(400).send({ err: '아직 게시 되지 않은 blog입니다.' }); } const comment = new Comment({ content, user, blog }); //comment생성 뿐 아니라 //해당 comment를 내장하게될 blog의 정보도 수정 await Promise.all([ await comment.save(), await Blog.updateOne({ _id: blogId }, { $push: { comments: comment } }), ]); return res.status(400).send({ comment }); } catch (error) { console.error(error); return res.status(500).send({ err: error.message }); } };
저장하는 작업에서 부하가 조금 걸리겠지만 사실 블로그나 댓글을 새로 작성하는 작업보다는 해당 데이터를 읽기 즉 보는 작업이 수십배, 수백배는 빈번하게 읽어나기 때문에 읽기 시에 리소스를 절약하기위해 저장시에 작업에 리소스를더 투입하는 방식이라고 이해하면 된다.
API 수정 후 저장된 blog 데이터
[출처 - mongoDB기초부터 실무까지(feat. Node.js), 저 김시훈]