Home MongoDB-관계된-데이터-관리
Post
Cancel

MongoDB-관계된-데이터-관리

강의 링크


1. 1:N 관계

1:N 관계는 한 쪽 엔티티가 관계를 맺은 엔티티 쪽의 여러 객체를 가질 수 있는 것을 의미한다.

실제 DB를 설계할 때 자주 쓰이는 방식이다.

1) Schema 작성

일반적인 게시판을 생각하면, 하나의 유저가 여러 개의 게시글을 작성한다.

콜렉션관계
유저1
게시글N

이러한 유저와 게시글의 관계를 1:N 관계라고 하는데, RDBMS의 경우에는 FK (Foregin Key)를 통해 한 테이블의 필드 중 다른 테이블을 연결하는 방식으로 표현한다. MongoDB에서도 마찬가지로 ID로 관계를 표현한다.


image-20230204203345615

  • 위의 도큐먼트는 유저가 작성한 게시글 데이터이다.
  • 이렇게 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 관계를 가진 콜렉션이다.

블로그 게시 글에도 여러 개의 댓글이 달릴 수도 있고, 유저도 여러 개의 댓글을 작성할 수 있기 때문이다.

image-20230205221054059

  • 위의 도큐먼트는 특정 유저가 특정 게시글에 작성한 댓글 데이터이다.
  • 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;


This post is licensed under CC BY 4.0 by the author.