Session 확장
Session 확장 (The Session Extension)
소개 (Introduction)
세션(session) 확장은 SQLite 데이터베이스의 테이블에 대한 변경을 기록하고, 그 변경을 "changeset"(변경 집합) 또는 "patchset"(패치 집합) 파일로 묶은 다음, 같은 스키마와 호환되는 시작 데이터를 가진 다른 데이터베이스에 같은 변경 집합을 나중에 적용하는 편리한 메커니즘을 제공해요. "changeset"은 또한 뒤집어서 세션을 "실행 취소"하는 데 사용할 수도 있어요.
이 문서는 세션 확장에 대한 소개예요. 인터페이스의 세부 사항은 별도의 세션 확장 C 언어 인터페이스 문서에 있어요.
출처: 문서
본문
1. 전형적인 사용 사례 (Typical Use Case)
SQLite가 특정 설계 애플리케이션의 애플리케이션 파일 형식으로 사용된다고 가정해 보세요. 두 사용자 Alice와 Bob은 각각 약 1기가바이트 크기의 기준 설계(baseline design)로 시작해요. 그들은 하루 종일 병렬로 작업하며 각자 설계에 자신만의 사용자화와 조정을 해요. 하루가 끝나면 그들은 자신들의 변경 사항을 단일 통합 설계로 병합하고 싶어해요.
세션 확장은 Alice와 Bob의 데이터베이스에 대한 모든 변경을 기록하고 그 변경을 changeset 또는 patchset 파일로 써서 이것을 가능하게 해요. 하루가 끝나면 Alice가 자신의 changeset을 Bob에게 보내고 Bob은 그것을 자신의 데이터베이스에 "적용"(apply)할 수 있어요. (충돌이 없다고 가정하면) 결과는 Bob의 데이터베이스에 자신의 변경과 Alice의 변경이 모두 포함되는 것이에요. 마찬가지로 Bob도 자신의 작업 changeset을 Alice에게 보낼 수 있고 Alice는 그의 변경을 자신의 데이터베이스에 적용할 수 있어요.
즉, 세션 확장은 SQLite 데이터베이스 파일에 대해 unix patch 유틸리티 프로그램이나 Fossil, Git, Mercurial 같은 버전 관리 시스템의 "merge" 기능과 유사한 기능을 제공해요.
2. 세션 확장 얻기 (Obtaining the Session Extension)
버전 3.13.0(2016-05-18)부터 세션 확장은 SQLite amalgamation 소스 배포에 포함됐어요. 기본적으로 세션 확장은 비활성화돼요. 활성화하려면 다음 컴파일러 스위치로 빌드해요.
-DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_PREUPDATE_HOOK
또는 표준 빌드 시스템을 사용한다면 configure 스크립트에 --enable-session 옵션을 전달해요.
3. 제한 사항 (Limitations)
- SQLite 버전 3.17.0 이전에는 세션 확장이 rowid 테이블에서만 동작하고 WITHOUT ROWID 테이블에서는 동작하지 않았어요. 3.17.0부터 rowid와 WITHOUT ROWID 테이블이 모두 지원돼요. 하지만 WITHOUT ROWID 테이블 변경에 대한 기본 키를 기록하려면 추가 단계가 필요해요.
- 가상 테이블에 대한 지원은 없어요. 가상 테이블에 대한 변경은 캡처되지 않아요.
- 세션 확장은 선언된 PRIMARY KEY가 있는 테이블에서만 동작해요. 테이블의 PRIMARY KEY는 INTEGER PRIMARY KEY(rowid 별칭)이거나 외부 PRIMARY KEY일 수 있어요.
- SQLite는 PRIMARY KEY 열에 NULL 값을 저장할 수 있게 해요. 하지만 세션 확장은 그런 행을 모두 무시해요. PRIMARY KEY 열에 NULL 값이 하나 이상 있는 행에 영향을 주는 변경은 세션 모듈에 의해 기록되지 않아요.
개념 (Concepts)
1. Changeset과 Patchset (Changesets and Patchsets)
세션 모듈은 changeset을 만들고 조작하는 것을 중심으로 해요. changeset은 데이터베이스에 대한 일련의 변경을 인코딩하는 blob 데이터예요. changeset의 각 변경은 다음 중 하나예요.
- INSERT. INSERT 변경은 데이터베이스 테이블에 추가할 단일 행을 포함해요. INSERT 변경의 페이로드(payload)는 새 행의 각 필드 값으로 구성돼요.
- DELETE. DELETE 변경은 기본 키 값으로 식별되는, 데이터베이스 테이블에서 제거할 행을 나타내요. DELETE 변경의 페이로드는 삭제된 행의 모든 필드 값으로 구성돼요.
- UPDATE. UPDATE 변경은 기본 키 필드로 식별되는, 데이터베이스 테이블 내 단일 행의 하나 이상의 비-PRIMARY KEY 필드 수정을 나타내요. UPDATE 변경의 페이로드는 다음으로 구성돼요.
- 수정된 행을 식별하는 PRIMARY KEY 값,
- 행의 각 수정된 필드에 대한 새 값, 그리고
- 행의 각 수정된 필드에 대한 원래 값.
UPDATE 변경은 변경에 의해 수정되지 않은 비-PRIMARY KEY 필드에 대한 어떤 정보도 포함하지 않아요. UPDATE 변경이 PRIMARY KEY 필드에 대한 수정을 지정하는 것은 불가능해요.
단일 changeset은 둘 이상의 데이터베이스 테이블에 적용되는 변경을 포함할 수 있어요. changeset이 적어도 하나의 변경을 포함하는 각 테이블에 대해 다음 데이터도 인코딩돼요.
- 데이터베이스 테이블의 이름,
- 테이블이 가진 열의 수, 그리고
- 그 열 중 어느 것이 PRIMARY KEY 열인지.
Changeset은 changeset에 저장된 위 세 가지 기준과 일치하는 테이블을 포함하는 데이터베이스에만 적용될 수 있어요.
patchset은 changeset과 유사해요. changeset보다 약간 더 컴팩트하지만 더 제한된 충돌 감지 및 해결 옵션을 제공해요(자세한 내용은 다음 절 참조). patchset과 changeset의 차이는 다음과 같아요.
- DELETE 변경의 경우 페이로드는 PRIMARY KEY 필드만으로 구성돼요. 다른 필드의 원래 값은 patchset의 일부로 저장되지 않아요.
- UPDATE 변경의 경우 페이로드는 PRIMARY KEY 필드와 수정된 필드의 새 값만으로 구성돼요. 수정된 필드의 원래 값은 patchset의 일부로 저장되지 않아요.
2. 충돌 (Conflicts)
changeset이나 patchset이 데이터베이스에 적용될 때 각 INSERT 변경에 대해 새 행을 삽입하고, 각 DELETE 변경에 대해 행을 제거하며, 각 UPDATE 변경에 대해 행을 수정하려는 시도가 이루어져요. 대상 데이터베이스가 changeset이 기록된 원래 데이터베이스와 같은 상태라면 이는 간단한 문제예요. 하지만 대상 데이터베이스의 내용이 정확히 이 상태가 아니라면 changeset이나 patchset을 적용할 때 충돌이 발생할 수 있어요.
INSERT 변경을 처리할 때 다음 충돌이 발생할 수 있어요.
- 대상 데이터베이스가 INSERT 변경이 지정하는 것과 같은 PRIMARY KEY 값을 가진 행을 이미 포함할 수 있어요.
- UNIQUE나 CHECK 제약 조건 같은 다른 데이터베이스 제약 조건이 새 행이 삽입될 때 위반될 수 있어요.
DELETE 변경을 처리할 때 다음 충돌이 감지될 수 있어요.
- 대상 데이터베이스에 삭제할 지정된 PRIMARY KEY 값을 가진 행이 없을 수 있어요.
- 대상 데이터베이스에 지정된 PRIMARY KEY 값을 가진 행이 있지만, 다른 필드가 changeset의 일부로 저장된 값과 일치하지 않는 값을 포함할 수 있어요. 이 유형의 충돌은 patchset을 사용할 때는 감지되지 않아요.
UPDATE 변경을 처리할 때 다음 충돌이 감지될 수 있어요.
- 대상 데이터베이스에 수정할 지정된 PRIMARY KEY 값을 가진 행이 없을 수 있어요.
- 대상 데이터베이스에 지정된 PRIMARY KEY 값을 가진 행이 있지만, 변경에 의해 수정될 필드의 현재 값이 changeset 내에 저장된 원래 값과 일치하지 않을 수 있어요. 이 유형의 충돌은 patchset을 사용할 때는 감지되지 않아요.
- UNIQUE나 CHECK 제약 조건 같은 다른 데이터베이스 제약 조건이 행이 업데이트될 때 위반될 수 있어요.
충돌 유형에 따라 세션 애플리케이션은 충돌하는 변경을 생략하거나, 전체 changeset 적용을 중단하거나, 충돌에도 불구하고 변경을 적용하는 것에 이르기까지 다양한 구성 가능한 충돌 처리 옵션을 가져요. 자세한 내용은 sqlite3changeset_apply() API 문서를 참조하세요.
3. Changeset 구성 (Changeset Construction)
세션 객체가 구성되면 그것은 구성된 테이블에 대한 변경을 모니터링하기 시작해요. 하지만 데이터베이스의 행이 수정될 때마다 전체 변경을 기록하지는 않아요. 대신 삽입된 각 행에 대해 PRIMARY KEY 필드만, 그리고 업데이트되거나 삭제된 행에 대해 PRIMARY KEY와 모든 원래 행 값만 기록해요. 단일 세션에 의해 행이 두 번 이상 수정되면 새 정보는 기록되지 않아요.
changeset이나 patchset을 만드는 데 필요한 다른 정보는 sqlite3session_changeset() 또는 sqlite3session_patchset()이 호출될 때 데이터베이스 파일에서 읽어져요. 구체적으로,
- INSERT 연산의 결과로 기록된 각 기본 키에 대해 세션 모듈은 일치하는 기본 키를 가진 행이 여전히 테이블에 있는지 확인해요. 있으면 INSERT 변경이 changeset에 추가돼요.
- UPDATE나 DELETE 연산의 결과로 기록된 각 기본 키에 대해 세션 모듈은 테이블 내에 일치하는 기본 키를 가진 행도 확인해요. 찾을 수 있지만 비-PRIMARY KEY 필드 중 하나 이상이 원래 기록된 값과 일치하지 않으면 UPDATE가 changeset에 추가돼요. 또는 지정된 기본 키를 가진 행이 전혀 없으면 DELETE가 changeset에 추가돼요. 행이 존재하지만 비-PRIMARY KEY 필드 중 어느 것도 수정되지 않았다면 changeset에 추가되는 변경은 없어요.
위의 한 가지 의미는 단일 세션 내에서 변경이 이루어졌다가 다시 취소되면(예를 들어 행이 삽입된 후 다시 삭제되면) 세션 모듈이 어떤 변경도 보고하지 않는다는 것이에요. 또는 같은 세션 내에서 행이 여러 번 업데이트되면 모든 업데이트가 changeset이나 patchset blob 내의 단일 업데이트로 합쳐져요.
세션 확장 사용 (Using The Session Extension)
이 절은 세션 확장을 사용하는 방법을 보여주는 예제를 제공해요.
1. Changeset 캡처하기 (Capturing a Changeset)
아래 예제 코드는 SQL 명령을 실행하면서 changeset을 캡처하는 단계를 보여줘요. 요약하면:
-
sqlite3session_create() API 함수를 호출해 세션 객체(sqlite3_session* 타입)를 만들어요. 단일 세션 객체는 단일 sqlite3* 데이터베이스 핸들을 통해 단일 데이터베이스(즉 "main", "temp" 또는 첨부된 데이터베이스)에 대해 이루어진 변경을 모니터링해요.
-
세션 객체는 변경을 모니터링할 테이블 집합으로 구성돼요. 기본적으로 세션 객체는 어떤 데이터베이스 테이블의 변경도 모니터링하지 않아요. 그렇게 하려면 먼저 구성되어야 해요. 변경을 모니터링할 테이블 집합을 구성하는 방법은 세 가지가 있어요.
- 각 테이블에 대해 sqlite3session_attach()를 한 번 호출해 테이블을 명시적으로 지정하거나,
- NULL 인수로 sqlite3session_attach()를 한 번 호출해 데이터베이스의 모든 테이블이 변경을 모니터링되도록 지정하거나,
- 각 테이블이 처음 쓰여질 때 호출되어 그 테이블의 변경이 모니터링되어야 하는지 여부를 세션 모듈에 나타내는 콜백을 구성하는 것.
아래 예제 코드는 위에 열거된 방법 중 두 번째를 사용해요. 모든 데이터베이스 테이블의 변경을 모니터링해요.
-
SQL 문을 실행해 데이터베이스에 변경을 가해요. 세션 객체는 이 변경을 기록해요.
-
sqlite3session_changeset()(또는 patchset을 사용한다면 sqlite3session_patchset() 함수)를 호출해 세션 객체에서 changeset blob을 추출해요.
-
sqlite3session_delete() API 함수를 호출해 세션 객체를 삭제해요.
changeset이나 patchset을 추출한 후 세션 객체를 삭제할 필요는 없어요. 그것을 데이터베이스 핸들에 첨부된 채로 두면 구성된 테이블의 변경을 계속 모니터링할 수 있어요. 하지만 세션 객체에 sqlite3session_changeset()이나 sqlite3session_patchset()을 두 번째로 호출하면 changeset이나 patchset은 세션이 생성된 이후 연결에서 일어난 모든 변경을 포함할 거예요. 즉, 세션 객체는 sqlite3session_changeset()이나 sqlite3session_patchset() 호출에 의해 재설정되거나 0으로 만들어지지 않아요.
/*
** Argument zSql points to a buffer containing an SQL script to execute
** against the database handle passed as the first argument. As well as
** executing the SQL script, this function collects a changeset recording
** all changes made to the "main" database file. Assuming no error occurs,
** output variables (*ppChangeset) and (*pnChangeset) are set to point
** to a buffer containing the changeset and the size of the changeset in
** bytes before returning SQLITE_OK. In this case it is the responsibility
** of the caller to eventually free the changeset blob by passing it to
** the sqlite3_free function.
**
** Or, if an error does occur, return an SQLite error code. The final
** value of (*pChangeset) and (*pnChangeset) are undefined in this case.
*/
int sql_exec_changeset(
sqlite3 *db, /* Database handle */
const char *zSql, /* SQL script to execute */
int *pnChangeset, /* OUT: Size of changeset blob in bytes */
void **ppChangeset /* OUT: Pointer to changeset blob */
){
sqlite3_session *pSession = 0;
int rc;
/* Create a new session object */
rc = sqlite3session_create(db, "main", &pSession);
/* Configure the session object to record changes to all tables */
if( rc==SQLITE_OK ) rc = sqlite3session_attach(pSession, NULL);
/* Execute the SQL script */
if( rc==SQLITE_OK ) rc = sqlite3_exec(db, zSql, 0, 0, 0);
/* Collect the changeset */
if( rc==SQLITE_OK ){
rc = sqlite3session_changeset(pSession, pnChangeset, ppChangeset);
}
/* Delete the session object */
sqlite3session_delete(pSession);
return rc;
}
2. Changeset을 데이터베이스에 적용하기 (Applying a Changeset to a Database)
changeset을 데이터베이스에 적용하는 것은 changeset을 캡처하는 것보다 더 단순해요. 보통 아래 예제 코드에 묘사된 대로 sqlite3changeset_apply()에 대한 단일 호출로 충분해요.
복잡한 경우에 changeset 적용의 복잡함은 충돌 해결에 있어요. 자세한 내용은 위에 연결된 API 문서를 참조하세요.
/*
** Conflict handler callback used by apply_changeset(). See below.
*/
static int xConflict(void *pCtx, int eConflict, sqlite3_changeset_iter *pIter){
int ret = (int)pCtx;
return ret;
}
/*
** Apply the changeset contained in blob pChangeset, size nChangeset bytes,
** to the main database of the database handle passed as the first argument.
** Return SQLITE_OK if successful, or an SQLite error code if an error
** occurs.
**
** If parameter bIgnoreConflicts is true, then any conflicting changes
** within the changeset are simply ignored. Or, if bIgnoreConflicts is
** false, then this call fails with an SQLITE_ABORT error if a changeset
** conflict is encountered.
*/
int apply_changeset(
sqlite3 *db, /* Database handle */
int bIgnoreConflicts, /* True to ignore conflicting changes */
int nChangeset, /* Size of changeset in bytes */
void *pChangeset /* Pointer to changeset blob */
){
return sqlite3changeset_apply(
db,
nChangeset, pChangeset,
0, xConflict,
(void*)bIgnoreConflicts
);
}
3. Changeset 내용 검사하기 (Inspecting the Contents of a Changeset)
아래 예제 코드는 changeset의 모든 변경과 관련된 데이터를 반복하고 추출하는 데 사용되는 기법을 보여줘요. 요약하면:
- sqlite3changeset_start() API를 호출해 changeset의 내용을 반복할 반복자(iterator)를 만들고 초기화해요. 처음에 반복자는 어떤 요소도 가리키지 않아요.
- 반복자에 대한 sqlite3changeset_next()에 대한 첫 호출은 그것이 changeset의 첫 변경을 가리키도록 이동시켜요(또는 changeset이 완전히 비어 있으면 EOF로). sqlite3changeset_next()는 반복자를 유효한 항목을 가리키도록 이동시키면 SQLITE_ROW를, EOF로 이동시키면 SQLITE_DONE을, 오류가 발생하면 SQLite 오류 코드를 반환해요.
- 반복자가 유효한 항목을 가리키면 sqlite3changeset_op() API를 사용해 반복자가 가리키는 변경 유형(INSERT, UPDATE 또는 DELETE)을 결정할 수 있어요. 또한 같은 API를 사용해 변경이 적용되는 테이블의 이름과 예상 열 수 및 기본 키 열을 얻을 수 있어요.
- 반복자가 유효한 INSERT 또는 UPDATE 항목을 가리키면 sqlite3changeset_new() API를 사용해 변경 페이로드 내의 new.* 값을 얻을 수 있어요.
- 반복자가 유효한 DELETE 또는 UPDATE 항목을 가리키면 sqlite3changeset_old() API를 사용해 변경 페이로드 내의 old.* 값을 얻을 수 있어요.
- 반복자는 sqlite3changeset_finalize() API 호출로 삭제돼요. 반복하는 동안 오류가 발생하면 SQLite 오류 코드가 반환돼요(같은 오류 코드가 이미 sqlite3changeset_next()에 의해 반환됐더라도). 또는 오류가 발생하지 않았다면 SQLITE_OK가 반환돼요.
/*
** Print the contents of the changeset to stdout.
*/
static int print_changeset(void *pChangeset, int nChangeset){
int rc;
sqlite3_changeset_iter *pIter = 0;
/* Create an iterator to iterate through the changeset */
rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset);
if( rc!=SQLITE_OK ) return rc;
/* This loop runs once for each change in the changeset */
while( SQLITE_ROW==sqlite3changeset_next(pIter) ){
const char *zTab; /* Table change applies to */
int nCol; /* Number of columns in table zTab */
int op; /* SQLITE_INSERT, UPDATE or DELETE */
sqlite3_value *pVal;
/* Print the type of operation and the table it is on */
rc = sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0);
if( rc!=SQLITE_OK ) goto exit_print_changeset;
printf("%s on table %s\n",
op==SQLITE_INSERT?"INSERT" : op==SQLITE_UPDATE?"UPDATE" : "DELETE",
zTab
);
/* If this is an UPDATE or DELETE, print the old.* values */
if( op==SQLITE_UPDATE || op==SQLITE_DELETE ){
printf("Old values:");
for(i=0; i<nCol; i++){
rc = sqlite3changeset_old(pIter, i, &pVal);
if( rc!=SQLITE_OK ) goto exit_print_changeset;
printf(" %s", pVal ? sqlite3_value_text(pVal) : "-");
}
printf("\n");
}
/* If this is an UPDATE or INSERT, print the new.* values */
if( op==SQLITE_UPDATE || op==SQLITE_INSERT ){
printf("New values:");
for(i=0; i<nCol; i++){
rc = sqlite3changeset_new(pIter, i, &pVal);
if( rc!=SQLITE_OK ) goto exit_print_changeset;
printf(" %s", pVal ? sqlite3_value_text(pVal) : "-");
}
printf("\n");
}
}
/* Clean up the changeset and return an error code (or SQLITE_OK) */
exit_print_changeset:
rc2 = sqlite3changeset_finalize(pIter);
if( rc==SQLITE_OK ) rc = rc2;
return rc;
}
확장 기능 (Extended Functionality)
대부분의 애플리케이션은 이전 절에서 설명한 세션 모듈 기능만 사용할 거예요. 하지만 changeset과 patchset blob의 사용과 조작을 위해 다음 추가 기능을 사용할 수 있어요.
- 둘 이상의 changeset/patchset을 sqlite3changeset_concat() 또는 sqlite3_changegroup 인터페이스를 사용해 결합할 수 있어요.
- changeset은 sqlite3changeset_invert() API 함수를 사용해 "뒤집을" 수 있어요. 뒤집힌 changeset은 원래 것에 의해 이루어진 변경을 실행 취소해요. changeset C+가 changeset C의 역이라면, 데이터베이스에 C를 적용한 다음 C+를 적용하면 데이터베이스는 변경되지 않은 상태로 남아야 해요.