개인 개발 프로젝트/Graphql, MongoDB 실습

[Graphql, MongoDB 실습] 9. N:M 관계

종범2 2020. 8. 9. 13:55

N:M 관계

MongoDB를 이용할 때 N:M 관계를 구현하는 방법을 설명하겠다. 간단한 예시를 들어 설명하겠다. Post라는 collection이 있고 Tag라는 collection이 있을 때, Post와 Tag가 N:M으로 존재해야 하는 예시이다.

 

Post에 Tag의 Id를 배열로 저장하는 방법 vs Tag에 Post의 Id를 배열로 저장하는 방법

// Post
{
  _id : 1,
  name : '음식 게시물',
  tagIds : [1, 2]
}
// Tag
{
  _id : 1,
  name : '일상'
}
{
  _id : 2,
  name : '맛집'
}

전자의 예시는 다음과 같다. Post에 TagIds 속성이 존재하고, 그 속성에는 Post에서 사용하는 Tag의 Id를 배열로 저장한다.

// Post
{
  _id : 1,
  name : '음식 게시물',
}
// Tag
{
  _id : 1,
  name : '일상',
  postIds : [1]
}
{
  _id : 2,
  name : '맛집',
  postIds : [1]
}

후자의 예시는 다음과 같다. Tag에 PostIds 속성이 존재하고, 그 속성에는 Tag를 사용한 Post의 Id를 배열로 저장한다. N:M 관계를 구현할 때에는 Post에 Tag가 많은지, Tag를 사용한 Post가 많은 지 살펴봐야 한다. 개인적인 생각으로는 Post는 Tag를 일부 사용하지만, Tag는 수많은 Post에서 사용할 수 있다. 많은 배열을 저장하는 방법은 성능과 리소스 측면에서 좋지 않기 때문에 이 예제에서는 전자의 경우로 구현해야 한다.

 

models/post.js

const mongoose = require('mongoose');
const { Schema } = mongoose;
const postSchema = new Schema({
  name: {
    type: String,
    required: true
  },
  tagIds: [{
    type:Schema.Types.ObjectId,
    ref:'Tag'
  }],
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

module.exports = mongoose.model('Post', postSchema);

models/tag.js

const mongoose = require('mongoose');
const { Schema } = mongoose;
const tagSchema = new Schema({
  name: {
    type: String,
    required: true
  },
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

module.exports = mongoose.model('Tag', tagSchema);

우선 mongoose를 이용하여 스키마를 정의한다. 위에서 설명한 대로 Post에는 tagIds라는 속성이 있고 Tag의 ID를 배열로 저장한다.

 

graphql/schema/index.js

const { gql } = require('apollo-server');
const typeDefs = gql`
  type Query {
    posts: [Post]
    tags: [Tag]
  }
  type Post{
    _id: String
    name: String
    tags:[Tag]
    createdAt: String
  }
  input PostInput{
    name: String
  }
  input PostTagInput{
    postId: String
    tagId: String
  }
  type Tag{
    _id: String
    name: String
    posts:[Post]
    createdAt: String
  }
  input TagInput{
    name: String
  }
  type Mutation{
    createPost(postInput: PostInput): Post!
    addTag(postTagInput: PostTagInput):Post!
    createTag(tagInput: TagInput):Tag!
  }
`;

module.exports = typeDefs;

Query, Post, Tag, Mutation 타입을 정의하고 PostInput, PostTagInput, TagInput 인풋을 정의한다.

 

graphql/resolvers/index.js

const Post = require('../../models/post');
const Tag = require('../../models/tag');
const resolvers = {
  Query: {
    async posts(_, args) {
      try {
        const posts = await Post.find();
        return posts;
      } catch (err) {
        console.log(err);
        throw err;
      }
    },
    async tags(_, args){
      try {
        const tags = await Tag.find();
        return tags;
      } catch (err) {
        console.log(err);
        throw err;
      }
    },
  },
  Post: {
    async tags(_, args) {
      const tags = await Tag.find({ _id: { $in: _.tagIds } })
      return tags
    },
  },
  Tag: {
    async posts(_,args){
      const posts = await Post.find({ tagIds: { $in: _._id } })
      return posts
    },
  },
  Mutation: {
    async createPost(_, args) {
      try {
        const post = new Post({
          ...args.postInput
        })
        const result = await post.save();
        return result;
      } catch (err) {
        console.log(err);
        throw err;
      }
    },
    async addTag(_, args) {
      try {
        const result = await Post.findByIdAndUpdate(args.postTagInput.postId,
          { $push: { tagIds: args.postTagInput.tagId } },
          { useFindAndModify: false }
        );
        return result;
      } catch (err) {
        console.log(err);
        throw err;
      }
    },
    async createTag(_, args){
      try {
        const tag = new Tag({
          ...args.tagInput
        })
        const result = await tag.save();
        return result;
      } catch (err) {
        console.log(err);
        throw err;
      }
    },
  }
};

module.exports = resolvers; 

Query의 posts 함수는 모든 Post를 반환하고, tags 함수는 모든 Tag를 반환한다. Post Type에 대한 요청은 Post에서 담당하고 Tag Type에 대한 요청은 Tag에서 담당한다. 이때 Post의 Tag를 요청할 때 Tag의 Id를 배열을 반환하지 않고 Id로 조회한 Tag를 배열로 반환해야 한다. 이를 위해 Post의 tags 함수에 별도의 로직을 작성한다. 마찬가지로 Tag의 Post를 요청할 때 Tag의 Post를 배열로 반환해야한다. 이를 위해 Tag의 posts 함수에 별도의 로직을 작성한다.

 

Mutation에는 세 가지 함수가 있다. createPost는 Post를 생성하고 createTag는 Tag를 생성한다. addTag는 Post의 Id와 Tag의 Id를 인자로 받아 해당하는 Post의 tagIds에 Tag의 Id를 추가한다.

 

Playground 실행

애플리케이션을 실행하고 playground에서 우선 post와 tag를 생성한다

mutation{
  createPost(postInput:{
    name:"음식 게시물"
  }){
    _id
    name
    createdAt
	}
}

mutation{
  createTag(tagInput:{
    name:"일상"
  }){
    _id
    name
    createdAt
  }
}

mutation{
  createTag(tagInput:{
    name:"맛집"
  }){
    _id
    name
    createdAt
  }
}

다음으로는 생성한 두 개의 Tag를 Post에 추가한다.

mutation{
  addTag(postTagInput:{
    postId:"5f2f7fee8bffb80e844b78c8"
    tagId:"5f2f802c8bffb80e844b78c9"
  }){
    _id
    name
    createdAt
  }
}

mutation{
  addTag(postTagInput:{
    postId:"5f2f7fee8bffb80e844b78c8"
    tagId:"5f2f80458bffb80e844b78ca"
  }){
    _id
    name
    createdAt
  }
}

이제 생성된 Post 정보를 조회한다.

query{
  posts{
    _id
    name
    tags{
      _id
      name
    }
  }
}