그 동안 다소 소홀했던 MongoDB에 대해 좀 더 공부해보기 위해 정리해본다.

MongoDB Getting Started

MongoDB는 Database -> Collection -> Document로 이루어져있다. RDB로 따지면 Schema -> Table -> Row 정도로 해석될 것 같다. JSON과 유사한 형식으로 저장되기 때문에 데이터 처리가 굉장히 용이하다는 장점을 가지고 있다.

use examples

db.inventory.insertMany([
   { item: "journal", qty: 25, status: "A", size: { h: 14, w: 21, uom: "cm" }, tags: [ "blank", "red" ] },
   { item: "notebook", qty: 50, status: "A", size: { h: 8.5, w: 11, uom: "in" }, tags: [ "red", "blank" ] },
   { item: "paper", qty: 10, status: "D", size: { h: 8.5, w: 11, uom: "in" }, tags: [ "red", "blank", "plain" ] },
   { item: "planner", qty: 0, status: "D", size: { h: 22.85, w: 30, uom: "cm" }, tags: [ "blank", "red" ] },
   { item: "postcard", qty: 45, status: "A", size: { h: 10, w: 15.25, uom: "cm" }, tags: [ "blue" ] }
]);

db.inventory.find({})
db.inventory.find( { tags: "red" } )
db.inventory.find( { size: { h: 14, w: 21, uom: "cm" } } )

위와 같이 현재 지정된 db examples에 collection inventory를 생성한 뒤, 아래 쿼리에 해당하는 documents 들을 저장한다. 내부에 nesting된 항목들에 대해서도 검색이 가능하며, 아래와 같이 array 내부에 있는 특정 요소에 대해서 역시 검색이 가능하다.

db.inventory.find( { tags: "red" } )

DB의 결과값 중 반환할 값만을 지정하여 반환할 수 있다. 아래와 같이, find 명령어 뒤에 조건을 넣음으로써 MySQL의 SELECT item, status FROM examples와 같은 효과를 도출해낼 수 있다.

db.inventory.find( { }, { item: 1, status: 1 } );

여기서 _id 필드가 자동으로 반환이 되는데, 별도로 해당 필드에 0 숫자를 할당하면 반환하지 않을 수 있다.

Databases & Collections

Database나 Collection은 존재하지 않는 경우 MongoDB가 첫 번째 데이터 저장시에 생성을 같이 한다.

use myNewDB

db.myNewCollection1.insertOne( { x: 1 } )

이 상황에서는, insertOne() 호출시에 동시에 존재하지 않는 myNewDBmyNewCollection1을 생성한다.

  • db.createCollection()와 같은 메서드를 사용하면 명시적으로 만들 수 있기는 하다

Document Validation

일반적으로 Collection은 Document들이 동일한 스키마를 가지는 것을 요구하지 않는다. 다만 Document 검증 규칙을 적용할 수 있다.

Document Validation Rule

Documents

MongoDB는 BSON Document 형식으로 데이터를 저장한다. JSON과 비슷하긴 한데, 수용하는 데이터 타입이 더 많다.

예를들어, 기본으로 추가되는 _id 필드의 경우 ObjectId 데이터 타입을 갖는다. ObjectId는 12 bytes로 이루어진 유니크한 값이다.

구성은 (4-bytes unix timestamp) + (5-bytes random value) + (3-bytes counter, 랜덤값)으로 이루어져있다.

이외에도 Double, Binary data, Regular expression 등 기존 JSON에서 사용할 수 없었던 데이터 타입들을 사용할 수 있다.

Data Types

MongoDB CRUD

Create Operation

collection에 새로운 document를 추가하는 작업을 진행한다. MongoDB에서 insert 명령은 하나의 collection을 대상으로 한다. MongoDB에서 모든 쓰기 작업들은 하나의 Document에 대해 원자성을 유지한다. (All write operations in MongoDB are atomic on the level of a single document.)

db.collection.insertOne();
db.collection.insertMany();

insert*() 명령을 사용하여 값을 생성한 경우 _id 필드 값이 추가가 된다.

Read Operation

읽기 작업은 collection으로부터 documents를 가져오는 작업을 진행한다. find()와 같은 항목을 사용할 수 있다.

db.users.find(
  { age: { $gt: 18 }}, # 검색 쿼리
  { name: 1, address: 1} # 가져올 칼럼
)

NodeJS의 경우, find()의 결과물은 Cursor 객체를 반환한다. 조건부로 가져올 경우, 해당 조건을 쿼리 연산자를 이용하여 조건을 지정할 수 있다.

const cursor = db.collection('inventory').find({
  status: { $in: ['A', 'D'] }
});

Query Operators

쿼리 연산자에는 다음과 같은 목록이 있다.

Query and Projection Operators

  • $eq: equals to (반대는 $ne)
  • $gt: greater than (반대는 $lt)
  • $gte: greater than or equal to
  • $in: 값이 행렬 안에 있는지 확인 (반대는 $nin)

  • $and / $not / $nor / $or: 논리 연산자
  • $exists: 필드의 존재 유무 (boolean 값을 넘기면된다). 아래 코드는, qty 필드가 존재하며, 5와 15가 아닌 값을 반환한다.
db.inventory.find({ qty: { $exists: true, $nin: [5, 15] } });
  • $type: BSON type을 넘겨서 매치되는 항목을 반환한다.

  • $expr / $regex / $text / $where: 정규식과 같은 방법으로 검색되는 경우

  • $jsonSchema: JSON Schema에 매칭되는 도큐먼트를 반환한다. 해당 값을 통해 스키마 검증을 진행할 수 있다. 내용이 길다보니 도큐먼트 참조 필요.

MongoDB $jsonSchema docs

JSON Schema

JSON Schema Draft

JSON Schema는 JSON data의 구조를 정의하는 JSON 포멧으로 “application/schema+json” 미디어타입에 해당한다. JSON Schema는 검증, 도큐멘테이션, 하이퍼링크 네비게이션, 그리고 JSON data에 대하여 상호작용을 위해 의도되었다.

검증을 위한 키워드는 아래 링크에서 확인이 가능하다

MongoDB JSON Schema available keywords

{ $jsonSchema: <JSON Schema Object> }

{
  $jsonSchema: {
    required: [ "name", "major", "gpa", "address" ],
    properties: {
      name: {
        bsonType: "string",
        description: "must be a string and is required"
      },
      year: {
        bsonType: "int",
        minimum: 2017,
        maximum: 3017,
        description: "must be an integer in between [ 2017, 3017 ] and is required"
      },
      major: {
        enum: [ "Math", "English", "Computer Science", "History", null ],
        description: "can only use one of the enum values and is required"
      },
      gpa: {
        bsonType: [ "double" ],
        description: "must be a double if value exists"
      },
      address: {
        bsonType: "object",
        required: [ "city" ],
        properties: {
          street: {
            bsonType: "string",
            description: "must be a string if the field exists"
          },
          city: {
            bsonType: "string",
            description: "must be a string and is required"
          }
        }
      }
    }
  }
}

위의 코드에서 볼 수 있듯이, bsonType 항목을 지정해주면, 해당 값만 받게 된다. “string”으로 설정되면 문자열만 들어가게 되고, [ “double” ]과 같은 경우는 Typescript union type과 유사하다고 생각하면 될듯하다. 이 안에 “int”도 넣게 되면 동시에 “int” 타입도 “받을 수는 있다”. 다만 이런식으로 할거면 굳이 스키마 만들 필요 없으니 그냥 하나만 쓰도록 하자.

또한, 스키마 객체 안에 bsonType: "object"가 있으면 또 한 번 네스팅을 할 수 있다.

일반적으로 type 키워드와 bsonType 키워드는 거의 동일한데, MongoDB판은 “integer” 타입을 지원하지 않기 때문에, bsonType: "int" 혹은 bsonType: "long"을 사용해줘야 한다.

  • 그러면 NodeJS driver에서는 어떻게 사용해야할까?: 간단하다. MongoDB 패키지에 있는 Long이나 Int를 가져다가 사용하면 된다.
import { Long } from "mongodb";

let seq = this.db.collection("seq");
seq.insertOne({
  value: Long.fromInt(1);
}, () => {});

Update Operation

기존에 collection에 존재하는 document(들)을 수정한다.

db.collection.updateOne();
db.collection.updateMany();
db.collection.replaceOne();

Create 작업과 동일하게 MongoDB에 있는 모든 쓰기 작업들은 single document 단계에서 원자성을 갖는다.

db.users.updateMany(
  { age: { $lt: 18 }},  # update filter
  { $set: { status: "reject" }} # update action
)

여기에 있는 $set과 같은 연산자들은 필드가 존재하지 않는 경우 필드 생성을 병행한다.

MongoDB $set keyword

{
  "_id": 100,
  "sku": "abc123",
  "quantity": 250,
  "instock": true,
  "reorder": false,
  "details": { "model": "14Q2", "make": "xyz" },
  "tags": ["apparel", "clothing"],
  "ratings": [{ "by": "ijk", "rating": 4 }]
}

위와 같은 도큐먼트가 있다고 가정하였을 때, 아래와 같이 최상단에 존재하는 필드들의 값을 수정한다고 가정해보자.

db.products.update(
  { _id: 100 },
  {
    $set: {
      quantity: 500,
      details: { model: '14Q3', make: 'xyz' },
      tags: ['coats', 'outerwear', 'clothing']
    }
  }
)

이렇게 될 경우, quantitydetails, tags 필드의 값들이 모두 새 값으로 변경된다.

도큐먼트 내에 내재되어 있는 도큐먼트나 array를 특정하여 수정하고자 하는 경우, dot notation을 사용한다.

예를들어, 위의 details 도큐먼트 내의 make 필드를 수정하고자 하는 경우, 아래와 같이 점으로 구분된 문자열 값을 전달한다.

db.products.update(
   { _id: 100 },
   { $set: { "details.make": "zzz" } }
)

이와 더불어 array 내의 특정 요소 값을 수정하고자 하는 경우, 번호를 지정해줄 수 있다.

db.products.update(
   { _id: 100 },
   { $set:
      {
        "tags.1": "rain gear",
        "ratings.0.rating": 2
      }
   }
)

Delete Operation

말 그대로 documentcollection으로부터 제거한다.

db.collection.deleteOne();
db.collection.deleteMany();

db.users.deleteMany(
  { status: "reject" } # delete filter
)

Aggregation (집합)

집합 연산은 데이터 레코드를 제작하고, 계산된 결과를 반환한다. MySQL의 GROUP_BY와 같은 역할을 해준다고 볼 수도 있을듯하다.

Aggregation Pipeline

이 방법은 도큐먼트들로 하여금 다단계의 파이프라인을 통해 종합한 결과로 도출해내도록 한다.

db.orders.aggregate([
   { $match: { status: "A" } },
   { $group: { _id: "$cust_id", total: { $sum: "$amount" } } }
])
  1. $match 키워드는 status 필드의 값이 “A”인 도큐먼트들을 색인한 다음, 다음 번 스테이지로 전달한다.
  2. $group 키워드는 1번의 결과값 중에서 _id값을 $cust_id로 묶고 (여기서 해당 id 앞에 달러 표시가 붙은 것을 유의하자), 이에 해당하는 묶인 total을 $sum으로 묶어준다.

Aggregation pipeline operators

Map-Reduce

향후에 공부하는 것으로

Single Purpose Aggregation Operations

db.collection.estimatedDocumentCount() db.collection.count() db.collection.distinct()

이 연산자들은 한 개의 컬렉션으로부터 도큐먼트들을 집합하는 역할을 한다. 일반적으로 하나의 목적을 가지고 사용된다.