[MongoDB] 도큐먼트 갱신

참조

  • 몽고DB 완벽가이드 3판


1.1 도큐먼트 치환

  • replaceOne은 도큐먼트를 새로운 것으로 완전치 치환해주는 함수입니다.
  • 이는 대대적인 스키마 마이그레이션에 유용합니다.
  • 예를 들어 사용자 도큐먼트를 다음과 같이 큰 규모로 변경한다고 가정합니다.
use('jbh_MongoDB');

db.users.insertOne({"name" : "joe", "friends" : 32, "enemies" : 2});
{
  "_id": {
    "$oid": "6180fd8ec9fdfd8cd0f7d657"
  },
  "name": "joe",
  "friends": 32,
  "enemies": 2
}
  • "friends" 와 "enemies" 필드를 "relationships" 라는 서브도큐먼트로 옮겨 보겠습니다.
use('jbh_MongoDB');

var joe = db.users.findOne({"name" : "joe"});

joe.relationships = {"friends" : joe.friends, "enemies" : joe.enemies};

joe.username = joe.name;

delete joe.friends;

delete joe.enemies;

delete joe.name;

db.users.replaceOne({"name" : "joe"}, joe);
{
  "_id": {
    "$oid": "6180fd8ec9fdfd8cd0f7d657"
  },
  "relationships": {
    "friends": 32,
    "enemies": 2
  },
  "username": "joe"
}


  • 흔히 하는 실수로, 조건절로 2개 이상의 도큐먼트가 일치되게 한 후 두 번째 매개변수로 중복된 "_id" 값을 갖는 도큐먼트를 생성하는 경우가 있습니다.
  • 이때 데이터베이스는 오류를 반환하고 아무것도 변경하지 않습니다.
use('jbh_MongoDB');

db.users.drop();

db.people.insertOne({"name" : "joe", "age" : 65});
db.people.insertOne({"name" : "joe", "age" : 20});
db.people.insertOne({"name" : "joe", "age" : 49});

joe = db.people.findOne({"name" : "joe", "age" : 20});

joe.age++;
db.people.replaceOne({"name" : "joe"}, joe);
After applying the update, the (immutable) field '_id' was found to have been altered to _id: ObjectId('6180ffea2ae980e6008e8697')
  • 위와 같이 갱신을 요청하면 데이터베이스는 {"name" : "joe"} 와 일치하는 도큐먼트를 찾습니다.
  • 첫 번째로 65세 joe 를 발견하고 치환하려는 도큐먼트와 같은 "_id" 를 갖는 도큐먼트가 이미 컬렉션에 있음에도 현재 joe 변수 내 도큐먼트를 치환하려고 시도합니다.
  • 그런데 "_id" 값은 고유해야 하므로 갱신에 실패하게 됩니다.
  • 위와 같은 상황을 피하려면 "_id" 키로 일치하는 고유한 도큐먼트를 찾는 방법처럼, 고유한 도큐먼트를 갱신 대상으로 지정하는 것이 좋습니다.
use('jbh_MongoDB');

db.users.drop();

db.people.insertOne({"name" : "joe", "age" : 65});
db.people.insertOne({"name" : "joe", "age" : 20});
db.people.insertOne({"name" : "joe", "age" : 49});

joe = db.people.findOne({"name" : "joe", "age" : 20});

joe.age++;
db.people.replaceOne({"_id" : ObjectId("6180ffea2ae980e6008e8697")}, joe);
{
  "acknowledged": true,
  "insertedId": null,
  "matchedCount": 1,
  "modifiedCount": 1,
  "upsertedCount": 0
}

{
  "_id": {
    "$oid": "6180ffea2ae980e6008e8697"
  },
  "name": "joe",
  "age": 21
}


1.2 갱신 연산자

  • 일반적으로 도큐먼트의 특정 부분만 갱신하는 경우가 많습니다.
  • 부분 갱신에는 원자적 갱신 연산자를 사용합니다.
  • 갱신 연산자는 키를 변경, 추가, 제거하고, 심지어 배열과 내장 도큐먼트를 조작하는 복잡한 갱신 연산을 지정하는 데 사용하는 특수키 입니다.
  • 아래 예제는, 누군가 페이지를 방문할 때마다 카운터가 증가하는 할 예제 입니다.
use('jbh_MongoDB');

db.people.drop();

db.analytics.insertOne({"url" : "www.example.com", "pageviews" : 52});
{
  "_id": {
    "$oid": "618101cd9c9804fd1d2cfb30"
  },
  "url": "www.example.com",
  "pageviews": 52
}
  • 누군가 페이지를 방문할 때마다 URL로 페이지를 찾고 "pageviews" 키의 값을 증가시키려면 $inc 제한자를 사용합니다.
use('jbh_MongoDB');

db.people.drop();

db.analytics.insertOne({"url" : "www.example.com", "pageviews" : 52});

db.analytics.updateOne({"url" : "www.example.com"},
                       {"$inc" : {"pageviews" : 1}});
{
  "acknowledged": true,
  "insertedId": null,
  "matchedCount": 1,
  "modifiedCount": 1,
  "upsertedCount": 0
}

{
  "_id": {
    "$oid": "618101cd9c9804fd1d2cfb30"
  },
  "url": "www.example.com",
  "pageviews": 53
}
  • 연산자를 사용할 때 "_id" 값은 변경할 수 없습니다. (변경하려면 도큐먼트 전체를 치환해야 합니다.)
  • 그외 다른 키 값은 모두 변경할 수 있습니다.


  • "$set" 은 필드 값을 설정합니다.
  • 필드가 존재하지 않으면 새 필드가 생성됩니다.
  • 이 기능은 스키마를 갱신하거나 사용자 정의 키를 추가할 때 편리합니다.
use('jbh_MongoDB');

db.analytics.drop();

db.users.insertOne({"name" : "joe", "age" : 30 ,"성별" : "남자", "위치" : "수원"});
{
  "_id": {
    "$oid": "618103bec9b11bf69ecf491a"
  },
  "name": "joe",
  "age": 30,
  "성별": "남자",
  "위치": "수원"
}
  • 위의 예제에서 사용자가 좋아하는 책을 프로필에 추가하려면 "$set" 을 사용하면 됩니다.
use('jbh_MongoDB');

db.users.updateOne({"_id" : ObjectId("6181041dde032de2a7ac0208")},
                   {"$set" : {"favorite book" : "해리포터"}});
{
  "acknowledged": true,
  "insertedId": null,
  "matchedCount": 1,
  "modifiedCount": 1,
  "upsertedCount": 0
}

{
  "_id": {
    "$oid": "6181041dde032de2a7ac0208"
  },
  "name": "joe",
  "age": 30,
  "성별": "남자",
  "위치": "수원",
  "favorite book": "해리포터"
}
  • 사용자가 다른 책을 좋아한다고 하면 "$set" 으로 값을 변경할 수도 있습니다.
use('jbh_MongoDB');

db.users.updateOne({"name" : "joe"},
                   {"$set" : {"favorite book" : "반지의 제왕"}});
{
  "acknowledged": true,
  "insertedId": null,
  "matchedCount": 1,
  "modifiedCount": 1,
  "upsertedCount": 0
}

{
  "_id": {
    "$oid": "6181041dde032de2a7ac0208"
  },
  "name": "joe",
  "age": 30,
  "성별": "남자",
  "위치": "수원",
  "favorite book": "반지의 제왕"
}
  • 사용자가 책을 좋아하지 않는다는 것을 깨닫게 되면 "$unset" 으로 키와 값을 모두 제거할 수 있습니다.
use('jbh_MongoDB');

db.users.updateOne({"name" : "joe"},
                   {"$unset" : {"favorite book" : "반지의 제왕"}});
{
  "acknowledged": true,
  "insertedId": null,
  "matchedCount": 1,
  "modifiedCount": 1,
  "upsertedCount": 0
}

{
  "_id": {
    "$oid": "6181041dde032de2a7ac0208"
  },
  "name": "joe",
  "age": 30,
  "성별": "남자",
  "위치": "수원"
}

  • 키를 추가, 변경, 삭제할 때는 항상 $ 제한자를 사용해야 합니다.
  • 하지만, 초보자가 흔히 범하는 오류가 있습니다.
  • 키 값을 다른 값으로 갱신하게 되면 오류가 발생합니다.
use('jbh_MongoDB');

db.users.updateOne({"name" : "joe"},
                   {"name" : "Kim"});
Update document requires atomic operators

use('jbh_MongoDB');

db.users.updateOne({"name" : "joe"},
                   {"$set" : {"name" : "Kim"}});
{
  "_id": {
    "$oid": "6181041dde032de2a7ac0208"
  },
  "name": "Kim",
  "age": 30,
  "성별": "남자",
  "위치": "수원"
}
  • 이처럼 갱신 도큐먼트는 갱신 연산자를 반드시 포함해서 사용해야 합니다.


  • "$inc" 연산자는 이미 존재하는 키의 값을 변경하거나 새 키를 생성하는데 사용합니다.
  • 분석, 분위기, 투표 등과 같이 자주 변하는 수치 값을 갱신하는데 매우 유용합니다.
  • 아래 예제는 게임을 저장하고 점수를 갱신하는 게임 컬렉션을 생성한다고 가정합니다.
  • 사용자가 핀볼이라는 게임을 시작하면, 게임 이름과 플레이어로 게임을 식별하는 도큐먼트를 하나 삽입합니다.
use('jbh_MongoDB');

db.users.drop();

db.games.insertOne({"game" : "pinball", "user" : "joe"});
  • 게임에서는 공이 범퍼에 부딪치면 플레이어의 점수가 증가합니다.
  • 점수는 꽤 쉽게 얻을 수 있고 기본 단위가 50이라고 가정합니다.
  • 플레이어의 점수에 50을 더하려면 "$inc" 제한자를 사용하면 됩니다.
use('jbh_MongoDB');

db.games.updateOne({"game" : "pinball", "user" : "joe"},
                   {"$inc" : {"score" : 50}});
{
  "acknowledged": true,
  "insertedId": null,
  "matchedCount": 1,
  "modifiedCount": 1,
  "upsertedCount": 0
}

{
  "_id": {
    "$oid": "618108247bd04bf7957cae86"
  },
  "game": "pinball",
  "user": "joe",
  "score": 50
}
  • 처음에 "score" 키는 존재하지 않았지만, "$inc" 제한자에 의해 생성되고 50만큼 증가했습니다.
  • 공이 보너스 슬롯에 들어가면 10000점을 추가한다고 가정합니다.
use('jbh_MongoDB');

db.games.updateOne({"game" : "pinball", "user" : "joe"},
                   {"$inc" : {"score" : 10000}});
{
  "_id": {
    "$oid": "618108247bd04bf7957cae86"
  },
  "game": "pinball",
  "user": "joe",
  "score": 10050
}
  • 기존에 50에서 10000을 더한 10050 으로 값이 증가한 것을 확인할 수 있습니다.
  • "$inc" 는 int, long, double, decimal 타입 값에만 사용할 수 있습니다.
  • null, 불리언, 문자열로 나타낸 숫자와 같이 여러 언어에서 숫자로 자동 변환되는 데이터형의 값에는 사용할 수 없습니다.
use('jbh_MongoDB');

db.games.drop();

db.strcounts.insert({"count" : "1"})

db.strcounts.update({}, {"$inc" : {"count" : 1}});
Cannot apply $inc to a value of non-numeric type. {_id: ObjectId('61810b4ee3b253fbfcd79ecb')} has the field 'count' of non-numeric type string


  • 배열을 다루는 데 갱신 연산자를 사용할 수 있습니다.
  • "$push" 는 배열이 이미 존재하면 배열 끝에 요소를 추가하고, 존재하지 않으면 새로운 배열을 생성합니다.
  • 예를들어, 블로그 게시물에 배열 형태의 "comments" 키를 삽입한다고 가정합니다.
  • 존재하지 않던 "comments" 배열이 생성되고 댓글이 생성되는 것을 볼 수 있습니다.
use('jbh_MongoDB');

db.strcounts.drop();

db.blog.posts.insertOne({"title" : "A blog post", "content" : "..."});

db.blog.posts.updateOne({"title" : "A blog post"},
                        {"$push" : {"comments" :
                                   {"name" : "joe", "email" : "joe@example.com", "content" : "nice post."}}});

db.blog.posts.findOne();
{
  "_id": {
    "$oid": "61810d3efc787aa16eb34fb9"
  },
  "title": "A blog post",
  "content": "...",
  "comments": [
    {
      "name": "joe",
      "email": "joe@example.com",
      "content": "nice post."
    }
  ]
}
  • 댓글을 더 추가하려면 "$push" 를 또 사용하면 됩니다.
use('jbh_MongoDB');

db.strcounts.drop();

db.blog.posts.insertOne({"title" : "A blog post", "content" : "..."});

db.blog.posts.updateOne({"title" : "A blog post"},
                        {"$push" : {"comments" :
                                   {"name" : "joe", "email" : "joe@example.com", "content" : "nice post."}}});

db.blog.posts.updateOne({"title" : "A blog post"},
                        {"$push" : {"comments" :
                                   {"name" : "bob", "email" : "bob@example.com", "content" : "good post."}}});

db.blog.posts.findOne();
{
  "_id": {
    "$oid": "61810d3efc787aa16eb34fb9"
  },
  "title": "A blog post",
  "content": "...",
  "comments": [
    {
      "name": "joe",
      "email": "joe@example.com",
      "content": "nice post."
    },
    {
      "name": "bob",
      "email": "bob@example.com",
      "content": "good post."
    }
  ]
}

  • 더 복잡한 개병 기능에도 사용할 수 있습니다.
  • "$push", "$each" 제한자를 사용하면 작업 한번으로 값을 여러 개 추가할 수 있습니다.
use('jbh_MongoDB');

db.stock.ticker.insertOne({"_id" : "GOOG"});

db.stock.ticker.updateOne({"_id" : "GOOG"},
                          {"$push" : {"hourly" : {"$each" : [562.776, 562.790, 559.123]}}});
{
  "_id": "GOOG",
  "hourly": [
    562.776,
    562.79,
    559.123
  ]
}

  • 배열을 특정 길이로 늘이려면 "$slice" 를 "$push" 와 결합해 사용합니다.
  • 배열이 특정 크기 이상으로 늘어나지 않게 하고 효과적으로 top N 목록을 만들 수 있습니다.
use('jbh_MongoDB');

db.movies.insertOne({"title" : "horror"});

db.movies.updateOne({"title" : "horror"},
                    {"$push" : {"top10" : {"$each" : ["쏘우1", "쏘우2", "쏘우3","쏘우4","쏘우5","쏘우6","쏘우7"
                                                      ,"쏘우8","쏘우9","쏘우10","쏘우11","쏘우12",],
                    "$slice" : -10}}});
{
  "_id": {
    "$oid": "61810ff742a25ddae5f16ca1"
  },
  "title": "horror",
  "top10": [
    "쏘우3",
    "쏘우4",
    "쏘우5",
    "쏘우6",
    "쏘우7",
    "쏘우8",
    "쏘우9",
    "쏘우10",
    "쏘우11",
    "쏘우12"
  ]
}
  • 위의 예제는 배열에 추가할 수 있는 요소의 개수를 10개로 제한합니다.
  • 추가 후에 배열 요소의 개수가 10보다 작으면 모든 요소가 유지되고, 10보다 크면 마지막 10개 요소만 유지됩니다.

  • "$sort" 제한자를 "$push" 작업에 적용할 수 있습니다.
use('jbh_MongoDB');

db.movies.drop();

db.movies.insertOne({"title" : "horror"});

db.movies.updateOne({"title" : "horror"},
                    {"$push" : {"top10" : {"$each" : [{"name" : "나이트메어", "rating" : 6.6},
                                                      {"name" : "Saw" , "rating" : 4.3},
                                                      {"name" : "태극기 휘날리며" , "rating" : 3.3}],
                    "$slice" : -10,
                    "$sort" : {"rating" : 1}}}});
{
  "_id": {
    "$oid": "618111caa5abfdac3927a342"
  },
  "title": "horror",
  "top10": [
    {
      "name": "태극기 휘날리며",
      "rating": 3.3
    },
    {
      "name": "Saw",
      "rating": 4.3
    },
    {
      "name": "나이트메어",
      "rating": 6.6
    }
  ]
}
  • "$slice" 나 "$sort" 를 배열상에서 "$push" 와 함께 쓰려면 반드시 "$each" 도 사용해야 합니다.


  • 특정 값이 배열에 존재하지 않을 때 해당 값을 추가하면서, 배열을 집합처럼 처리하려면 쿼리 도큐먼트에 "$ne" 를 사용합니다.
  • 예를 들어 인용 목록에 저자가 존재하지 않을 때만 해당 저자를 추가하려면 다음처럼 하면 됩니다.
use('jbh_MongoDB');

db.papers.drop();

db.papers.insertOne({"name" : "Joe"});

db.papers.updateOne({"authors cited" : {"$ne" : "Richie"}},
                    {$push : {"authors cited" : "Richie"}});

db.papers.findOne();
{
  "_id": {
    "$oid": "61811b118680ba324b61b7bf"
  },
  "name": "Joe",
  "authors cited": [
    "Richie"
  ]
}
  • "$addToSet" 을 이용할 수도 있습니다.
  • "$addToSet" 은 "$ne" 가 작동하지 않을 때나 "$addToSet"을 사용하면 무슨 일이 일어났는지 더 잘 알 수 있을 때 유용합니다.
  • 예를 들어 사용자 정보 도큐먼트가 있다고 가정하고, 도큐먼트에는 사용자가 입력한 이메일 주소 셋이 있습니다.
use('jbh_MongoDB');

db.users.insertOne({"username" : "joe", "emails" : ["joe@example.com", "joe@gmail.com", "joe@yahoo.com"]});

db.users.findOne({"_id" : ObjectId("61811d11f4ca6ac8b8453c0c")})
{
  "_id": {
    "$oid": "61811d11f4ca6ac8b8453c0c"
  },
  "username": "joe",
  "emails": [
    "joe@example.com",
    "joe@gmail.com",
    "joe@yahoo.com"
  ]
}
  • 위와 같이 주소들이 있는데, 다른 주소를 추가할 때 "$addToSet" 을 사용하면 중복을 피할 수 있습니다.
use('jbh_MongoDB');

db.users.updateOne({"_id" : ObjectId("61811d11f4ca6ac8b8453c0c")},
                   {"$addToSet" : {"emails" : "joe@gmail.com"}});


db.users.findOne({"_id" : ObjectId("61811d11f4ca6ac8b8453c0c")});

db.users.updateOne({"_id" : ObjectId("61811d11f4ca6ac8b8453c0c")},
                   {"$addToSet" : {"emails" : "joe@daum.com"}});

db.users.findOne({"_id" : ObjectId("61811d11f4ca6ac8b8453c0c")});
{
  "_id": {
    "$oid": "61811d11f4ca6ac8b8453c0c"
  },
  "username": "joe",
  "emails": [
    "joe@example.com",
    "joe@gmail.com",
    "joe@yahoo.com",
    "joe@daum.com"
  ]
}
  • 기존에 joe@gmail.com 은 이미 존재하기 때문에 갱신되지 않고, joe@daum.com 은 없는 주소이기 때문에 추가된 것을 확인할 수 있습니다.


  • 배열에서 요소를 제거하는 방법에는 몇 가지가 있습니다.
  • 배열을 큐나 스택처럼 사용하려면 배열의 양쪽 끝에서 요소를 제거하는 "$pop" 을 사용하면 됩니다.
  • {"$pop" : {"key" : 1}} 은 배열의 마지막부터 요소를 제거하고 {"$pop" : {"key" : -1}} 은 배열의 처음부터 요소를 제거합니다.
  • "$pull" 은 주어진 조건에 맞는 배열 요소를 제거하는데 사용합니다.
use('jbh_MongoDB');

db.lists.insertOne({"todo" : ["dishes", "laundry", "dry cleaning"]});

db.lists.updateOne({}, {"$pull" : {"todo" : "laundry"}});

db.lists.findOne();
{
  "_id": {
    "$oid": "61824ed1ebc4a175d63211f0"
  },
  "todo": [
    "dishes",
    "dry cleaning"
  ]
}
  • "$pull" 은 조건와 일치하는 모든 도큐먼트를 지웁니다.
  • 예를 들어 [1, 1, 2, 1] 과 같은 배열에서 1을 뽑아내면 [2] 하나만 남게 됩니다.


  • 값이 여러 개인 배열에서 일부를 변경하는 조작은 꽤 어렵습니다.
  • 배열 내 여러 값을 다루는 방법은 두 가지가 있습니다.
  • 위치를 이용하거나, 위치 연산자를 사용할 수 있습니다.
  • 배열 인덱스틑 기준이 0이며, 배열 요소는 인덱스를 도큐먼트의 키 처럼 사용합니다.
use('jbh_MongoDB');

db.blog.drop();
db.blog.posts.drop();

db.blog.insertOne({"content" : "...", "comments" : [{"comment" : "good post", "author" : "John", "votes" : 0},
                                                    {"comment" : "i thought it was too short", "author" : "Claire", "votes" : 3},
                                                    {"comment" : "free watches", "author" : "Alice", "votes" : -5}
                                                   ]});

db.blog.updateOne({"comments.author" : "John"},
                  {"$set" : {"comments.$.author" : "Jim"}});

db.blog.findOne();
{
  "_id": {
    "$oid": "61825375fc84f6cbde3299f9"
  },
  "content": "...",
  "comments": [
    {
      "comment": "good post",
      "author": "Jim",
      "votes": 0
    },
    {
      "comment": "i thought it was too short",
      "author": "Claire",
      "votes": 3
    },
    {
      "comment": "free watches",
      "author": "Alice",
      "votes": -5
    }
  ]
}
  • 위치 연산자는 첫 번째로 일치하는 요소만 갱신합니다.
  • 따라서 John 이 댓글을 2개 이상 남겼다면 처음 남긴 댓글의 작성자명만 변경됩니다.


  • 몽고DB 3.6에서는 개별 배열 요소를 갱신하는 배열 필터인 arrayFilters를 도입해 특정 조건에 맞는 배열 요소를 갱신할 수 있습니다.
  • 예를들어 반대표가 5표 이상인 댓글을 추가합니다.
use('jbh_MongoDB');

db.blog.drop();
db.blog.posts.drop();

var post_id = 1;

db.blog.insertOne({"post" : post_id, "comments" : [{"comment" : "good post", "author" : "John", "votes" : 0},
                                                    {"comment" : "i thought it was too short", "author" : "Claire", "votes" : 3},
                                                    {"comment" : "free watches", "author" : "Alice", "votes" : -5}
                                                   ]});

db.blog.updateOne({"post" : post_id},
                  {"$set" : {"comments.$[elem].hidden" : true}},
                  {arrayFilters: [{"elem.votes" : {$lte: -5}}]});

db.blog.findOne();
{
  "_id": {
    "$oid": "61825508e93d921e76e70612"
  },
  "post": 1,
  "comments": [
    {
      "comment": "good post",
      "author": "John",
      "votes": 0
    },
    {
      "comment": "i thought it was too short",
      "author": "Claire",
      "votes": 3
    },
    {
      "comment": "free watches",
      "author": "Alice",
      "votes": -5,
      "hidden": true
    }
  ]
}
  • 위의 예제는 "comments" 배열의 각 일치 요소에 대한 식별자로 elem을 정의했습니다.
  • elem이 식별한 댓글의 투표값이 -5 이하면 "comments" 도큐먼트에 "hidden" 필드를 추가하고 값이 true 로 설정됩니다.


728x90

이 글을 공유하기

댓글

Designed by JB FACTORY