1. 1:N 관계
1:N 관계
는 한 쪽 엔티티가 관계를 맺은 엔티티 쪽의 여러 객체를 가질 수 있는 것을 의미한다.실제 DB를 설계할 때 자주 쓰이는 방식이다.
1) Schema 작성
일반적인 게시판을 생각하면, 하나의 유저가 여러 개의 게시글을 작성한다.
콜렉션 | 관계 |
---|---|
유저 | 1 |
게시글 | N |
이러한 유저와 게시글의 관계를 1:N 관계라고 하는데, RDBMS의 경우에는 FK (Foregin Key)를 통해 한 테이블의 필드 중 다른 테이블을 연결하는 방식으로 표현한다. MongoDB
에서도 마찬가지로 ID로 관계를 표현한다.
- 위의 도큐먼트는 유저가 작성한 게시글 데이터이다.
- 이렇게 user의 id를 user에 저장함으로 둘이 연결되어 있는 데이터임을 보여주는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/models/Blog.js
import { Schema, model, Types } from "mongoose";
const BlogSchema = new Schema(
{
title: { type: String, required: true },
content: { type: String, required: true },
// 기본값 설정
isLive: { type: Boolean, required: true, default: false },
// 블로그는 유저 모델과 1:N 관계이다.
// 그래서 어떤 model과 관계가 있는지 ref를 통해 몽구스에게 알려준다.
user: { type: Types.ObjectId, required: true, ref: "user" },
},
{ timestamps: true }
);
const Blog = model("blog", BlogSchema);
export default Blog;
- Blog를 작성한 user를 표현하기 위해 user의 id를 도큐먼트에 기록해야한다.
ref
속성을 사용하여 어떤 콜렉션과 연결되어있는지를 몽구스에게 알려주어야한다.ref
속성에model
의 이름을 기입해야 한다.
2) post 요청 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/routes/blogRoute.js
import { Router } from "express";
import { Blog, User } from "../models/index.js";
import { isValidObjectId } from "mongoose";
const blogRouter = Router();
blogRouter.post("/", async (req, res) => {
try {
const { title, content, isLive, userId } = req.body;
// 유효성 검사
if (typeof title !== "string") res.status(400).send({ error: "title is required" });
if (typeof content !== "string") res.status(400).send({ error: "content is required" });
if (isLive && typeof isLive !== "boolean") res.status(400).send({ error: "isLive must be a boolean" });
if (!isValidObjectId(userId)) res.status(400).send({ error: "userId is invalid" });
// 유저가 존재하는지 검사
let user = await User.findById(userId);
if (!user) res.status(400).send({ error: "user does not exist" });
// blog 인스턴스에는 user 객체가 담겨져 있지만, db에 저장될 때는 알아서 id만 저장한다.
let blog = new Blog({ ...req.body, user });
await blog.save();
return res.send({ blog });
} catch (error) {
console.log(error);
res.status(500).send({ error: error.message });
}
});
- 1:N 관계에서 데이터를 저장할 때 유의해야 하는 것은 관계가 연결된 도큐먼트가 실제로 존재하는지 확인하는 과정을 거쳐야 한다.
fingById
를 통해 해당 user가 존재하는 지 확인할 수 있다.
- 앞서 말했 듯, user의 id를 Blog 도큐먼트에 저장을 해야 하는데 이때 그냥 user 객체 자체를 넣어서 저장해도 db에서는 알아서 user의 id만 저장한다.
2. 예시
1) Schema
Comment 콜렉션은 Blog와 1:N, 유저와 1:N 관계를 가진 콜렉션이다.
블로그 게시 글에도 여러 개의 댓글이 달릴 수도 있고, 유저도 여러 개의 댓글을 작성할 수 있기 때문이다.
- 위의 도큐먼트는 특정 유저가 특정 게시글에 작성한 댓글 데이터이다.
- user의 id와 블로그의 id가 comment 도큐먼트에 저장되어 있음을 알 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/models/Comment.js
import { Schema, model, Types } from "mongoose";
const CommentSchema = new Schema(
{
content: { type: String, required: true },
user: { type: Types.ObjectId, required: true, ref: "user" },
blog: { type: Types.ObjectId, required: true, ref: "blog" },
},
{ timestamps: true }
);
const Comment = model("comment", CommentSchema);
export default Comment;
2) API
일반적으로 댓글은 특정 블로그의 댓글을 불러오기 때문에 블로그의 자식 개념이다. 그래서 계층 구조를 uri에 명확히 기입해야 한다.
/blog/:blogId/comment/:commentId
로 표현을 해야 한다. 그래서 router를 만들 때 다음과 같이 만들어야 한다.
1
2
3
4
5
6
7
8
9
// server.js
app.use("/user", userRouter);
app.use("/blog", blogRouter);
// comment는 특정 블로그의 후기를 불러오는 개념이기 때문에 자식 개념이기 떄문에 uri를 다음과 같이 한다.
app.use("/blog/:blogId/comment", commentRouter);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// src/routes/commentRoute.js
import { Router } from "express";
import { User, Blog, Comment } from "../models/index.js";
import { isValidObjectId } from "mongoose";
// blogId를 params에서 불러오기 위해 mergeParams를 true로 설정해야 한다.
const commentRouter = Router({ mergeParams: true });
commentRouter.post("/", async (req, res) => {
try {
const { blogId } = req.params;
const { content, userId } = req.body;
// 유효성 검사
if (!isValidObjectId(blogId)) return res.status(400).send({ error: "blogId is invalid" });
if (!isValidObjectId(userId)) return res.status(400).send({ error: "userId is invalid" });
if (typeof content !== "string") return res.status(400).send({ error: "content is required" });
// 순차적으로 데이터를 불러올 필요는 없다.
// 동시에 데이터를 불러오는 것이 더 빠르다.
const [blog, user] = await Promise.all([Blog.findById(blogId), User.findById(userId)]);
// 유효성 검사
if (!blog || !user) return res.status(400).send({ error: "blog or user does not exist" });
if (!blog.isLive) return res.status(400).send({ error: "blog is not available" });
// 저장
const comment = new Comment({ content, user, blog });
await comment.save();
return res.send({ comment });
} catch (error) {
console.log(error);
res.status(500).send({ error: error.message });
}
});
commentRouter.get("/", async (req, res) => {
const { blogId } = req.params;
if (!isValidObjectId(blogId)) return res.status(400).send({ error: "blogId is invalid" });
const comments = await Comment.find({ blog: blogId });
return res.send({ comments });
});
export default commentRouter;