[Ch3] 도큐먼트 생성, 갱신, 삭제 (갱신 1)

2022. 7. 12. 15:48책/몽고DB 완벽 가이드

3. 갱신

도큐먼트 갱신에는 크게 치환과 수정이 있다(각각 replaceOne / updateOne, updateMany ) 갱신은 원자적으로 이뤄지므로 갱신 요청 두 개가 동시에 발생하면 서버에 먼저 도착한 요청이 적용된 후 다음 요청이 적용된다.

 

만약 이러한 기본 동작이 마음에 들지 않으면 도큐먼트 버저닝 패턴 The Document Versioning Pattern을 고려하면 된다(9장에서 나올 예정)

 

3.1 replaceOne( filter, replacement, options )

특정 도큐먼트를 다른 도큐먼트로 아예 대체시킨다. 이때 "_id"가 동일한 도큐먼트가 존재하지 않도록 주의해야 한다.

 

3.1.1. 직접 해보기

const temp = db.users.findOne({"name": "존잘"});
temp.relationships = { "friends": temp.friends, "enemies": temp.enemies };
// 현재 temp는 아래와 같은 상태
// { "_id" : 1111, "name" : "존잘", "friends" : 1, "enemies" : 10,
//    "relationships" : { "friends" : 1, "enemies" : 10 } }

delete temp.friends;
delete temp.enemies;

db.users.replaceOne({ "name": "존잘" }, temp);

3.1.2. 주의사항

상황) 24살 존잘 친구가 25살이 됐다고 하자. 2번째 도큐먼트를 새롭게 치환할 것이다

const joe = db.people.findOne({ "name": "존잘", "age": 24 }); // 1번
joe.age++ // 2번. age가 25가 된다
db.people.replaceOne({ "name": "존잘" }, joe); // 3번
 // 필터와 일치하는 첫 번째 도큐먼트를 찾으므로 60세 존잘이 잡힌다
 // "_id": 1111이던 도큐먼트를 "_id": 2222인 도큐먼트로 치환한다
 // "_id": 2222는 이미 있으므로 중복 에러로 실행되지 않는다

결론) 애초에 중복될 소지가 없도록 도큐먼트 쿼리를 잘하셈;;

 

3.2 updateOne(filter, update, options)

일반적으로 도큐먼트 갱신시 일부분만 업데이트하기 마련이다. 이때 부분 갱신을 위해 갱신 연산자 update operator란 특수키를 사용한다. 갱신 연산자를 통해 키의 변경, 추가, 제거, 배열과 내장 도큐먼트 조작이 가능하다.

종류가 워낙 많으니 필요하다면 공식문서를 확인해보도록 하고, 여기선 몇 가지 중요한 연산자들만 살펴보자

※ 아래 연산자들을 설명할 때, 책에선 JSON 형식을 유지하면서 예시를 작성한다. 예를 들어 $set : { title : "ㅎㅇ" }라는 문이 있다면 "$set" : { "title": "ㅎㅇ" }처럼 " "를 씌워주는 형식이다.

 

그러나 몽고 DB쉘 자체가 JS 런타임이고, 도큐먼트는 그냥 객체 덩어리이므로 굳이 " "이 필요한 건 아니다. 실제로 공식문서에선 쌍따옴표 다 생략하고 설명하고 있다.

 

더보기

# db.collection.updateOne( <filter>, <update>, {

  upsert: <boolean>,

  writeConcern: <document>,

  collation: <document>,

  arrayFilters: [ <filterdocument1>, ... ],  

  hint: <document|string> // Available starting in MongoDB 4.2.1

})

  • filter <document > : 업데이트할 도큐먼트를 지정할 쿼리문
  • update <document | piplelin >: 어떻게 업데이트할 것인지 지정.
     - Update document : 갱신 연산자 표현식을 이용
     - Aggregation pipeline : aggregation stage를 이용
  • upsert : 필터에 해당하는 도큐먼트가 없을 시 새로 추가할 지 결정
  • arrayFilters : 배열의 특정 요소들을 갱신하기 위해 사용

 

 

3.2.1. Field

◎ $set

{ $set: { <field1>: <value1>, ... } }
 
  • 도큐먼트의 필드 값을 바꾸기 위해 사용한다.
  • 만약 필드가 존재하지 않는다면 새 필드가 생성된다.

1) 기본형태

도큐먼트에 joe가 좋아하는 야한 책 정보를 넣어보자.

db.users.find() 
// { "_id" : 1111, "name": "joe", "age" : 30 }

const joe = db.users.findOne({ "name" : "joe", "age" : 30 });
// joe = { "_id" : 1111, "name": "joe", "age" : 30 }

db.users.updateOne(joe, { "$set" : { "like book": "야한 책" } });

db.users.find().pretty();
// { "_id": 1111, "name": "joe", "age": 30, "like book": "야한 책" }

 

2) 데이터형 변경

$set은 키의 데이터형도 변경할 수 있다. 예를 들어, 위 예시에서 joe는 like book으로 야한 책 하나만 좋아하지만 취향이 여럿이라면 배열 형태로 바꿔서 갱신할 수도 있다. 

joe는 사실 야한책 말고도 좋아하는게 많다

const joe = db.users.findOne({ "name" : "joe", "age" : 30 });

db.users.updateOne(joe, { "$set" : { "like book": ["야한 책", "라노벨 책", "씹덕 책"] } });

 

3) dot notation

만약 내장 도큐먼트나 배열 내부를 바꾸고 싶다면 dot notation을 사용해 데이터를 변경할 수 있다.

예를 들어 이렇게 생긴 도큐먼트가 있고, auther의 name을 바꾸고싶다면

db.books.find();
{ "title": "야한 책", "autor" : { "name": "진심인 자", "email": "진심@naver.com" } }

let book = db.books.findOne({ "title": "야한 책" });

db.books.updateOne(book, { "$set" : { "auther.name" : "오히려 좋아" } });

 

$unset

{ $unset: { <field1>: "", ... } }
  • 도큐먼트의 특정 필드를 제거할 때 사용한다.
  • 필드 제거 시 작성하는 " "는 아무 역할도 없고, 표현의 통일성을 위해 작성해줘야 하는 것뿐이다.

예를 들어 joe가 사실 좋아하는 책이 하나도 없다고 말한다면(구라일듯), 
도큐먼트의 "like book" 필드를 $unset을 이용해 없애줄 수 있다. 

const joe = db.users.findOne({ "name" : "joe", "age" : 30 });

db.users.updateOne(joe, { "$unset" : { "like book": "" } });

 

◎ $inc

{ $inc: { <field1>: <amount1>, <field2>: <amount2>, ... } }
 
  • 필드값을 지정한 값만큼 증가시킨다.
  • $set과 유사하게 도큐먼트內 해당 필드 값이 없다면 새로운 필드를 추가 후 값을 더해준다.
  • dot notation 또한 사용할 수 있다.
  • 당연한 소리지만 숫자를 증가시키므로 int, long, double, decimal 타입 값에만 쓸 수 있다.

예를 들어 핀볼 게임을 하나 만들었다 치고 
벽을 때리면 50점, 보너스 슬롯에 골인시키면 10,000점을 준다고 하자. 
그럼 아래와 같은 방식으로 쓸 수 있다.

db.games.insertOne({ "game":"pinball" });

let pinball = db.games.findOne({ "game": "pinball"});

// 스코어 필드는 따로 없지만 $inc가 알아서 추가해줄 듯
let touchBlock = function() {
  db.games.updateOne(pinball, { "$inc": { "score": 50 } });
}
let inBonusSlot = function() { 
  db.games.updateOne(pinball, { "$inc": { "score": 10000 }});
}

touchBlock();
inBonusSlot();

db.games.find();
{ "_id" : ObjectId("62cd1a8d8eb722812b42f6d9"), 
  "game" : "pinball", 
  "user" : "joe", 
  "score" : 10050 }