---
# System prepended metadata

title: 상황별 몽고디비 모델링
tags: [tech sharing]

---

# 상황별 몽고디비 모델링

## 1:1 관계

### Embedded Document Pattern
하나의 document가 다른 하나를 포함하는 관계일 때 사용한다.
```javascript=
{
   _id: "joe",
   name: "Joe Bookreader",
   address: {
              street: "123 Fake Street",
              city: "Faketon",
              state: "MA",
              zip: "12345"
            }
}
```

### Subset Pattern
embedded document pattern의 잠재적인 문제점은 어플리케이션이 필요로 하지 않는 필드들을 포함해 document 크기가 커진다는 것이다. 대신에 subset pattern을 사용해서 한 데이터베이스의 call에서 가장 많이 접근 되는 subset을 가져오도록 하는 방법을 쓸 수 있다.

먄약 movie collection이 다음과 같을 때
```javascript=
{
  "_id": 1,
  "title": "The Arrival of a Train",
  "year": 1896,
  "runtime": 1,
  "released": ISODate("01-25-1896"),
  "poster": "http://ia.media-imdb.com/images/M/MV5BMjEyNDk5MDYzOV5BMl5BanBnXkFtZTgwNjIxMTEwMzE@._V1_SX300.jpg",
  "plot": "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, ...",
  "fullplot": "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, the line dissolves. The doors of the railway-cars open, and people on the platform help passengers to get off.",
  "lastupdated": ISODate("2015-08-15T10:06:53"),
  "type": "movie",
  "directors": [ "Auguste Lumière", "Louis Lumière" ],
  "imdb": {
    "rating": 7.3,
    "votes": 5043,
    "id": 12
  },
  "countries": [ "France" ],
  "genres": [ "Documentary", "Short" ],
  "tomatoes": {
    "viewer": {
      "rating": 3.7,
      "numReviews": 59
    },
    "lastUpdated": ISODate("2020-01-09T00:02:53")
  }
}
```

여기에는 simple overview에는 필요가 없는 정보들이 포함되어 있다. 이런 정보들을 하나의 collection에 다 모아놓기 보다는 collection을 나눠놓을 수 있다.

```javascript=
// movie collection

{
  "_id": 1,
  "title": "The Arrival of a Train",
  "year": 1896,
  "runtime": 1,
  "released": ISODate("1896-01-25"),
  "type": "movie",
  "directors": [ "Auguste Lumière", "Louis Lumière" ],
  "countries": [ "France" ],
  "genres": [ "Documentary", "Short" ],
}

// movie_details collection

{
  "_id": 156,
  "movie_id": 1, // reference to the movie collection
  "poster": "http://ia.media-imdb.com/images/M/MV5BMjEyNDk5MDYzOV5BMl5BanBnXkFtZTgwNjIxMTEwMzE@._V1_SX300.jpg",
  "plot": "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, ...",
  "fullplot": "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, the line dissolves. The doors of the railway-cars open, and people on the platform help passengers to get off.",
  "lastupdated": ISODate("2015-08-15T10:06:53"),
  "imdb": {
    "rating": 7.3,
    "votes": 5043,
    "id": 12
  },
  "tomatoes": {
    "viewer": {
      "rating": 3.7,
      "numReviews": 59
    },
    "lastUpdated": ISODate("2020-01-29T00:02:53")
  }
}
```

이렇게 나누는 것이 read 성능을 높인다. 대부분의 request를 충족하는 data의 크기가 작아지기 때문이다.

### subset pattern의 trade off

subset pattern을 사용하면 전체적인 working set의 크기를 줄인다. 또한 읽기 성능을 늘린다.

하지만 만약 적절하지 못하게 데이터를 여러개의 collection으로 나누면, 어플리케이션이 데이터를 가져올 때 multiple trip을 해야할 수도 있다. 

# 1:many 관계

[참고링크](https://docs.mongodb.com/manual/tutorial/model-embedded-one-to-many-relationships-between-documents/)

한명의 patron이 있고 여러개의 주소와 관계가 있을 때를 생각해보자. 하나의 context 속에서 다른 entity를 봐야할 때 embedding이 좋다.

```javascript=
{
   "_id": "joe",
   "name": "Joe Bookreader",
   "addresses": [
                {
                  "street": "123 Fake Street",
                  "city": "Faketon",
                  "state": "MA",
                  "zip": "12345"
                },
                {
                  "street": "1 Some Other Street",
                  "city": "Boston",
                  "state": "MA",
                  "zip": "12345"
                }
              ]
 }
```

## subset pattern
e-commerce 사이트에서 상품에 대한 리뷰 데이터를 갖고 있다고 하자.
```javascript=
{
  "_id": 1,
  "name": "Super Widget",
  "description": "This is the most useful item in your toolbox.",
  "price": { "value": NumberDecimal("119.99"), "currency": "USD" },
  "reviews": [
    {
      "review_id": 786,
      "review_author": "Kristina",
      "review_text": "This is indeed an amazing widget.",
      "published_date": ISODate("2019-02-18")
    },
    {
      "review_id": 785,
      "review_author": "Trina",
      "review_text": "Nice product. Slow shipping.",
      "published_date": ISODate("2019-02-17")
    },
    ...
    {
      "review_id": 1,
      "review_author": "Hans",
      "review_text": "Meh, it's okay.",
      "published_date": ISODate("2017-12-06")
    }
  ]
}

```

리뷰는 생성된 시간의 반대로 정렬되어 있다. 사용자가 상품 페이지에 접근했을 때 최근 10개의 리뷰를 가져온다.

모든 리뷰를 product와 함게 저장하지 않고 두 개의 collection으로 나눌 수 있다.

product collection에는 10개의 최신 리뷰만 가지고 있다.
```javascript=
{
  "_id": 1,
  "name": "Super Widget",
  "description": "This is the most useful item in your toolbox.",
  "price": { "value": NumberDecimal("119.99"), "currency": "USD" },
  "reviews": [
    {
      "review_id": 786,
      "review_author": "Kristina",
      "review_text": "This is indeed an amazing widget.",
      "published_date": ISODate("2019-02-18")
    }
    ...
    {
      "review_id": 776,
      "review_author": "Pablo",
      "review_text": "Amazing!",
      "published_date": ISODate("2019-02-16")
    }
  ]
}
```

review collection에는 모든 리뷰가 저장되어 있다.
```
{
  "review_id": 786,
  "product_id": 1,
  "review_author": "Kristina",
  "review_text": "This is indeed an amazing widget.",
  "published_date": ISODate("2019-02-18")
}
{
  "review_id": 785,
  "product_id": 1,
  "review_author": "Trina",
  "review_text": "Nice product. Slow shipping.",
  "published_date": ISODate("2019-02-17")
}
...
{
  "review_id": 1,
  "product_id": 1,
  "review_author": "Hans",
  "review_text": "Meh, it's okay.",
  "published_date": ISODate("2017-12-06")
}
```

### subset pattern의 trade off
data 복제가 생길 수 있다. 예를들어, 리뷰가 product collection과 reivews collection 모두에 있을 수 있다. 두 collection 사이에 데이터 일관성이 있도록 처리를 해주어야한다.

또한 product collection이 가장 최근의 collection이 되도록 유지해주어야한다.

## 1:many with references

[참고링크](https://docs.mongodb.com/manual/tutorial/model-referenced-one-to-many-relationships-between-documents/)

출판사와 책의 관계를 생각해보자 publisher 정보의 반복을 피하기 위해서 referencing이 좋다.

embedding 방식을 쓰면 다음과 같이 된다.
```javascript=
{
   title: "MongoDB: The Definitive Guide",
   author: [ "Kristina Chodorow", "Mike Dirolf" ],
   published_date: ISODate("2010-09-24"),
   pages: 216,
   language: "English",

   publisher: {

              name: "O'Reilly Media",

              founded: 1980,

              location: "CA"

            }

}

{
   title: "50 Tips and Tricks for MongoDB Developer",
   author: "Kristina Chodorow",
   published_date: ISODate("2011-05-06"),
   pages: 68,
   language: "English",

   publisher: {

              name: "O'Reilly Media",

              founded: 1980,

              location: "CA"

            }

}
```

publisher 데이터의 중복을 줄이기 위해서 reference를 쓴다.

reference 방식을 쓸 때 관계의 증가가 어디에 reference를 저장할지를 결정한다. 만약 한 출판사에 대한 책의 수가 작고 증가 폭이 제한되어 있다면 출판사 document 안에 책 reference를 넣는 것이 효율적이다.

하지만 출판사에 대한 책의 수가 크고 제한이 없다면 다음과 같이 mutable, growing array가 된다.
```javascript=
{
   name: "O'Reilly Media",
   founded: 1980,
   location: "CA",

   books: [123456789, 234567890, ...]

}

{
    _id: 123456789,
    title: "MongoDB: The Definitive Guide",
    author: [ "Kristina Chodorow", "Mike Dirolf" ],
    published_date: ISODate("2010-09-24"),
    pages: 216,
    language: "English"
}

{
   _id: 234567890,
   title: "50 Tips and Tricks for MongoDB Developer",
   author: "Kristina Chodorow",
   published_date: ISODate("2011-05-06"),
   pages: 68,
   language: "English"
}
```

> 숫자가 제한이 되어있지 않은 array는 왜 안 좋은가?

document의 사이즈가 예상하지 못한 사이즈로 커질 수 있다. 배열이 계속해서 증가하면 array에 index를 지정하는 것이 점점 성능이 나빠질 수 있다. 나중에는 BSON document size 한계를 넘을 수 있다. 

[참고](https://docs.atlas.mongodb.com/schema-suggestions/avoid-unbounded-arrays/)
[참고: blog와 growing comments](https://stackoverflow.com/questions/9306815/mongodb-performance-with-growing-data-structure)

mutable하고 growing array를 피하기 위해 출판사 reference를 책 document 안에 저장한다.
```javascript=
// publisher
{
   _id: "oreilly",
   name: "O'Reilly Media",
   founded: 1980,
   location: "CA"
}

//book

{
   _id: 123456789,
   title: "MongoDB: The Definitive Guide",
   author: [ "Kristina Chodorow", "Mike Dirolf" ],
   published_date: ISODate("2010-09-24"),
   pages: 216,
   language: "English",

   publisher_id: "oreilly"

}

{
   _id: 234567890,
   title: "50 Tips and Tricks for MongoDB Developer",
   author: "Kristina Chodorow",
   published_date: ISODate("2011-05-06"),
   pages: 68,
   language: "English",

   publisher_id: "oreilly"

}
```

## keyword 검색이 가능한 데이터 모델

[참고링크](https://docs.mongodb.com/manual/tutorial/model-data-for-keyword-search/)

keyword가 document에 저장이 되어있는 경우에 사용한다.

keyword base query를 지원하기 위한 구조를 추가하기 위해서 array field를 만들고 문자열을 array에 넣어놓는다. 그리고 array에 multi-key index를 추가할 수 있다.

```javascript=
{ title : "Moby-Dick" ,
  author : "Herman Melville" ,
  published : 1851 ,
  ISBN : 0451526996 ,
  topics : [ "whaling" , "allegory" , "revenge" , "American" ,
    "novel" , "nautical" , "voyage" , "Cape Cod" ]
}
```

```javascript=
db.volumes.createIndex( { topics: 1 } )
db.volumes.findOne( { topics : "voyage" }, { title: 1 } )
```




```
users {
    name,
    id,
    level,
    address,
    image // url
    ratings: [1,23,4,5,3,2,65,3]; // 평
}

~~sale_list~~ {
    user_id, 
    post_id
}

~~buy_list~~ {
    user_id,
    post_id
}

// sale list와 buy list 를 하나의 거래완료 collection으로 나타낼 수 있다.

buy_request_list {
    user_id,
    post_id
}

~~user_ratings:~~ {
    user_id, //평가 당한 사람
    user_id, // 평가 한 사람
    post_id,
}

// 게시물당 하나의 평가만 할 수 있기 때문에 post안으로 평가를 했는지를 나타내는 것을 넣어도 될 것 같다.

~~user_rating_histories~~: {
    user_id, // 평가당한 사람 id
    histories: [1,23,4,5,3,2,65,3]; // 평
}

// rating 정보를 유저 안으로 넣어도 될 것 같다. 크기가 크지 않고 고정 크기의 배열이라서.
=====
user_post: {
    user_id, // 판매자
    user_id, // 구매자
    post_id: // 게시글
}

// 요청을 따로 하는 경우도 많이 있어서 collection을 분리하는 것이 더 좋을 것 같다.

// buyer request 리스트가 따로 있으면 writer, buyer, post가 1:1 관계가 되어서 따로 분리할 필요가 없을 것 같아서 post 안에 넣어주는 것이 좋을 것 같다.



categories {
    name: string
}

post {
    writer: user_id,
    buyer_id: user_id
    title : string,
    content : string,
    images: [url: string], // 개수 제한
    category: string,
    ~~recent_comments~~ : [] // 최근 10개. 최근 댓글로 업데이트 하는 문제. 여러 사람이 동시에 댓글을 작성할 때 lock이 걸려서 post 읽기가 느려지는 문제.
    ~~state~~ : string, // 구매가 되었는지를 나타내는 상태 bool으로 하면 예약 중 상태를 표현할 수 없어서 확장성을 고려해서 string으로
    cost: number,
    uploadTime, // createdAt으로 자동으로 생성되면 안 써도 된다
}

post_comments {
    post_id,
    writer_id,
    content
}

```

## 궁금한점
#### 1. buy list와 sale list를 users 안에 넣는 것이 좋을까?
```
users {
    name,
    id,
    level,
    address,
    image // url
    ratings: [1,23,4,5,3,2,65,3]; // 평
}

sale_list {
    user_id, 
    post_id
}

buy_list {
    user_id,
    post_id
}
```

- 장점
join을 없애서 읽기 성능을 높일 수 있다.

- 단점
sale list와 buy list의 크기가 계속해서 커져서 document 크기가 커질 수 있다.


답변 : 거래 완료내역만 추가되는 별개의 테이블이 있으면 어떨까요?
      no sql이 느리다곤 하지만 join을 없애서 읽기 성능이 얼마만큼 좋아지는지… 의문입니다 ㅎㅎ
      
- sale list, buy list를 합쳐서 collection을 만들고 index를 해놓으면 읽기를 하는 속도가 빨라질 수 있을 것 같다.
- 그런데 buy request list는 삭제를 많이 해야하는데 이 때 index가 있으면 삭제 성능이 나빠지기 때문에 buy request list는 따로 두는 것이 좋을 것 같다.


#### 2. recent comments를 post 안에 넣는 것이 좋을까?
```
post {
    writer: user_id,
    title : string,
    content : string,
    images: [url: string], // 개수 제한
    category: string,
    recent_comments : [] // 최근 10개. 최근 댓글로 업데이트 하는 문제. 여러 사람이 동시에 댓글을 작성할 때 lock이 걸려서 post 읽기가 느려지는 문제.
    state : string, // 구매가 되었는지를 나타내는 상태 bool으로 하면 예약 중 상태를 표현할 수 없어서 확장성을 고려해서 string으로
    cost: number,
    uploadTime, // createdAt으로 자동으로 생성되면 안 써도 된다.
    
}

post_comments {
    post_id,
    writer_id,
    content
}

```
- 장점
post를 읽을 때 최근 댓글을 빠르게 읽을 수 있다.

- 단점
댓글을 여러명이 한번에 작성하면 document에 lock이 걸려서 해당 document의 읽기 성능이 나빠진다.
검색했을 때 가져오는 post data에 댓글 데이터는 포함이 안돼도 된다.
최신 댓글 10개를 유지하는 문제가 있다.



###### tags: `tech sharing`