返回:SQLite—系列文章目录
上一篇:SQLite R*Tree 模块(三十三)
下一篇:SQLite—系列文章目录
1. 引言
会话扩展提供了一种方便记录的机制 对 SQLite 数据库中某些表的部分或全部更改,以及 将这些更改打包到“变更集”或“补丁集”文件中,可以 稍后用于将同一组更改应用于另一个数据库 相同的架构和兼容的起始数据。“变更集”可能 也可以倒置并用于“撤消”会话。
本文档是对会话扩展的介绍。 该接口的详细信息位于单独的会话扩展 C 语言接口文档中。
1.1. 典型用例
假设 SQLite 被用作应用程序文件格式 特定的设计应用。两个用户 Alice 和 Bob 分别启动 基线设计大小约为 1 GB。他们工作 一整天,并行,每个人都在进行自己的定制和调整 到设计。归根结底,他们想合并他们的 一起改变成一个统一的设计。
会话扩展通过记录对 Alice 和 Bob 的数据库,并将这些更改写入 变更集或补丁集文件。在一天结束时,爱丽丝可以送她 changeset 到 Bob,Bob 可以将其“应用”到他的数据库中。结果(假设 没有冲突)是 Bob 的数据库包含他的 变化和爱丽丝的变化。同样,Bob 可以发送 他的工作交给了爱丽丝,她可以将他的更改应用到她的数据库中。
换言之,会话扩展提供了以下工具: 类似于 unix 补丁实用程序的 SQLite 数据库文件, 或版本控制系统的“合并”功能,例如 饰演 Fossil, Git, 或Mercurial。
1.2. 获取会话扩展
自 3.13.0 版(2016-05-18)起, 会话扩展已包含在 SQLite 合并源分发中。默认情况下,会话扩展为 禁用。若要启用它,请使用以下编译器开关进行生成:
-DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_PREUPDATE_HOOK
或者,如果使用 autoconf 构建系统, 将 --enable-session 选项传递给 configure 脚本。
1.3. 限制
-
在 SQLite 版本 3.17.0 之前,会话扩展仅适用于 rowid 表,而不适用于 ROWID 表。从 3.17.0 开始,两者都 支持 rowid 和 WITHOUT ROWID 表。但是,额外的步骤是 需要记录 WITHOUT ROWID 表更改的主键。
-
不支持虚拟表。对虚拟表的更改包括 未捕获。
-
会话扩展仅适用于声明了 主键。表的 PRIMARY KEY 可以是 INTEGER PRIMARY KEY (rowid 别名)或外部 PRIMARY KEY。
-
SQLite 允许将 NULL 值存储在 PRIMARY KEY 列。但是,会话扩展会忽略所有 这样的行。不会影响具有一个或多个 NULL 值的行的更改 在 PRIMARY KEY 中,列由 sessions 模块记录。
2. 概念
2.1. 变更集和补丁集
会话模块围绕创建和操作展开 变更集。变更集是编码一系列 对数据库的更改。变更集中的每个更改都是 以后:
-
一个 INSERT。INSERT 更改包含要添加到的单行 数据库表。INSERT 更改的有效负载包括 新行的每个字段的值。
-
A 删除。DELETE 更改表示一行,由 其主键值,要从数据库表中删除。有效载荷 的 DELETE 更改由 已删除的行。
-
更新。UPDATE 更改表示对 数据库中单行的一个或多个非 PRIMARY KEY 字段 表,由其 PRIMARY KEY 字段标识。UPDATE 的有效负载 更改包括:
- 标识修改后的行的 PRIMARY KEY 值,
- 行中每个已修改字段的新值,以及
- 行中每个已修改字段的原始值。
UPDATE 更改不包含有关以下方面的任何信息 未被更改修改的非 PRIMARY KEY 字段。事实并非如此 UPDATE 更改可以指定对 PRIMARY 的修改 KEY 字段。
单个变更集可能包含适用于多个变更集的变更 数据库表。对于变更集至少包含一个更改的每个表 因为,它还对以下数据进行编码:
- 数据库表的名称,
- 表的列数,以及
- 这些列中哪一列是 PRIMARY KEY 列。
变更集只能应用于包含表的数据库 匹配变更集中存储的上述三个条件。
补丁集类似于变更集。它比 变更集,但提供更有限的冲突检测和解决 选项(有关详细信息,请参阅下一节)。之间的区别 patchset 和 changeset 是:
-
对于 DELETE 更改,有效负载由 PRIMARY KEY 组成 仅限字段。其他字段的原始值不存储为 补丁集的一部分。
-
对于 UPDATE 更改,有效负载由 PRIMARY KEY 组成 字段和仅修改字段的新值。原文 已修改字段的值不会存储为补丁集的一部分。
2.2. 冲突
当变更集或补丁集应用于数据库时,尝试是 为每个 INSERT 更改插入一个新行,为每个更改删除一行 DELETE 更改并修改每个 UPDATE 更改的一行。如果目标 数据库与变更集的原始数据库处于相同的状态 被记录下来,这是一件简单的事情。但是,如果 目标数据库并不完全处于此状态,当以下情况下可能会发生冲突 应用变更集或补丁集。
处理 INSERT 更改时,以下冲突可能 发生:
- 目标数据库可能已包含具有相同 PRIMARY 的行 由 INSERT 更改指定的 KEY 值。
- 其他一些数据库约束,例如 UNIQUE 或 CHECK 约束,在插入新行时可能会被违反。
处理 DELETE 更改时,可能会出现以下冲突: 检测:
- 目标数据库可能不包含具有指定 PRIMARY 的行 要删除的 KEY 值。
- 目标数据库可能包含具有指定 PRIMARY 的行 KEY 值,但其他字段可能包含不 匹配存储为变更集一部分的那些。这种类型的冲突 使用补丁集时未检测到。
处理 UPDATE 更改时,可能会出现以下冲突: 检测:
- 目标数据库可能不包含具有指定 PRIMARY 的行 要修改的 KEY 值。
- 目标数据库可能包含具有指定 PRIMARY 的行 KEY 值,但要修改的字段的当前值 通过更改可能与存储在 变更集。使用修补程序集时,不会检测到此类冲突。
- 其他一些数据库约束,例如 UNIQUE 或 CHECK 约束,更新行时可能会被违反。
根据冲突的类型,会话应用程序具有多种 用于处理冲突的可配置选项,范围从省略 冲突的更改、中止整个变更集应用程序或应用 尽管存在冲突,但变化。有关详细信息,请参阅文档 sqlite3changeset_apply() API。
2.3. 变更集构造
配置会话对象后,它将开始监视 对其配置表的更改。但是,它不会记录整个 每次修改数据库中的行时进行更改。相反,它会记录 仅插入每个行的 PRIMARY KEY 字段,以及 PRIMARY KEY 以及任何更新或删除的行的所有原始行值。如果一行是 单个会话多次修改,不会记录新信息。
创建变更集或补丁集所需的其他信息包括 调用 sqlite3session_changeset() 或 sqlite3session_patchset() 时从数据库文件中读取。具体说来
-
对于作为 INSERT 操作结果记录的每个主键, 会话模块检查是否存在具有匹配主节点的行 键仍在表中。如果是这样,则将 INSERT 更改添加到 变更集。
-
对于作为 UPDATE 或 DELETE 结果记录的每个主键 操作中,会话模块还会检查具有匹配的行 表中的主键。如果可以找到一个,但一个或多个 非 PRIMARY KEY 字段与原始记录不匹配 值,则 UPDATE 将添加到变更集中。或者,如果没有行 使用指定的主键时,DELETE 将添加到 变更集。如果该行确实存在,但没有非主键 字段已修改,变更集不会添加任何更改。
上述的一个含义是,如果进行了更改,然后 在单个会话中取消(例如,如果插入了一行,然后 再次删除),会话模块根本不会报告任何更改。或 如果同一会话中同一行多次更新,则所有更新 合并到任何变更集或修补程序集 blob 中的单个更新中。
3. 使用会话扩展
本节提供的示例演示了如何使用会话 外延。
3.1. 捕获变更集
下面的示例代码演示了捕获 执行 SQL 命令时的变更集。综上所述:
-
会话对象(类型 sqlite3_session*)是通过创建 调用 sqlite3session_create() API 函数。
单个会话对象监视对单个数据库所做的更改 (即“main”、“temp”或附加数据库)通过单个 sqlite3* 数据库句柄。
-
会话对象配置了一组要监视的表 更改。
默认情况下,会话对象不会监视任何 数据库表。在执行此操作之前,必须对其进行配置。那里 是配置表集以监视更改的三种方法 上:
- 通过对每个表使用一次对 sqlite3session_attach() 的调用显式指定表,或者
- 通过指定应监视数据库中的所有表 对于使用对 sqlite3session_attach() 的调用进行更改,带有 NULL 参数,或者
- 通过配置每个表首次调用的回调 写入该会话模块,指示会话模块是否或 不应监视表上的更改。
下面的示例代码使用枚举的第二个方法 上面 - 它监视所有数据库表上的更改。
-
通过执行 SQL 语句对数据库进行更改。这 会话对象记录这些更改。
-
使用调用从会话对象中提取变更集 Blob to sqlite3session_changeset() (或者,如果使用补丁集,则调用 sqlite3session_patchset() 函数)。
-
使用对 sqlite3session_delete() API 函数的调用删除会话对象。
解压后无需删除会话对象 来自它的变更集或补丁集。它可以留在 数据库句柄,并将继续监视 像以前一样配置了表。但是,如果 sqlite3session_changeset() 或 sqlite3session_patchset() 是 对会话对象、变更集或补丁集进行第二次调用 将包含连接上发生的所有更改 自会话创建以来。换言之, 会话对象未重置或 通过调用 sqlite3session_changeset()或 sqlite3session_patchset()。
/*
** 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;
}
3.2. 将变更集应用于数据库
将变更集应用于数据库比捕获变更集更简单。 通常,对 sqlite3changeset_apply() 的单个调用,如 下面的示例代码就足够了。
在复杂的情况下,应用 变更集在于解决冲突。请参阅链接的 API 文档 以上了解详情。
/*
** Conflict handler callback used by apply_changeset(). See below.
*/
static int xConflict(void *pCtx, int eConflict, sqlite3_changset_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 SQLTIE_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.3. 检查变更集的内容
下面的示例代码演示了用于迭代的技术 通过并提取与变更集中的所有更改相关的数据。自 总结:
-
调用 sqlite3changeset_start() API 来创建和 初始化迭代器以遍历 变更集。最初,迭代器根本不指向任何元素。
-
在迭代器上对 sqlite3changeset_next() 的第一次调用会移动 它指向变更集中的第一个更改(或指向 EOF,如果 变更集完全为空)。sqlite3changeset_next() 返回 SQLITE_ROW 如果它移动迭代器以指向有效条目, SQLITE_DONE是否将迭代器移动到 EOF,或者出现 SQLite 错误 如果发生错误,则进行代码。
-
如果迭代器指向有效条目,则 sqlite3changeset_op() API 可用于确定更改类型(INSERT、UPDATE 或 DELETE),迭代器指向。此外,相同的 API 可用于获取更改所适用的表的名称 以及其预期的列数和主键列数。
-
如果迭代器指向有效的 INSERT 或 UPDATE 条目,则可以使用 sqlite3changeset_new() API 来获取新的 .* 值 在更改有效负载中。
-
如果迭代器指向有效的 DELETE 或 UPDATE 条目,则可以使用 sqlite3changeset_old() API 获取旧的 .* 值 在更改有效负载中。
-
使用对 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;
}
4. 扩展功能
大多数应用程序将仅使用所描述的会话模块功能 在上一节中。但是,以下附加功能是 可用于使用和操作变更集和修补程序集 Blob:Available for the use and manipulation of changeset and patchset blob:
-
可以使用 sqlite3changeset_concat() 或 sqlite3_changegroup 接口组合两个或多个变更集/补丁集。
-
可以使用 sqlite3changeset_invert() API 函数“反转”变更集。反向变更集撤消了 源语言。如果变更集 C+ 是变更集 C 的逆,则 将 C 和 C+ 应用于数据库应该会离开 数据库未更改。