会话扩展提供了一种机制,可以方便地记录对 SQLite 数据库中某些或所有特定表的更改,并将这些更改打包成一个“变更集”或“补丁集”文件,该文件稍后可用于将相同的更改集应用于另一个具有相同模式和兼容起始数据的数据库。 “变更集”也可以反转并用于“撤消”会话。
本文档是对会话扩展的介绍。接口的详细信息在单独的 会话扩展 C 语言接口 文档中。
假设 SQLite 用于作为特定设计应用程序的 应用程序文件格式。 两个用户,Alice 和 Bob,各自从一个大约 1 GB 大小的基本设计开始。 他们整天并行工作,每个人对设计进行自己的自定义和调整。 在一天结束时,他们希望将他们的更改合并到一个统一的设计中。
会话扩展通过记录 Alice 和 Bob 数据库的所有更改并将这些更改写入变更集或补丁集文件来促进此过程。 在一天结束时,Alice 可以将她的变更集发送给 Bob,Bob 可以将其“应用”到他的数据库。 结果(假设没有冲突)是 Bob 的数据库包含了他的更改和 Alice 的更改。 同样,Bob 可以将他的工作的变更集发送给 Alice,Alice 可以将其应用于她的数据库。
换句话说,会话扩展为 SQLite 数据库文件提供了一种类似于 unix patch 实用程序程序或版本控制系统(如 Fossil、Git 或 Mercurial)的“合并”功能的功能。
自 版本 3.13.0(2016-05-18)起,会话扩展已包含在 SQLite amalgamation 源代码分发中。 默认情况下,会话扩展被禁用。 要启用它,请使用以下编译器开关构建
-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 值。 但是,会话扩展会忽略所有此类行。 会话模块不会记录影响 PRIMARY KEY 列中有一个或多个 NULL 值的行的所有更改。
会话模块围绕创建和操作变更集进行。 变更集是编码对数据库的一系列更改的数据块。 变更集中每次更改都是以下之一
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 更改时,可能会检测到以下冲突
处理 UPDATE 更改时,可能会检测到以下冲突
根据冲突的类型,会话应用程序有多种可配置选项来处理冲突,从省略冲突更改、中止整个变更集应用或尽管发生冲突但仍应用更改。 有关详细信息,请参阅 sqlite3changeset_apply() API 文档。
配置会话对象后,它将开始监视对其已配置表的更改。 但是,它不会在数据库中每次修改一行时记录整个更改。 相反,它只记录每个插入行的 PRIMARY KEY 字段,以及每个更新或删除行的 PRIMARY KEY 和所有原始行值。 如果在一个会话中多次修改一行,则不会记录任何新信息。
创建变更集或补丁集所需的另一些信息是在调用 sqlite3session_changeset() 或 sqlite3session_patchset() 时从数据库文件中读取的。 具体来说,
对于每个作为 INSERT 操作的结果记录的主键,会话模块都会检查表中是否仍然存在具有匹配主键的行。 如果是这样,则会将 INSERT 更改添加到变更集中。
对于每个作为 UPDATE 或 DELETE 操作的结果记录的主键,会话模块还会检查表中是否具有匹配主键的行。 如果可以找到一个,但一个或多个非 PRIMARY KEY 字段与记录的原始值不匹配,则会将 UPDATE 添加到变更集中。 或者,如果根本没有具有指定主键的行,则会将 DELETE 添加到变更集中。 如果行存在但没有一个非 PRIMARY KEY 字段被修改,则不会将任何更改添加到变更集中。
上述情况的一个含义是,如果在单个会话中进行更改然后撤消更改(例如,如果插入一行然后又将其删除),则会话模块根本不会报告任何更改。 或者,如果在一个会话中多次更新一行,所有更新都会合并为变更集或补丁集 blob 中的单个更新。
本节提供演示如何使用会话扩展的示例。
下面的示例代码演示了在执行 SQL 命令时捕获变更集所涉及的步骤。 总之
通过调用 sqlite3session_create() API 函数创建会话对象(类型 sqlite3_session*)。
单个会话对象通过单个 sqlite3* 数据库句柄监视对单个数据库(即“main”、“temp”或已附加的数据库)所做的更改。
会话对象配置了一组要监视更改的表。
默认情况下,会话对象不会监视对任何数据库表的更改。 在这样做之前,它必须进行配置。 有三种方法可以配置要监视更改的表集
下面的示例代码使用了上面列出的三种方法中的第二种 - 它监视对所有数据库表的更改。
通过执行 SQL 语句对数据库进行更改。 会话对象记录这些更改。
通过调用 sqlite3session_changeset()(或如果使用补丁集,则调用 sqlite3session_patchset() 函数)从会话对象中提取变更集 blob。
通过调用 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; }
将变更集应用于数据库比捕获变更集更简单。通常,只需对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; }
大多数应用程序只使用上一节中描述的会话模块功能。但是,以下附加功能可用于使用和操作变更集和补丁集 blob
可以使用 sqlite3changeset_concat() 或 sqlite3_changegroup 接口组合两个或多个变更集/补丁集。
可以使用 sqlite3changeset_invert() API 函数“反转”变更集。反转的变更集撤消了原始变更集所做的更改。如果变更集 C+ 是变更集 C 的反转,则将 C 和 C+ 应用于数据库应使数据库保持不变。
此页面最后修改于 2024-06-01 14:57:22 UTC