會話擴展提供了一種機制,可以方便地記錄 SQLite 資料庫中部分或全部特定表格的變更,並將這些變更打包成「變更集」或「修補集」檔案,以便日後用於將相同的變更應用於具有相同結構描述和相容起始資料的另一個資料庫。「變更集」也可以反轉,用於「復原」一個會話。
本文檔是會話擴展的簡介。介面的詳細資訊請參閱單獨的 會話擴展 C 語言介面 文件。
假設 SQLite 被用作特定設計應用程式的 應用程式檔案格式。兩個使用者,Alice 和 Bob,各自以一個大小約為 1GB 的基準設計開始工作。他們整天並行工作,各自對設計進行客製化和調整。在一天結束時,他們希望將他們的變更合併成一個統一的設計。
會話擴展透過記錄 Alice 和 Bob 資料庫的所有變更,並將這些變更寫入變更集或修補集檔案來促進此操作。在一天結束時,Alice 可以將她的變更集傳送給 Bob,Bob 可以將其「應用」到他的資料庫。結果(假設沒有衝突)是 Bob 的資料庫包含了他自己的變更和 Alice 的變更。同樣地,Bob 可以將他的工作變更集傳送給 Alice,她可以將他的變更應用於她的資料庫。
換句話說,會話擴展為 SQLite 資料庫檔案提供了一個類似於 unix patch 公用程式或版本控制系統(例如 Fossil、Git 或 Mercurial)的「合併」功能的工具。
自 3.13.0 版(2016 年 5 月 18 日)起,會話擴展已包含在 SQLite 合併 原始碼發行版中。預設情況下,會話擴展是停用的。要啟用它,請使用以下編譯器參數進行建置:
-DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_PREUPDATE_HOOK
或者,如果使用 autoconf 建置系統,請將 --enable-session 選項傳遞給 configure 指令碼。
在 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 值。然而,此 sessions 模組會忽略所有此類列。任何影響 PRIMARY KEY 欄位中含有一個或多個 NULL 值的列之變更,都不會被 sessions 模組記錄。
sessions 模組的核心是建立和操作變更集。變更集是一段編碼了一系列資料庫變更的資料。變更集中的每個變更都是以下其中一種:
INSERT(插入)。INSERT 變更包含要新增到資料庫表格的一列資料。INSERT 變更的有效負載包含新列每個欄位的值。
DELETE(刪除)。DELETE 變更表示要從資料庫表格中移除的一列資料,由其主鍵值識別。DELETE 變更的有效負載包含被刪除列所有欄位的值。
UPDATE(更新)。UPDATE 變更表示修改資料庫表格中單一列的一個或多個非 PRIMARY KEY 欄位,該列由其 PRIMARY KEY 欄位識別。UPDATE 變更的有效負載包含:
UPDATE 變更不包含任何關於未被變更修改的非 PRIMARY KEY 欄位的資訊。UPDATE 變更無法指定對 PRIMARY KEY 欄位的修改。
單個變更集可能包含應用於多個資料庫表格的變更。對於變更集包含至少一個變更的每個表格,它還會編碼以下資料:
變更集只能應用於包含符合儲存在變更集中的上述三個條件的表格的資料庫。
修補集與變更集類似。它比變更集略為精簡,但提供的衝突檢測和解決方案選項更有限(詳見下一節)。修補集和變更集之間的差異在於:
對於 DELETE 變更,有效負載僅包含 PRIMARY KEY 欄位。其他欄位的原始值不會儲存在修補集中。
對於 UPDATE 變更,有效負載僅包含 PRIMARY KEY 欄位和被修改欄位的新值。被修改欄位的原始值不會儲存在修補集中。
當將變更集或修補集應用於資料庫時,系統會嘗試為每個 INSERT 變更插入新列,為每個 DELETE 變更移除列,並為每個 UPDATE 變更修改列。如果目標資料庫與記錄變更集的原始資料庫處於相同的狀態,則這是很簡單的事情。但是,如果目標資料庫的內容並非完全處於此狀態,則在應用變更集或修補集時可能會發生衝突。
處理 INSERT 變更時,可能會發生以下衝突:
處理 DELETE 變更時,可能會偵測到以下衝突:
處理更新變更時,可能會偵測到以下衝突
根據衝突的類型,工作階段應用程式有多種可配置的選項來處理衝突,範圍從略過衝突的變更、中止整個變更集應用程式或不顧衝突應用程式變更。詳細資訊,請參閱 sqlite3changeset_apply() API 的文件。
設定工作階段物件後,它會開始監控其設定表格的變更。但是,它不會在每次修改資料庫中的列時記錄整個變更。相反地,它只會記錄每個插入列的主鍵欄位,以及任何已更新或已刪除列的主鍵和所有原始列值。如果單一工作階段多次修改列,則不會記錄新的資訊。
建立變更集或修補程式集所需的其他資訊會在呼叫 sqlite3session_changeset() 或 sqlite3session_patchset() 時從資料庫檔案中讀取。具體來說:
對於因 INSERT 作業而記錄的每個主鍵,工作階段模組會檢查表格中是否仍存在具有相符主鍵的列。如果是,則會將 INSERT 變更新增至變更集。
對於因 UPDATE 或 DELETE 作業而記錄的每個主鍵,工作階段模組也會檢查表格中是否有具有相符主鍵的列。如果找到一列,但一個或多個非主鍵欄位與記錄的原始值不符,則會將 UPDATE 新增至變更集。或者,如果根本沒有具有指定主鍵的列,則會將 DELETE 新增至變更集。如果列確實存在,但沒有任何非主鍵欄位已被修改,則不會將任何變更新增至變更集。
上述含義之一是,如果在單一工作階段內進行變更然後取消變更(例如,如果插入一列然後再次刪除),則工作階段模組根本不會報告任何變更。或者,如果在同一個工作階段內多次更新一列,則所有更新都會合併成變更集或修補程式集 Blob 中的單一更新。
本節提供示範如何使用工作階段擴充功能的範例。
以下範例程式碼示範在執行 SQL 命令時擷取變更集的步驟。總而言之:
透過呼叫 sqlite3session_create() API 函式建立工作階段物件(類型 sqlite3_session*)。
單一工作階段物件透過單一 sqlite3* 資料庫控制代碼監控對單一資料庫(即「main」、「temp」或附加資料庫)所做的變更。
工作階段物件會設定一組要監控變更的表格。
根據預設,工作階段物件不會監控任何資料庫表格的變更。在這樣做之前,必須先進行設定。有三種方法可以設定要監控變更的表格集:
下面的範例程式碼使用了上面列舉的第二種方法 - 它監控所有資料庫表格的變更。
透過執行 SQL 陳述式來更改資料庫。session 物件會記錄這些變更。
使用 sqlite3session_changeset() 函式 (或者,如果使用 patchset,則呼叫 sqlite3session_patchset() 函式) 從 session 物件中提取變更集 blob。
使用 sqlite3session_delete() API 函式刪除 session 物件。
從 session 物件中提取變更集或 patchset 後,不一定要刪除該物件。它可以保持附加到資料庫控制代碼,並將像以前一樣繼續監控已設定表格的變更。但是,如果在 session 物件上再次呼叫 sqlite3session_changeset() 或 sqlite3session_patchset(),則變更集或 patchset 將包含自建立 session 以來在連線上發生的*所有*變更。換句話說,呼叫 sqlite3session_changeset() 或 sqlite3session_patchset() 不會重置或歸零 session 物件。
/* ** 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; }
將變更集應用於資料庫比擷取變更集更簡單。通常,只需單次呼叫 sqlite3changeset_apply() 即可,如下面的範例程式碼所示。
在應用變更集很複雜的情況下,複雜性在於衝突解決。有關詳細資訊,請參閱上面連結的 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 ); }
下面的範例程式碼示範了用於逐一查看並提取與變更集中所有變更相關的資料的技巧。總結如下:
呼叫 sqlite3changeset_start() API 來建立並初始化一個迭代器,以逐一查看變更集的內容。最初,迭代器不指向任何元素。
第一次在迭代器上呼叫 sqlite3changeset_next() 會將其移動到指向變更集中的第一個變更(如果變更集完全為空,則指向 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; }
大多數應用程式只會使用前一節中描述的 session 模組功能。然而,以下額外的功能可用於操作 changeset 和 patchset blob。
可以使用 sqlite3changeset_concat() 或 sqlite3_changegroup 介面合併兩個或多個 changeset/patchset。
可以使用 sqlite3changeset_invert() API 函式「反轉」changeset。反轉後的 changeset 會復原原始 changeset 所做的變更。如果 changeset C+ 是 changeset C 的反轉,那麼將 C 應用到資料庫後再應用 C+,應該會使資料庫保持不變。
本頁最後修改時間:2024-06-01 14:57:22 UTC