6.1 도큐먼드 업데이트

1 . MongoDB에서 업데이트를 하려면 두 가지 방법을 사용할 수 있다. 도큐먼트 전체를 대치하든지 아니면 도큐먼트 내의 특정 필드를 수정하기 위해 업데이트 연산자를 조합해서 사용할 수 있다.

user_id = BSON::ObjectId("4c4b1476238d3b4dd5000001")
doc = @user.find_one({:_id => user_id})

doc['email'] = 'mongodb-user@10gen.com'
@users.update({:_id => user_id}, doc, :safe => true)
  • 도규먼트를 수정하는 방법
    유저의 _id를 가지고 먼저 도큐먼트를 검색한다. 그런 다음 불러온 도큐먼트의 email속성을 수정하고, 수정된 도큐먼트를 update 메소드의 파라미터로 넘겨주게 된다. 마지막 라인은 유저 컬렉션에서 주어진 _id로 도큐먼트를 찾아서 새로이 제공하는 도큐먼트로 대치하라는 의미이다.(도규먼트를 수정하는 방법)
  • 연산자로 수정하는 방법
    업데이트 연산자 중에 하나인 $set을 사용했다. 이 경우에 업데이트 요청은 주어진 조건으로 유저 도큐먼트를 찾아서 email필드를 mongodb-ser@10gen.com으로 수정하라 와 같은 방식으로 좀 더 특정 필드를 목표로 하고 있다.
@users.update({:_id => user_id}, {'$set' => {:email => 'mongodb-user@10gen.com'}}, :safe => true)

2 . 유저의 주소 리스트에 배송 주소를 하나 더 추가하려고 한다. 도큐먼트 대치 방식은 다음과 같다.

doc = @users.find_one({:_id => user_id})

new_address = {
   :name => "work",
   :street => "17 W. 18th St.",
   :city => "New York",
   :state => "NY",
   :zip => 10011
}

doc['shipping_addresses'].append(new_address)
@users.update({:_id => user_id}, doc)

타깃 방식은 다음과 같다.

@users.update({:_id => user_id},
   {'$push' => {:addresses =>
      {:name => "work",
      :street => "17 W. 18th St.",
      :city => "New York",
      :state => "NY",
      :zip => 10011
      }
   } 
})

대치 방식은 이전과 같이 서버로부터 유저 도큐먼트를 가져와서 수정을 한 후에 다시 서버로 보낸다. 여기에서 업데이트문은 이메일 주소를 업데이트하기 위해 사용했던 업데이트문과 같다. 이와는 다르게 타깃 방식의 업데이트는 새 주소를 기존의 addresses배열에 추가하기 위해 $push라고 하는 또 다른 업데이트 연산자를 사용한다.

6.2 전자상거래 업데이트

6.2.1 상품과 카테고리

  • 상품의 평점 평균
    상품은 여러가지 업데이트 전략이 적용될 수 있다. 상품 정보를 수정할 수 있는 인터페이스가 관리자에게 제공된다고 가정하면,가장 쉬운 업데이트는 현재의 상품 도큐먼트를 가지고 와서 관리자에 의해 수정된 것과 통합한 후에 새로운 도큐먼트로 이전의 도큐먼트를 대치하는 것이다.다른 경우로는 단지 몇가지 속성만 바꾸는 경우인데,이때는 타깃 방식의 업데이트가 사용되어야 한다. 상품의 평점의 평균을 구하는 경우가 여기에 속한다. 유저는 상품 리스트를 상품의 평균 평점으로 정렬할 필요가 있기 때문에 평점을 상품 도큐먼트에 저장하고, 리뷰가 추가되거나 삭제될 때마다 그 값을 업데이트 한다.
average = 0.0
count = 0
total = 0
cursor = @reviews.find({:product_id => product_id}, :fields => ["rating"])
while cursor.has_next? && review = cursor.next()
   total += review['rating']
   count += 1
end

average = total / count
@product.update({:_id => BSON::ObjectId("4c4b1476238d3b4dd5003981")},
   ${"$set" => {:total_reviews => count, :average_review => average}})

이 코드는 각 상품 리뷰 도큐먼트로부터 rating 필드를 집계해서 평균을 구한다. 반복문에서는 상품에 대한 리뷰의 총 개수도 계산하는데, 이로 인해 count함수에 대한 추가적인 데이터베이스 호출을 안해도 된다. 리뷰의 총 개수와 평균 평점으로 $set을 사용해서 타깃 업데이트를 수행할 수 있다.

다른 전략도 가능하다.예를 들어, 상품 도큐먼트에 리뷰 평점의 총합을 보관하는 필드를 추가할 수도 있다. 새 리뷰가 인서트 된 후에 상품에 대한 리뷰의 총 개수와 평점의 총합을 질의하고 평균을 계산해서 다음과 같이 셀렉터를 사용해 업데이트를 수행 할 수 있다.

 ${"$set" => {:average_review => average, :ratings_total => total}, '$inc' => {:total_reviews => 1}})
  • 카테고리 계층구조
    대부분의 데이터베이스에서는 카테고리 계층구조를 쉽게 표현할 수 있는 방법이 없다.도규먼트 구조가 어느 정도 도움이 되긴 하지만 MongoDB에서도 이것은 마찬가지이다. 도큐먼트는 각 카테고리가 조상 카테고리의 리스트를 가지고 있기 때문에 읽기에 최적화되어 있다. 한가지 까다로운 요구사항은 모든 좃ㅇ 카테고리를 최신으로 가지고 있는 것이다.이것이 어떻게 이루어지는지 다음의 예를 통해 살펴보자.

우선, 주어진 카테고리에 대해 조상 리스트를 업데이트하기 위한 일반적인 방법이 필요하다. 한 가지 가능한 방법은 아래와 같다.

def generate_ancestors(_id, parent_id)
   ancestor_list = []
   while parent = @categories.find_one(:_id => parent_id) do
      ancestor_list.unshift(parent)
      parent_id = parent['parent_id']
   end
   
   @categories.update({:_id => _id}, {'$set' {:ancestors => ancestor_list}}

이 메소드는 카테고리 계층구조를 거슬러 올라가면서 루트 노드에 도달할 때까지 각 노드의 parent_id 속성을 연달아 질의하여 조상 리스트를 구한다. 이렇게 해서 조상들의 순서가 있는 리스트를 구하고 그 결과를 ancestor_list에 저장한다. 마지막으로 $set을 사용해서 카테고리의 ancestors 속성에 저장한다.

이제 기본적인 것을 갖추었으니 새 카테고리를 인서트하는 프로세스를 살펴보자. 그림 6-1과 같이 간단한 카테고리 계층구조가 있다고 하자. (책 144페이지 참조)

이 카테고리 구조에 Gardening이라는 카테고리를 Home 카테고리 아래에 새로 추가한다고 하자.새로운 카테고리 도큐먼트를 삽입하고, 좀 전에 우리가 이미 살펴본대로 그 카테고리의 조상 리스트를 생성하는 메소드를 실행한다.

category = {
   :parent_id => parent_id,
   :slug => "gardening",
   :name => "Gardening",
   :description => "All gardening implements, tools, seeds, and soil."
}
gardening_id = @categiries.insert(category)
generate_ancestors(gardening_id, parent_id)

Outdoorss카테고리를 Gardening카테고리 아래로 옮기려고 한다면 어떻게 될까?이것은 카테고리에 대해서 그 조상 리스트를 변경해야 하기 때문에 복잡해질 가능성이 많다.Outdoors 카테고리의 parent_id의 값을 Gardening의 _id 값으로 변경하는 것으로 시작할 수 있다.

@categories.update({:_id => outdoors_id}, {'$set' => {:parent_id => gardening_id}})

Outdoors 카테고리를 옮겼기 때문에 Outdoors의 모든 자손 노드들은 유효하지 않은 조상 리스트를 갖게 된다. 이것은 조상 리스트에 Outdoors를 가지고 있는 모든 카테고리를 질의하여 불러온 다음, 이 카테고리들의 조상 리스트를 재 계산하여 수정할 수 있다. MongoDB의 강력한 배열에 대한 질의로 이것이 간단하게 된다.

@categories.find({'ancestors.id' => outdoors_id}).each do |category|
generate_ancestors(category['_id'], outdoors_id)
end

하지만 이제 카테고리 이름을 변경해야 한다면 어떻게 해야 할까? Outdoors를 TheGreatOutdoors로 수정하려 한다면 다른 카테고리의 조상 카테고리 리스트에 있는 모든 Outdoors를 TheGreateOutdoors로 변경해야 한다. 이 부분에서 당연하게 이런 생각이 들지도 모르겠다. 비정규화를 하면 나타나는 문제점이 바로 이런것들이지 하지만 조상 리스트를 다시 계산할 필요없이 이 업데이트를 수행할 수 있다는 것을 알게되면 이런 생각이 조금은 풀릴 것이다.

doc = @categories.find_one({:id => outdoors_id})
doc['name'] = "The Great Outdoors"
@categories.update({:_id => outdoors_id}, doc)

@categories.update({'ancestors.id' => outdoors_id},
{'$set' => {'ancestors.$' => doc}}, multi => true)

우선 Outdoors 도큐먼트를 찾아서 name속성을 로컬 도큐먼트에서 바꿔주소 대치 방식으로 업데이트를 수행한다. 그리고서 수정된 Outdoors 도큐먼트를 이용해서 모든 조상 리스트에 나타나는 Outdoors를 바꿔주는데, 이것은 위치 연산자와 다중 업데이트를 통해 수행한다.다중 업데이트가 무엇인지 이해하는 것은 어렵지 않다. 실렉터와 일치하는 도큐먼트를 모두 수정하기를 원할 경우 :multi => true로 지정하면 되는 것을 기억하기 바란다.

위치 연산자는 이것보다는 조금 더 까다롭다. 주어진 카테고리의 조상 리스트에서 Outdoor 카테고리가 나타나는 위치를 알 수 있는 방법이 없다는 것을 생각하기 바란다. 따라서 어떤 도큐먼트에 대해 배열 내에서 Outdoor 카테고리의 위치를 동적으로 지정할 수 있는 업데이트 연산자가 필요하며, 이런 경우에 위치 연산자를 사용하면 된다. 위의 코드에서 ancestors.$의 $가 위치 연산자인데, 쿼리 실렉터와 일치하는 배열 인덴스를 그 자신으로 대치해 업데이트를 가능하게 한다.

6.2.2 리뷰

모든 리뷰가 동등한 것은 아니기 때문에 유저로 하여금 리뷰에 대해 추천하는 것을 허용하게 된다. 이러한 추천은 기초적인 것으로, 단지 어떤 리뷰가 도움이 되었는지를 가리킬 뿐이다. 리뷰 데이터를 모델링할 때 추천 수와 추천자의 아이디를 리스트로 가지고 있도록 만들었는데,리뷰 도큐먼트에서 이와 관련한 부분은 다음과 같다.

{helpful_votes : 3,
voter_ids: [ ObjectId("4c4b1476238d3b4dd5000041"),
             ObjectId("7a4f0376238d3b4dd5000003"),
             ObjectId("92c21476238d3b4dd5000032"),
           ]}

타깃 방식의 업데이트를 사용해서 추천과 관련한 레코드를 저장할 수 있다.이 전략은 $push 연산자를 사용해서 추천자의 ID를 리스트에 추가하고 $inc 연산자를 사용해서 추천 수를 하나 증가하는데,이 두 가지가 하나의 업데이트 연산을 통해 이루어진다.

db.reviews.update({ _id : ObjectId("4c4b1476238d3b4dd5000041")},
    {$push : { voter_ids : ObjectId("4c4b1476238d3b4dd5000001")},
      $inc: {helpful_votes: 1}
})

위의 업데이트 연산은 추천자가 이 리뷰에 대해 추천한 적이 없는 경우에만 수행되어야만 비로소 완전해진다. 이것을 위해 쿼리 실렉터를 수정해서 voter_ids 배열에 추가하려고 하는 아이디가 없는 경우에만 일치하도록 수정해야 한다. $ne 쿼리 연산자를 사용하면 이것이 쉽게 된다.

query_selector = {_id : ObjectId("4c4b1476238d3b4d5000041"),
   voter_ids :{$ne:ObjectId("4c4b1476238d3b4dd5000001")}
db.reviews.update({ query_selector,
    {$push : { voter_ids : ObjectId("4c4b1476238d3b4dd5000001")},
      $inc: {helpful_votes: 1}
})

6.2.3 오더

오더의 아이템 배열에 저장할 상품 도큐먼트를 생성하고 타깃 업데이트를 실행하는데, 이때의 업데이트는 upsert라는 연산을 사용한다. 업서트는 업데이트할 도큐먼트가 존재하지 않을 경우 새로운 도큐먼트를 인서트한다.

cart_item = {
   _id : ObjectId("4c4b1476238d3b4dd5003981"),
   slug : "wheel-barrow-9092",
   sku : "9092",
name: "Extra Large Wheel Barrow",
   pricing:{
   retail : 589700,
   sale : 489700
   }
}

selector = {user_id : ObjectId("4c4b1476238d3b4dd5000001"),
            state : 'CART',
            'line_items.id' :
               {'$ne' : ObjectId("4c4b1476238d3b4dd5003981")}  
            }

update = {'$push' : {'line_items' : cart_item}}
db.orders.update(selector, update, true, false)

쿼리 실레거를 생성하고 도큐먼트 업데이트를 따로 하는 것은 코드를 좀 더 명확하게 하기 위해서다.업데이트 도큐먼트는 카드 아이템 도큐먼트를 오더 아이템의 배열에 추가한다. 쿼리 실렉터가 보여주듯이 이 업데이트는 해당 아잍ㅁ이 오더 아이템 배열에 아직 존재하지 핞을 경우에만 성공적으로 수행된다.

cart_item = {
   _id : ObjectId("4c4b1476238d3b4dd5003981"),
   line_items : [{
      _id : ObjectId("4c4b1476238d3b4dd5003981")
      slug : "wheel-barrow-9092",
      sku : "9092",
      name: "Extra Large Wheel Barrow",
      pricing:{
      retail : 589700,
      sale : 489700
      }
   }]
}

selector = {user_id : ObjectId("4c4b1476238d3b4dd5000001"),
            state : 'CART',
            'line_items.id' : ObjectId("4c4b1476238d3b4dd5003981")}  

update = {'$inc' : {'line_items.$.qty' : 1, sub_total : cart_item['pricing']['sale']}}

db.orders.update(selector, update, true, false)

위의 코드에서 $inc 연산자를 사용해서 개별 오더 아이템에 대한 오더 액수의 합과 전체 개수를 업데이트하는 것을 알 수 있다.

6.3 원자적 도큐먼트 프로세싱

MongoDB에서 반드시 필요한 업데이트 툴은 findAndModify명령이다. 이 명령을 통해서 도큐먼트를 자동으로 업데이트하고 업데이트 된 도큐먼트를 리턴하는 것이 한번에 가느해진다.

6.3.1 오더 상태 전이

모든 상태 전이는 유효한 초기 상태를 확인하는 쿼리와 상태를 변경하는 업데이트,이럭게 두 부분으로 되어 있다.

  • 유저가 체크아웃 스크린에서 보고 있는 액수를 승인해야 한다.
  • 승인 과정에서는 카트의 내용이 변경되어서는 안된다.
  • 승인 절차가 실패할 경우 카트의 이전 상태로 돌아가야 한다.
  • 크레딧 카드가 성공적으로 승인될 경우 지불 정보가 오더에 저장되고 오더의 상태는 SHPIMENT PENDING로 바뀌어야 한다.
db.orders.findAndModify({
   query : {user_id : ObjectId("4c4b1476238d3b4dd5000001"), state : "CART"},
   update : {"$sest" : {"state" : "PRE-AUTHRIZE"}, new : true}
})

성공하면 findAndModify는 상태가 전이된 오더 객체를 리턴하게 된다. 일단 오더가 PRE-AUTHHORIZE 상태에 놓이게 되면 유저는 카트의 내용을 변경할 수 없다.이것은 카트에 대한 모든 업ㄷ이트는 오더의 상태가 CART일 때만 가능하기 때문이다.이제 승인 전 단계에서 오더 객체를 가지고 다양한 총계를 계산한다.

db.orders.findAndModify({
   query : {user_id : ObjectId("4c4b1476238d3b4dd5000001"),
   total : 99000,
   state : "PRE-AUTHRIZE"} ,
   update : {"$sest" : {"state" : "AUTHRIZING"}, new : true}
})

만일 두 번째 수행한 findAndModify가 실패하면 오더의 상태를 CART로 되돌리고 유저에게 업데이트된 총액을 알려줘야 한다.하지만 성공할 경우에는 승인된 총액이 유저에게 보여준 액수와 동일한 것을 의미하고, 이것은 실제의 승인 API호출을 할 수 있다는 것을 의미한다.따라서 애플리케이션에서 유저의 신용 카드에 대한 신인 요청을 할 수 있다. 하지만 승인이 성공하면 승인 정보를 오더에 저장하고 다음 단계로 넘어간다. 아래예에서 제시되는 전략은 한 번의 findAndModify호출로 두가지를 수행한다.

auth_doc = {ts : new Date(),
            cc : 3432003948284040,
            id : 2923838291029384483949348,
            gateway : "Authorize.net"}
db.orders.findAndModify({
   query : {user_id : ObjectId("4c4b1476238d3b4dd5000001"), state : "AUTHORIZING"},
   update : {"$sest" : {"state" : "PRE-SHIPPING"}, "authorization": auth_doc}
})

MongoDB의 어떤 기능들로 인해 이 트랜잭션 프로세스가 용이해지는지 알고 있어야 한다. 하나의 도큐먼트를 원자적으로 수정할 수 있다는 점, 한번의 연결에 일관적인 읽기를 보장한다는 점, 그리고 이들 연산이 MongoDB에서 제공하는 단일 도큐먼트 원자성에 적합하도록 해주는 도큐먼트 구조 그 자체, 이렇게 세가지이다.

6.3.2 재고 관리

PASS

6.4 실제적인 세부사항 : MongoDB업데이트와 삭제

6.4.1 업데이트 타입과 옵션

MongDB는 타깃 방식과 대치 방식의 업데이트를 지원한다. 타깃 업데이트는 하나 혹은 그 이상의 업데이트 연산자를 사용해서 정의한다. 대치 업데이트는 업데이트의 쿼리 실렉터와 일치하는 도큐먼트를 대치하기 위해 사용될 도큐먼드로 정의한다.

업데이트 도큐먼트가 모호하면 업데이트는 실패한다는 점을 알기 바란다. 아래에서 업데이트 연산자인 $addToSet을 대치 방식의 의미를 갖는 {name : ‘Pitchfork’}과 합쳤다.

db.products.update({}, {name : 'Pitchfork', $addToSet : {tags : 'cheap'})

원래 의도가 도큐먼트의 이름을 바꾸려는 것이었다면 다음과 같이 $set 연산자를 사용해야한다.

db.products.update({}, {$set : {name : 'Pitchfork'}, $addToSet : {tags : 'cheap'})
  • 다중업데이트 업데이트는 기본적으로 쿼리 실렉터와 일치하는 첫 번째 도큐먼트만을 업데이트 한다. 일치하는 모든 도큐먼트를 업데이트하려면 다중 업데이트라고 명확하게 알려줘야 한다. 셸에서는 이것을 우해 업데이트 메소드의 네 번째 파라미터 값을 true로 념겨준다.
db.products.update({}, {$addToSet : {tags : 'cheap'}, false, true)
  • UPSERT 아이템이 없으면 새로 추가하고 이미 있다면 업데이트를 하는 것은 일반적으로 필요한 일이다. 구현하기 까다로운 이러한 패턴은 보통 MongoDB의 업서트로 처리한다. 쿼리 실렉통허 일치하는 도큐먼트가 발견되면 업데이트가 정상적으로 수행된다. 하지만 일치하는 도큐먼트가 발견되지 않을 경우 새로운 도큐먼트를 삽입된다.
db.products.update({slug : 'hammer'}, {$addToSet : {tags : 'cheap'}, true)

6.4.2 업데이트 연산자

  • $INC
    $inc 연산자는 수치를 증가하거나 감소할 때 사용한다.
db.products.update({slug : "shovel"}, {$inc : {review_count : 1})
db.users.update({username : 'moe'}, {$inc : {password_retires: -1})

하지만 다음과 같이 수치를 임의로 더하거나 빼는 데도 사용할 수 있다.

db.readings.update({_id : 324}, {$inc : {temp : 2.7435})

$inc는 편리하고 효율적이다. 이 연산자는 도큐먼트의 크기를 변경하는 일이 거의 없기 때문에 디스크에서 해당 부분만 적용이 되어 지정된 값만 영향을 받는다. 쇼핑 카트에 아이템을 추가하는 것을 구현할 때 그랬듯이 $inc를 업서트와 함께 사용할 수 있다. 앞의 예제를 업서트와 함께 사용하는 것으로 바꾸면 다음과 같다.

db.readings.update({_id : 324}, {$inc : {temp : 2.7435}, true)
  • $SET 과 $UNSET
    도큐먼트에서 특정 키의 값을 정해주려면 $set을 사용한다. 키에 대한 값으로는 정수에서부터 서브도큐먼트와 배열에 이르기까지 어떤 것이라도 가능하다.즉, 다음과 같은 업데이트가 모두 가능하다.
db.readings.update({_id : 324}, {$set : {temp : 97.6}})
db.readings.update({_id : 325}, {$set : {temp : {f : 212, c : 100}})
db.readings.update({_id : 324}, {$set : {temps : [97.6, 98.4, 99.1]})

$unset은 도큐먼트에서 해당 키를 삭제한다. 아래의 예는 readings라는 컬렉션 내의 한 도큐먼트에서 temp키를 삭제하는 것을 보여준다.

db.readings.update({_id : 324}, {$unset : {temp : 1})

임베디드된 도큐먼트나 배열에 대해서도 $unset을 사용할 수 있다. 이 경우에 닷 표기법을 사용해서 내부 객체를 지정한다. 다음과 같이 2개의 도큐먼트가 컬렉션에 있다면,

{_id : 325, 'temp' : {f:212, c:100}}
{_id : 326, temps : [97.6, 98.4, 99.1]}

첫 번째 도큐먼트에서 화씨를 삭제하고 두 번째 도큐먼트에서 인덱스가 0인 값을 삭제하려면 다음과 같다.

db.reading.update({_id : 325},{$unset : {'temp.f' : 1})
db.reading.update({_id : 326},{$pop : {temps : -1})
  • $rename
    키의 이름을 바꿀 필요가 있을 경우에는 $rename을 사용한다.
db.readings.upadate({_id : 324}, {$rename : {'temp' : 'temperature'})

서브 도큐먼트도 다음과 같이 이름을 변경할 수 있다.

db.readings.upadate({_id : 324}, {$rename : {'temp.f' : 'temp.farenheit'})
배열 업데이트 연산자
  • $push AND $pushAll
    배열에 값을 추가해야 한다면 $push나 $pushAll을 사용한다. $push는 배열에 하나의 값을 추가하는 반면,$pushAll은 하나 이상의 값을 추가하는 데 사용한다. 예를 ㄷㄹ어, 부삽 상품에 새로운 태그를 추가하는 것은 아주 쉽다.
db.products.update({slug : 'shovel'}, {$push : {'tags' : 'tools'}})

태그를 여러 개 추가하려면 다음과 같이 하면 된다.

db.products.update({slug : 'shovel'}, {$pushAll : {'tags' : ['tools','dirt','garden']}})
  • $addToSet과 $each
    $addToSet은 배열에 값을 첨가하지만 이것을 좀 더 분별적으로 수행한다. 즉, 배열에 존재하지 않을 경우에만 값을 첨가한다.따라서 만일 부삽 상품이 이미 tool이라는 태그 값을 가지고 있으면 아래의 업데이트는 도큐먼트를 수정하지 않는다.
db.products.update({slug : 'shovel'}, {$addToSet: {'tags' : 'tools'}})

위의 경우를 하나 이상의 값에 대해서 수행하고자 한다면 아래에서와 같이 $addToSet을 $each 연산자와 함께 사용해야 한다.

db.products.update({slug : 'shovel'}, {$addToSet: {'tags' : {$each : ['tools','dirt','garden']}}})
  • $pop
    배열에서 아이템을 삭제하는 가장 기본적인 방법은 $pop 연산자를 사용하는 것이다.$push가 배열에 아이템을 첨가하는 것이라면 $pop은 마지막으로 첨가된 아이템을 지운다.많은 경우 $push와 함께 쓰이지만, $pop만을 사용하는 경우도 있다. tags배열이 [‘tools’, ‘dirt’, ‘steel’]의 값을 갖는다면 아래의 $pop은 steel태그를 삭제할 것이다.
db.products.update({slug : 'shovel'}, {$pop : {'tags' : 1}})

$unset에서처럼 $pop의 구문도 {$pop : {‘elementToRemove’ : 1}}이다. 하지만 $unset과는 다르게, $pop은 배열의 첫 번째 아이템을 지우기 위해 -1이라는 또 다른 값을 받는다.위의 배열에서 tools 태그를 삭제하는 것은 아래와 같다.

db.products.update({slug : 'shovel'}, {$pop : {'tags' : -1}})

$pop에서 한가지 당황스러울 수도 있는 점이 있는데, 그것은 $pop이 배열에서 삭제된 값을 리턴하지 않는다는 점이다. $pop은 스택 구조에서와 같이 동일하게 수행되지는 않는다는 점을 명심해야 한다.

  • $pull과 $pullAll
    $pull은 좀 더 정교한 $pop이라고 볼 수 있다. $pull은 배열에서 원소의 위치 대신값을 지정해서 아이템을 삭제한다. 위의 tags배열에서 태그 dirt를 삭제하고자 하면, 배열 내의 위치를 알 필요 없이 아래에서와 같이 단지 $pull 연산자에게 그것을 삭제할 것을 지시하기만 하면 된다.
db.products.update({slug : 'shovel'}, {$pull: {'tags' : 'dirt'}})

$pullAll은 $pushAll과 유사한데 삭제할 값의 리스트를 지정할 수 있다. dirt와 garden 태그를 둘 다 삭제하려면 다음과 같이 $pullAll을 사용할 수 있다.

db.products.update({slug : 'shovel'}, {$pullAll: {'tags' : ['dirt','garden']}})
  • 위치업데이트
    MongoDB에서 서브도큐먼트의 배열을 사용해서 데이터를 모델링하는 것이 일반적이지만, 서브 도큐먼트를 다루기가 쉽지 않기 때문에 위치 연산자를 사용한다. 위치 연산자를 통해 배열 내의 서브도큐먼트를 업데이트할 수 있다. 닷 표기법을 사용해서 업데이트할 서브도큐먼트를 쿼리 실렉터에서 지정한다.
query = {_id : ObjectId("4c4b1476238d3b4dd5003981"), 'line_items.sku' : '10027'}
update = {$set : 'line_items.&.quantity' : 5}}
db.orders.update(query, update)

line_items.$.quantity 에서 $가 위치 연산자다.쿼리 실렉터와 일치하는 도큐먼트가 발견도면 내부적으로 이 위치 연산자를 SKU가 10027인 도큐먼트의 인덱스로 대치하고, 그 결과 해당 도큐먼트를 업데이트하게 된다.

6.4.3 findAndModify

findAndModify명령을 사용하는 자세한 예제를 앞에서 이미 살펴봤으므로 여기서는 옵션을 살펴보겠다.아래의 옵션 중에서 query와,update 혹은 remove 둘 중의 하나는 반드시 필요한 옵션이다.

  • query = 도큐먼트 쿼리 실렉터이고 디폴트 값은 {}이다
  • update = 업데이트할 내용을 지정하는 도큐먼트이고 디폴트 값은 {}이다.
  • remove = 부울 값으로 true이면 오브젝트를 삭제하고 삭제한 오브젝트를 리턴한다. 디폴트는false
  • new = 부울 값으로 true이면 업데이트를 수행한 후에 업데이트가 적용된 도큐먼트를 리턴한다. 디폴트는 false
  • sort = 정렬 방향을 지정하는 도큐먼트 FindAndModify가 한 번에 하나의 도큐먼트를 수정하기 때문에 이 옵션을 일치하는 도큐먼트 중에서 어떤 것을 처리해야 하는지를 정한ㄴ데 사용된다.
  • fields = 일부의 필드만 필요하다면 이 옵션을 사용해서 필요한 필드를 지정한다.
  • upsert = 부울 값으로 true이면 findAndModify를 업서트처럼 수행한다. 찾고자 하는 도큐먼트가 없는 경우에는 그 도큐먼트를 생성한다.

6.4.4 삭제

도큐먼트 삭제가 그다지 어렵지 않다는 점을 알게 되면 안심이 될 것이다. 컬렉션 전체를 지울 수도 있고, remove 메소드에 쿼리 실렉터를 파라미터로 넘겨줘서 컬렉션내의 일부분의 도큐먼트를 지울 수도 있다.

db.reviews.remove({})
db.reviews.remove({user_id : ObjectId('4c4b1476238d3b4dd5000001')})

6.4.5 동시성, 원자성, 고립

  • MongoDB에서 동시성이 어떻게 수행되는지 이해하는 것이 중요하다. MongoDB 2.0에서는 잠금 전략이 상당히 정교하지 못하다. 하나의 글로벌 읽기-쓰기 잠금이 mongod인스턴스 전체를 관장한다.이것이 의미하는 바는 어느 한 시점에서 데이터베이스는 하나의 쓰기 혹은 다수의 릭기를 허용하지만,둘 다 허용하지는 않는다는 것이다. 어떤 램에 있는 내부적인 매핑 정보를 가지고 있어서, 램에 없는 도큐먼트에 대한 쓰기나 읽기 연산 요청이 들어오면 그 도큐먼트가 메모리로 페이징될때까지 다른 연산을 수행한다.
  • 두번째 최적화는 쓰기 잠금을 양보하는 것이다. 쓰기 연산의 시간이 오래 걸리면 다른 모든 읽기와 쓰기 연산은 원래의 쓰기가 완료될 때까지 블록된다. 모든 삽입,업데이트,삭제 연산은 쓰기 잠금을 수행한다. 삽입 연산은 시간이 오래 걸리는 경우가 거의 없다.. 하지만 업데이나 삭제의 경우 전체 걸렉션에 영향을 미치는 경우에는 오랫동안 실행 될 수 있다.이 문제에 대한 현재의 해결책은 오랜 시간이 소요되고 있는 연산을 주기적으로 다른 읽기와 쓰기에게 양보하는 것이다. 연산을 양보할 때 잠시 연산을 멈추고 락을 해제했다가 나중에 연산을 재개한다. 하지만 도큐먼트를 업ㄷ이트하고 삭제할 때 이러한 양보는 장단점이 있다. 어떤 연산이 수행되기 전에 반드시 모든 도큐먼트를 업데이트하거나 삭제해야만 하는 상황을 쉽게 생각해볼 수 있다.이런 경우에 $atomic이라고 하는 특수한 옵션을 사용해서 해당 연산이 양보되지 못하도록한다.
db.reviews.remove({user_id : ObjectId('4c4b1476238d3b4dd5000001'),
{$atomic : true}})

다중 업데이트에 대해서도 동일하게 적용할 수 있다. 이것은 다중 업데이트가 모두 한 연산으로 완료되게끔 한다.

db.reviews.remove({$atomic : true},{$set : {rating : 0}}, false, true),
{$atomic : true}})

6.4.6 업데이트 성능

디스크 상의 도큐먼트를 업데이트할 때 본질적으로 세 가지 종류가 있다.

  1. 가장 효율적인 것인데, 하나의 값만 수정하고 전체 BSON 도큐먼트의 크기는 변하지 않을때다.가장 일반적인 $inc 연산자를 사용할 때 발생한다. $inc는 단지 정수 값을 증가시키기 때문에 이 값이 디스크 상에서 차지하는 크기에는 변함이 없다.만일 이 정수가 int라면 디스크 상에서 4바이트를 차지할 것이고, long이나 double이면 8바이트가 된다.
  2. 도큐먼트의 크기나 구조를 바꾸는 업데이트다. BSON 도큐먼트는 문자 그대로 바이트 배열로 표현되고, 처음 네 바이트로 해당 도큐먼트의 크기를 나타낸다.따라서 도큐먼트에 대해 $push 연산자를 사용하면 도큐먼트의 전체 크기와 구조를 변경하게 된다.이것은 곧 전체 도큐먼트를 디스킁 다시 쓰기를 해야 한다는 의미다.
  3. 업데이트는 도큐먼트를 다시 쓰기 할때의 업데이트다.도큐먼트가 확장되고 디스크에서 할당된 공간 내에 들어갈 수 없으면 다시 쓰기를 해야 할뿐만 아니라 새로운 공간으로 이동까지 해야 한다.이것이 자주 발생하면 리소스를 많이 필요로 하기 쉽다.MongoDB에서는 컬렉션에 대해 패딩 지수를 동적으로 제어함으로써 이 문제를 완화하고자 한다.