In Matthew의 블로그 하위 문서(하위 문서) API 기능에 대한 간략한 개요를 소개합니다: 요약하면, 하위 문서를 사용하면 문서의 일부에 효율적으로 액세스할 수 있습니다(sub-문서)를 사용하여 전체 문서를 네트워크를 통해 전송할 필요가 없습니다.
이 블로그에서는 참조 문서를 사용하겠습니다. 그런 다음 이 문서는 서브도큐먼트 API를 사용하여 다양한 방법으로 액세스할 수 있습니다. 각 하위 문서 작업에 대해 참고하세요, doc_size - op_size
바이트의 대역폭이 절약되고 있습니다. doc_size
는 문서의 길이이고 op_size
는 경로의 길이와 하위 문서 값입니다.
아래 문서는 500바이트입니다. 간단한 get()
는 서버 응답에서 500바이트(프로토콜 오버헤드 추가)를 소비합니다. 배달 주소만 신경쓰는 경우, 배달 주소에 대한 lookup_in('customer123', SD.get('addresses.delivery'))
호출합니다. 네트워크를 통해 약 120바이트만 수신하게 되므로 동등한 전체 문서(풀독) 작동합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ "name": "더글러스 레이놀름", "이메일": "douglas@reynholmindustries.com", "주소": { "청구": { "line1": "123 애니 스트리트", "line2": "애니타운 ", "country": "영국" }, "delivery": { "line1": "123 애니 스트리트", "line2": "애니타운 ", "country": "영국" } }, "구매": { "완료": [ 339, 976, 442, 666 ], "버려진": [ 157, 42, 999 ] } } |
다음을 사용하여 예제를 시연하겠습니다. 파이썬 SDK의 개발 브랜치 그리고 카우치베이스 서버 4.5 개발자 프리뷰.
[편집: 이제 하위 문서 API의 실험용 버전을 최신 버전에서 사용할 수 있습니다. Python SDK버전 2.0.8로 업데이트되었으며, 아래 예제는 최신 API를 반영하도록 업데이트되었습니다].
Couchbase 4.5의 다른 새로운 기능에 대한 자세한 내용은 다음에서 확인할 수 있습니다. 돈 핀토의 블로그 게시물
하위 문서 작업
하위 문서 작업은 문서의 단일 경로에 대한 단일 작업입니다. 이는 다음과 같이 표현할 수 있습니다. GET('addresses.billing')
또는 ARRAY_APPEND('purchases.abandoned', 42)
. 일부 작업은 다음과 같습니다. 조회 (문서를 수정하지 않고 단순히 데이터를 반환하는 것), 일부는 돌연변이 (문서의 내용을 수정합니다).
대부분의 서브도큐먼트 작업은 풀도큐먼트 작업과 동등한 규모의 소규모 작업입니다. 단일 문서를 그 자체로 작은 키-값 저장소라고 생각하면 도움이 됩니다. Python SDK에서 오퍼레이션을 지정하려면 couchbase.subdocument
모듈을 사용하며, 이 블로그의 나머지 부분에서는 다음과 같이 약칭할 것입니다. SD
. 이는 다음을 통해 수행됩니다.
1 |
가져오기 카우치베이스.하위 문서 as SD |
이러한 작업을 살펴보면서 네트워크를 통해 전송되는 것은 전체 문서의 내용(풀독의 경우처럼)이 아니라 서브독 API 자체에 전달되는 인자뿐이라는 점에 유의하세요. 문서 자체는 작아 보일 수 있지만, 심지어 간단한
조회 작업
조회 작업은 문서에서 특정 경로를 쿼리하여 해당 경로를 반환합니다. 실제로 다음과 같은 경로를 선택할 수 있습니다. 검색 를 사용하여 문서 경로를 GET
작업 또는 간단히 존재 여부 쿼리 를 사용하여 경로의 존재
작업과 비교해보세요. 후자는 필요하지 않은 경우 경로의 내용을 검색하지 않음으로써 더 많은 대역폭을 절약할 수 있습니다.
1 2 |
rv = 버킷.lookup_in('customer123', SD.get('addresses.delivery.country')) 국가 = rv[0] # => '영국' |
1 2 |
rv = 버킷.lookup_in('customer123', SD.존재('purchases.pending[-1]')) rv.존재(0) #(첫 번째 명령의 경로가 존재하는지 확인): =>; False |
두 번째 코드 조각에서는 배열의 마지막 요소에 액세스하는 방법도 보여드리겠습니다. [-1]
경로 컴포넌트입니다.
이 두 가지 작업을 결합할 수도 있습니다:
1 2 3 4 5 6 |
rv = 버킷.lookup_in('customer123', SD.get('addresses.delivery.country'), SD.존재('purchases.pending[-1]')) rv[0] # => '영국' rv.존재(1) # => 거짓 rv[1] # => 하위 문서 경로를 찾을 수 없음 오류 |
돌연변이 연산
변경 작업은 문서에서 하나 이상의 경로를 수정합니다. 이러한 작업은 여러 그룹으로 나눌 수 있습니다:
- 사전/객체 연산: 이러한 작업은 JSON 사전 키의 값을 씁니다.
- 배열/목록 연산: 이 연산은 JSON 배열/목록에 연산을 추가합니다.
- 일반 연산: 이러한 연산은 기존 값 자체를 수정하며 컨테이너에 구애받지 않습니다.
돌연변이 작업은 다음과 같습니다. 전부 또는 전무내의 모든 작업 또는 mutate_in
가 성공했거나 성공하지 못했습니다.
사전 작업
이러한 작업 중 가장 간단한 작업은 UPSERT
. 풀독 수준의 업서트와 마찬가지로 기존 경로의 값을 수정하거나 경로가 존재하지 않는 경우 새로 만듭니다:
1 |
버킷.mutate_in('customer123', SD.업서트('팩스', '775-867-5309')) |
다음과 더불어 UPSERT
에서 삽입
연산은 경로가 존재하지 않는 경우에만 새 값을 경로에 추가합니다.
1 2 |
버킷.mutate_in('customer123', SD.삽입('purchases.complete', [42, True, 없음])) # 하위 문서 경로 존재 오류 |
위의 작업은 실패하지만, 전체 문서 값으로 유효한 것은 하위 문서 값으로도 유효하다는 점에 유의하세요: JSON으로 직렬화할 수만 있다면 말입니다. Python SDK는 위의 값을 다음과 같이 직렬화합니다. [42, true, null]
.
사전 값은 바꾸거나 제거할 수도 있습니다:
1 2 3 |
버킷.mutate_in('customer123', SD.제거('addresses.billing'), SD.대체('이메일', 'doug96@hotmail.com')) |
배열 연산
참 배열 추가 (ARRAY_APPEND
)를 추가하고 (ARRAY_PREPEND
) 작업은 서브도큐를 사용하여 수행할 수도 있습니다. 단순히 기존 값에 바이트를 연결하는 풀독 추가/추가 작업과 달리, 서브독 추가 및 준비 추가는 JSON을 인식합니다:
1 2 |
버킷.mutate_in('customer123', SD.array_append('purchases.complete', 777)) # 구매 완료는 이제 [339, 976, 442, 666, 777] 입니다. |
1 2 |
버킷.mutate_in('customer123', SD.array_prepend('구매.버려진', 18)) # 구매 포기됨[18, 157, 49, 999] |
배열 전용 문서를 만든 다음 다음을 수행할 수도 있습니다. array_
연산은 빈 경로를 사용합니다:
1 2 3 |
버킷.업서트('my_array', []) 버킷.mutate_in('my_array', SD.array_append('', '일부 요소')) # 문서 my_array가 이제 ["일부 요소"]입니다. |
배열을 고유 세트처럼 취급하는 데에도 제한적인 지원이 존재합니다. array_addunique
명령을 실행합니다. 그러면 실제로 항목을 배열에 추가하기 전에 주어진 값이 존재하는지 여부를 확인합니다:
1 2 3 4 5 |
버킷.mutate_in('customer123', SD.array_addunique('purchases.complete', 95)) # => 성공 버킷.mutate_in('customer123', SD.array_addunique('구매.버려진', 42)) # => 하위 문서 경로 존재 예외! |
배열 연산을 효율적인 FIFO 또는 LIFO 큐의 기초로 사용할 수도 있습니다. 먼저 대기열을 만듭니다:
1 |
버킷.업서트('my_queue', []) |
끝에 항목 추가하기
1 |
버킷.mutate_in('my_queue', SD.array_append('', 'job:953')) |
처음부터 아이템을 소비합니다.
1 2 3 4 |
rv = 버킷.lookup_in('my_queue', SD.get('[0]')) job_id = rv[0] 버킷.mutate_in('my_queue', SD.제거('[0]'), cas=rv.cas) run_job(job_id) |
위의 예는 GET
뒤에 제거
. . 제거
은 애플리케이션에 이미 작업이 있는 경우에만 수행되며, 이후 문서가 변경되지 않은 경우에만 성공합니다(대기열의 첫 번째 항목이 방금 제거한 항목이 되도록 하기 위해).
카운터 운영
카운터 조작을 통해 숫자 값으로 변경할 수 있습니다. 이러한 작업은 논리적으로 카운터
작업을 전체 문서에 적용합니다.
1 2 |
rv = 버킷.mutate_in('customer123', SD.카운터('logins', 1)) cur_count = rv[0] # => 1 |
그리고 카운터
연산은 숫자 값에 대한 간단한 산술을 수행합니다(값이 아직 존재하지 않는 경우 값이 생성됩니다).
카운터
도 감소할 수 있습니다:
1 2 3 4 |
버킷.업서트('player432', {'gold': 1000}) rv = 버킷.mutate_in('player432', SD.카운터('gold', -150)) 인쇄('player432에 {0} 골드가 남았습니다'.형식(rv[0])) # => 플레이어 432의 골드 850이 남았습니다. |
카운터 연산에 대한 기존 값은 64비트 부호 있는 정수 범위 내에 있어야 합니다.
중간체 생성
위의 모든 예는 기존 사전 내에서 하나의 새 필드를 만드는 것을 말합니다. 그러나 새 계층 구조를 만들면 오류가 발생합니다:
1 2 3 |
버킷.mutate_in('customer123', SD.업서트('phone.home', {'num': '775-867-5309', 'ext': 16})) # => 하위 문서 경로를 찾을 수 없음 |
이 작업은 UPSERT
를 사용하면 하위 문서는 기본적으로 누락된 계층 구조 생성을 거부합니다. 하위 문서의 create_parents
옵션은 성공할 수 있지만 옵션이 호출되는 프로토콜 수준을 추가하십시오. F_MKDIRP
와 같은 -p
옵션의 mkdir
명령을 실행합니다.
1 2 3 4 |
버킷.mutate_in('customer123', SD.업서트('phone.home', {'num': '775-867-5309', 'ext': 16}, create_parents=True)) |
하위 문서 및 CAS
서브닥은 대부분 추적의 필요성을 없애줍니다. CAS. 하위 문서 작업은 원자적이기 때문에 두 개의 다른 스레드가 두 개의 다른 하위 문서에 액세스해도 충돌이 발생하지 않습니다. 예를 들어 다음 두 블록은 충돌 위험 없이 동시에 실행될 수 있습니다:
1 |
버킷.mutate_in('customer123', SD.array_append('purchases.complete', 999)) |
1 |
버킷.mutate_in('customer123', SD.array_append('구매.버려진', 998)) |
수정할 때에도 동일 부분의 경우, 두 개의 동시 작업과 같이 반드시 충돌하지 않습니다. ARRAY_PREPEND
를 같은 배열에 추가하면 둘 다 성공하며 다른 배열을 덮어쓰지 않습니다.
그렇다고 해서 CAS가 더 이상 필요하지 않다는 의미는 아닙니다. 전체 문서 가 마지막 작업 이후 상태가 변경되지 않은 경우: 이는 특히 다음과 같은 경우에 중요합니다. 제거
작업을 수행하여 제거되는 요소가 이미 다른 요소로 대체되지 않았는지 확인합니다.
카우치베이스의 하위 문서 작업에 대한 FAQ
서브닥을 개발하는 과정에서 서브닥의 기능에 대해 몇 가지 질문을 받았는데, 차례로 답변해 드리겠습니다:
Subdoc과 N1QL의 차이점은 무엇인가요?
N1QL은 풍부하고 표현력이 풍부한 쿼리 언어로 다음을 검색하고 변경할 수 있습니다. 여러 문서 을 한 번에 검색할 수 있습니다. Subdoc은 한 문서 내에서 검색할 수 있도록 설계된 고성능 API/구현입니다. 단일 문서.
하위 문서는 고성능 의 간단하고 신중한 API 단일 문서 내에서 데이터에 액세스하는 데 필요한 시간을 줄이기 위해 네트워크 대역폭 전체 처리량을 증가시킵니다. 이 서비스는 KV 서비스의 일부로 구현되었기 때문에 이 서비스와의 일관성이 매우 높습니다.
N1QL은 풍부한 쿼리 언어 특정 기준을 준수하는 Couchbase 내의 여러 문서를 검색할 수 있습니다. 작동 방식 외부 를 사용하여 들어오는 쿼리를 충족하기 위해 최적화된 KV 및 인덱스 요청을 생성합니다. KV 서비스와의 일관성은 쿼리별로 구성할 수 있습니다(예를 들어, 쿼리당 사용 키
조항과 스캔 일관성
옵션).
N1QL은 언제 사용해야 하고 서브닥은 언제 사용해야 하나요?
N1QL은 다음과 같은 질문에 대한 답변을 제공합니다. X=42, Y=77인 모든 문서 찾기 하위 문서는 다음과 같은 질문에 대한 답변입니다. 문서 Z에서 X와 Y 가져오기. 보다 구체적으로는 모든 문서 ID를 알고 있는 경우(즉, N1QL 쿼리에 다음이 포함된 경우) subdoc을 사용해야 합니다. 사용 키
하위 문서 후보가 될 수 있습니다).
그러나 이 둘은 상호 배타적인 것이 아니며 애플리케이션에서 N1QL과 하위 문서를 모두 사용할 수 있습니다.
Are mutate_in
그리고 lookup_in
원자?
예, 원자 연산입니다. 이 두 작업 모두 모든 하위 명령(예 카운터
, GET
, 존재
, ADD_UNIQUE
)는 동일한 버전의 문서에서 작동합니다.
하위 문서로 여러 문서에 액세스하려면 어떻게 하나요?
선의는 없습니다. 멀티 연산은 하위 문서가 단일 문서의 범위 내에서 작동하기 때문입니다. 문서가 클러스터 전체에 걸쳐 샤딩되기 때문에(이는 Couchbase와 다른 모든 NoSQL 저장소에 공통적으로 적용됨), 다중 작업은 문서 간에 동일한 수준의 트랜잭션과 원자성을 보장할 수 없습니다.
배열의 명명 규칙이 마음에 들지 않습니다. 왜 추가
, 추가
등?
시중에는 많은 언어가 있으며 배열 액세스 함수를 호출하는 방법에 대해 모두 다른 생각을 가지고 있는 것 같습니다:
- 일반: 끝에 추가, 앞에 추가
- C++:
push_back()
,push_front()
- Python:
append()
,삽입(0)
,확장
- Perl, Ruby, 자바스크립트, PHP:
push()
,unshift()
- Java, C#:
add()
용어 추가
는 이미 전체 문서 바이트 연결을 지칭하기 위해 Couchbase에서 사용되고 있으므로 하위 문서에서 이 용어를 또 다른 방식으로 사용하는 것은 일관성이 없다고 생각했습니다.
왜 카운터
64비트 부호 있는 정수가 필요합니까?
이는 서브도큐먼트 코드가 C++로 구현된 결과입니다. 향후 구현에서는 더 넓은 범위의 기존 숫자 값(예: 큰 값, 적분이 아닌 값 등)을 허용할 수 있습니다.
어떻게 수행하나요? 팝왜 없나요? POP
작동합니까?
POP
는 한 번의 작업으로 배열에서 항목을 제거하고 반환하는 작업을 의미합니다.
POP
는 향후 실제로 구현될 수 있지만, 이를 사용하는 것은 본질적으로 위험합니다:
이 작업은 네트워크를 통해 수행되므로 서버가 항목 제거를 실행했지만 클라이언트가 이전 값을 받기 전에 네트워크 연결이 종료될 수 있습니다. 이 경우 해당 값은 더 이상 문서에 존재하지 않으므로 영구적으로 손실됩니다.
하위 문서 작업에 CAS를 사용할 수 있나요?
예, CAS 사용과 관련하여 하위 문서 작업은 다음과 유사한 일반적인 KV API 작업입니다. 업서트
, get
등
하위 문서 작업에 내구성 요구 사항을 사용할 수 있나요?
예, 내구성 요구 사항과 관련하여 그렇습니다, mutate_in
은 다음과 같이 표시됩니다. 업서트
, 삽입
그리고 대체
.