返回: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+ 应用于数据库应该会离开 数据库未更改。