MongoDB에서 업데이트를 하려면 두 가지 방법을 사용할 수 있다.
도큐먼트 전체를 대치 하든지 아니면 도큐먼트 내의 특정 필드를 수정하기 위해 업데이트 연산자를 사용할 수 있다.
user_id = ObjectId("4c4b1476238d3b4dd5003981")
doc = db.users.findOne({_id:user_id})
doc['email'] = 'mongodb-user@mongodb.com'
db.users.update({_id:user_id},doc)
'사용자 컬렉션에서 주어진 _id로 도큐먼트를 찾아서 새로이 제공하는 도큐먼트로 대치하라!'는 의미이다.
기억해야 할 점은 업데이트 연산이 전체 도큐먼트를 대치한다는 점이며, 이것이 바로 업데이트 연산이 가장 먼저 패치되어야 하는 이유다.
어려 사용자가 동일한 도큐먼트를 업데이트 하는 경우 마지막으로 쓰기 연산이 수행된 내용이 저장된다.
user_id = ObjectId("4c4b1476238d3b4dd5003981")
db.users.update({_id: user_id},{$set: {email:'mongodb-user2@mongodb.com'}})
몇 가지 특수한 업데이트 연산자 중에 하나인 $set을 사용했다.
'주어진 조건으로 사용자 도큐먼트를 찾아서 email 필드를 해당 값으로 수정하라'와 같은 방식으로 좀 더 특정 필드를 목표로 하고 있다.
이번에는 상품의 리뷰 수를 증가시키고자 한다. 도큐먼트 대치 방식은 다음과 같다.
대치
product_id = ObjectId("4c4b1476238d3b4dd5003982")
doc = db.products.findOne({_id: product_id})
doc ['total_reviews'] += 1
db.products.update({_id: product_id}, doc)
업데이트 연산자 방식은 다음과 같다.
연산자
db.products.update({_id: product_id}, {$inc: {total_reviews: 1}})
(그냥 딱봐도 연산자 수정이 더 나은 성능을 갖는 것 처럼 보인다..)
연산자 방식의 수정이 더 나은 성능을 갖는다.
평균 크기가 200KB인 도큐먼트를 대치 방식으로 업데이트하려면 업데이트당 서버로 200KB를 보내는 것이 된다.
반면 $set, $push 등 업데이트 명령에 사용된 도큐먼트는 수정되는 도큐먼트의 크기에 상관없이 100바이트 이내다.
또한, 연산자 방식은 도큐먼트를 원자적(atomic)으로 업데이트하는데 적합하다.
연산자 방식은 대량의 동시적 업데이트일지라도 각각의 $inc는 고립적으로 적용되어 증가하거나 증가하지 않거나 둘 중의 한 가지만 가능하다는 의미이다. (원자성이 보장된다!)
반면 대치 방식은 일종의 낙관적 잠금(optimistic locking)을 통해 원자성을 보장할 수 있다.
레코드를 잠그지 않고도 업데이트 연산이 제대로 수행되는 것을 보장하기 위한 기술이다.
사용자가 수정한 후에 업데이트하려고 할 때, 업데이트에 타임스탬프가 포함된다.
그 타임스탬프가 페이지의 최종 저장 시간보다 더 이전이면 업데이트를 할 수 없다.
하지만 아무도 그 페이지에 대해 수정하지 않았다면 업데이트가 가능하다.
예) wiki , zookeeper..?
<> 비관적 잠금을 사용하면 레코드가 트랙잭션에서 처음 엑세스할 때부터 완료될 때까지 레코드가 잠긴다.
multi update
매개변수를 이용하여 다중 업데이트를 할 수 있다.
multi 매개변수가 없으면 업데이트는 첫 번째로 일치하는 도큐먼트에만 영향을 미친다.
업서트(upsert)
업데이트할 도큐먼트가 존재하지 않을 경우 새로운 도큐먼트를 인서트한다.
원자적으로 업데이트를 하거나 도큐먼트가 존재하는지의 여부가 불확실할 때 업서트는 유용하다.
findAndModify 명령을 통해서 도큐먼트를 자동으로 업데이트하고 업데이트된 도큐먼트를 반환하는 것이 한 번에 가능해진다.
왜 이것이 유용할까?
도큐먼트를 패치하고 업데이트(또한 업데이트한 다음 패치)하면, 이러한 작업 사이에 다른 MongoDB 사용자가 도큐먼트를 변경할 수 있다. 따라서 findAndModify를 사용하지 않는 한 비록 업데이트가 원자적인 연산일지라도 업데이트 전이나 후의 도큐먼트의 실제 상태를 알 수 없다.
$inc : 수치가 증가하거나 감소할 때 사용한다.
$set : 특정 키에 값을 정해주기 위해 사용한다.
키에 대한 값이 이미 존재하면 그 값을 덮어쓰고, 그렇지 않은 경우 새로운 키가 생성된다.
$unset : 도큐먼트에서 해당 키를 삭제한다.
$rename : 키의 이름을 바꿔야 할 경우에 사용한다.
$push : 배열에 값을 추가한다. 태그를 여러 개 추가하려면 $each를 $push와 함께 사용할 수 있다.
MongoDB v2.4부터 $pushAll은 deprecated 되었다.
$slice : 업데이트 후 배열에 남아 있어야 하는 항목 수를 지정한다.
$addToSet : 배열에 값을 추가하지만 배열에 존재하지 않을 경우에만 값을 추가한다.
하나 이상의 값에 대해서 수행하고자 한다면 $each 연산자와 함께 사용한다.
$pop : 마지막으로 추가된 아이템을 지운다. 배열의 첫 번째 아이템을 지우기 위해 -1이라는 또 다른 값을 받는다.
$pull : 배열에서 원소의 위치 대신 값을 지정해서 아이템을 삭제한다.
$pushAll은 삭제할 값의 리스트를 지정할 수 있다.
컬렉션 전체를 지울수도 있고,쿼리 셀렉터를 매개변수로 넘겨줘서 컬렉션 내의 일부분의 도큐먼트를 지울 수도 있다.
db.reviews.remove({user_id: ObjectId('4c4b1476238d3b4dd5000001')})
db.reviews.remove({})
MongoDB v2.2 이전까지는 잠금 전략이 상당히 정교하지 못했다. (지금도.. 완벽하지는 않은 것 같은데..?)
MongoDB v3.0 에서 도큐먼트 수준의 잠금기능을 제공한다.
MongoDB의 잠금은 읽기가 많고 쓰기가 많은 작업 부하의 성능에 영향을 미칠 수 있다.
문제를 피하는 좋은 방법은 트래픽이 대량으로 발생하는 컬랙션을 별도의 데이터베이스에 저장하는 것이다.
( 이번 프로모션 모델링 때, 위와 같은 이유로 ppn 컬랙션을 생성함.. 개발 시 불편했음..)
$isolated (고립된)
여러 도큐먼트 업데이트에 다른 연산이 끼어드는(interleave) 것을 허용하지 않는다. 즉, 앵보되지 못하도록 한다.
어떤 연산이 수행되기 전에 반드시 모든 도큐먼트를 업데이트하거나 삭제해야만 하는 상황을 쉽게 생각해 볼 수 있다.
$isolated 연산자가 샤드된 컬렉션에서 작동하지 않는다는 사실과 결합하여 생각해 보면 이를 주의해서 사용해야 함을 알 수 있다.