사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
일반▾
인터페이스▾
관측성▾
성능▾
네이티브▾
개발▾

데이터 변경

조회수를 하나 올리거나, 여러 행을 한꺼번에 보관 처리하거나, 사용자가 연 레코드 하나를 고쳐야 할 때가 있습니다. 쓰는 길은 두 가지이고, 어느 쪽을 고르느냐에 따라 훅이 실행되는지가 갈립니다.
문서 경로
문서를 읽어 와 고친 뒤 저장합니다. 그래서 훅이 실행되고, 삭제라면 cascade도 함께 일어납니다.
await this.updatePost(id, data);
쿼리 쓰기
SQL 문 하나를 원자적으로 보내고 아무것도 읽지 않습니다. 빠르고 동시 요청에도 안전하지만 훅은 실행되지 않습니다.
await this.Post.updateOne(filter, change);
무엇이 실행되는가
문서마다 훅이나 cascade가 돌아야 한다면 문서 경로를 씁니다. 아래 표에는 세 가지 말이 나옵니다:
schema.preschema.post
스키마 훅입니다. _onSchema에 등록하고, 문서를 저장하거나 삭제할 때마다 실행됩니다.
_preCreate_postCreate_preUpdate_postUpdate_preRemove_postRemove
서비스 훅입니다. service에 override하며, 생성된 create<Model>, update<Model>, remove<Model>을 거칠 때만 실행됩니다.
cascade
field에 선언하는 연쇄 삭제입니다. 지운 문서에 딸린 문서가 함께 지워집니다.
메서드
스키마 훅
서비스 훅
cascade
문서 경로: 읽고, 고치고, 저장
update<Model>(id, data)
✓
✓
service에 생성되는 메서드입니다. 레코드 하나를 고칠 때 기본으로 씁니다.
remove<Model>(id)
✓
✓
✓
service에 생성되는 메서드입니다. 문서를 soft 삭제한 뒤 cascade를 실행합니다.
pickAndWrite(id, data)
✓
model에서 문서를 집어 값을 넣고 저장합니다. pickOneAndWrite(query, data)는 쿼리로 집습니다.
doc.set(data).save()
✓
이미 문서를 들고 있을 때 같은 일을 직접 쓰는 방식입니다.
쿼리 쓰기: SQL 문 하나, 아무것도 읽지 않음
updateOne · updateMany
가장 최근에 만든 행 하나, 또는 맞는 행 전부를 고칩니다.
removeOne · removeMany
가장 최근에 만든 행 하나, 또는 맞는 행 전부를 soft 삭제합니다.
updateById · removeById
같은 쿼리 쓰기를 id 하나로 좁힌 것입니다.
update<Filter>(…).set(…)
필터마다 생성됩니다. remove<Filter>, updateOne<Filter>, removeOne<Filter>도 같습니다.
bulkWrite(operations)
updateOne 작업 목록을 하나씩 차례로 실행합니다.
✓실행됨실행되지 않음

두 가지 작성법

쿼리 쓰기는 모두 필터를 먼저 받고, 그다음에 바꿀 내용을 받습니다. 바꿀 내용은 객체나 builder 함수로 씁니다:
형태언제예
객체값만 넣을 때 씁니다. 값을 그대로 쓰면 set과 같습니다.{ status: "published", pinned: true }
builder 함수inc, addToSet 같은 연산자가 필요할 때 씁니다.({ inc }) => ({ viewNum: inc(1) })
model 클래스 안에서는 이렇게 씁니다:
apps/myapp/lib/post/post.document.ts
  • builder가 받는 인자에 연산자가 모두 들어 있습니다. 필터에서 q가 조건을 담는 것과 같습니다. 쓰는 것만 꺼내면 되고, import할 것은 없습니다.
  • key는 field 경로입니다. "profile.city"처럼 쓰면 객체 field 안쪽에 쓰고, 선언하지 않은 field를 가리키면 에러가 납니다.
  • 기본 컬럼 네 개에는 set과 unset만 씁니다. id, createdAt, updatedAt, removedAt은 inc를 비롯한 다른 연산자를 거절합니다.

카운터와 집합

숫자와 배열 연산자는 데이터베이스 안에서 실행됩니다. 같은 카운터를 동시에 올리는 두 요청도 둘 다 반영되고, 서로의 변경을 덮어쓰지 않습니다:
apps/myapp/lib/post/post.document.ts
  • 숫자: inc, mul, min, max. field가 없으면 inc와 mul은 0에서 시작하고, min과 max는 값을 그대로 씁니다. 인자 없는 inc()는 1을 더합니다.
  • 배열: push, addToSet, pull. 배열이 없으면 빈 배열에서 시작합니다. push는 항상 붙이고, addToSet은 없을 때만 붙이며, pull은 같은 요소를 모두 뺍니다.
  • updateMany는 건드린 행 수를 modifiedCount로 돌려줍니다. SQL 문 하나로 모두 처리됩니다.

Upsert

upsert는 맞는 행을 고치고, 맞는 행이 없으면 새 행을 넣습니다. 세 번째 인자로 { upsert: true }를 넘깁니다:
apps/myapp/lib/stat/stat.document.ts
맞는 행이 없으면 호출의 각 부분은 이렇게 쓰입니다:
호출의 부분새 행에서는
{ key: "daily-visits" }필터에 값으로 쓴 조건은 새 행에 그대로 들어갑니다. q.oneOf() 같은 조건은 빠집니다.
total: inc(1)연산자는 빈 값에 적용되므로 total은 1에서 시작합니다.
status: setOnInsert("active")이 삽입 때만 쓰입니다. 맞는 행을 찾아 고칠 때는 무시됩니다.
반환값upsertedId에 새 id가 담기고 matchedCount는 0입니다.
  • upsert는 updateOne, updateById, bulkWrite에서만 됩니다. updateMany는 옵션을 받지 않습니다.
  • 삽입할 때는 "create" 스키마 훅만 실행됩니다. "save" 훅과 _postCreate 같은 서비스 훅은 실행되지 않습니다.

SQL로 바뀌는 방식

모든 연산자는 _doc 컬럼 위의 JSON 표현식 하나로 겹쳐지고, 쓸 때마다 updatedAt이 갱신됩니다. 데이터베이스는 이 변경 전체를 SQL 문 하나로 적용합니다.
값 그대로
field에 값을 넣습니다. set의 줄임입니다.
set(value)
field에 값을 넣습니다.
unset()
field를 지웁니다.
inc(by = 1)
by만큼 더합니다. field가 없으면 0에서 시작합니다.
mul(by)
by를 곱합니다. field가 없으면 0으로 봅니다.
min(value)
저장된 값과 value 중 작은 쪽을 남깁니다.
max(value)
저장된 값과 value 중 큰 쪽을 남깁니다.
push(value)
배열 끝에 붙입니다. 배열이 없으면 빈 배열에서 시작합니다.
addToSet(value)
같은 요소가 아직 없을 때만 붙입니다.
pull(value)
value와 같은 요소를 모두 뺍니다.
setOnInsert(value)
upsert가 새 행을 넣을 때만 field에 값을 넣습니다.
중첩 경로
점으로 이은 key는 객체 field 안쪽에 씁니다.
여러 연산 함께
연산자 여러 개가 표현식 하나로 겹쳐집니다.
  • 개념만 보이도록 줄인 SQLite 기준 SQL입니다. Postgres는 같은 일을 하는 jsonb 함수를 씁니다.
  • 모든 연산자는 변경 전 문서를 읽습니다. 그래서 한 호출 안의 변경은 모두 같은 원래 값을 봅니다.

팁과 주의할 점

  • 카운터와 대량 상태 변경에는 쿼리 쓰기를 씁니다. 단, 삭제 부수효과가 없는 model에서만입니다. 훅, cascade, 도메인 로직이 돌아야 한다면 문서 경로를 쓰세요.
  • 원자적 쓰기는 model 클래스에 둡니다. <model>.document.ts에 쓰고 !!modifiedCount를 돌려주면 service는 성공 여부만 받습니다.
  • builder 형태를 쓰세요. update 헬퍼를 module scope에서 import하지 않습니다.
  • q.search()는 쓰기의 필터에 쓸 수 없습니다. 검색이 들어간 필터로 쓰면 에러가 나므로, id를 먼저 찾은 뒤 id로 씁니다.
  • bulkWrite는 작업을 하나씩 실행합니다. 작업마다 SQL 문이 따로 실행되고, 결과의 개수는 합산됩니다.
쓰기가 돌려주는 값
쿼리 쓰기는 모두 같은 모양의 결과를 돌려줍니다. 쓰기가 반드시 어떤 행에 닿아야 한다면 modifiedCount를 확인하세요:
acknowledgedboolean
SQL 문이 실행되면 true입니다.
matchedCountnumber
필터에 맞은 행 수입니다. upsert로 새로 넣었다면 0입니다.
modifiedCountnumber
쓰기가 바꾼 행 수입니다. upsert로 넣은 행도 셉니다.
upsertedIdstring | nulloptional
upsert로 새 행을 넣었을 때의 id입니다. 아니면 null이거나 없습니다.
이어서 읽기

MIT 라이선스 하에 배포되었습니다.

내 AI에 이 문서 연결하기

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js 모든 권리 보유.시스템 관리자bassman