会话扩展提供了一种机制,用于记录对SQLite数据库中某些或所有rowid表的更改,并将这些更改打包到“ changeset”或“ patchset”文件中,这些文件以后可用于将同一组更改应用于另一个具有相同架构和兼容起始数据的数据库。“变更集”也可以反转并用于“撤消”会话。
本文档是会话扩展的简介。接口的详细信息在单独的 会话扩展C语言接口文档中。
假设将SQLite用作特定设计应用程序的应用程序文件格式。两个用户Alice和Bob均以基线设计开始,该基线设计的大小约为1G。他们整天并行工作,各自对设计进行自定义和调整。归根结底,他们希望将所做的更改合并到一个统一的设计中。
会话扩展通过记录对Alice和Bob数据库的所有更改并将这些更改写入更改集或补丁集文件来简化此操作。在一天结束时,爱丽丝可以将她的变更集发送给鲍勃,而鲍勃可以将其“应用”到他的数据库中。结果(假设没有冲突)是Bob的数据库同时包含他的更改和Alice的更改。同样,Bob可以将其工作的变更集发送给Alice,并且可以将其变更应用于自己的数据库。
换句话说,会话扩展为SQLite数据库文件提供了一种工具,该工具与unix 修补程序实用程序或版本控制系统(例如Fossil,Git或Mercurial)的“合并”功能类似。
自版本3.13.0(2016-05-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表。
不支持虚拟表。不会捕获对虚拟表的更改。
会话扩展仅适用于具有声明的PRIMARY KEY的表。表的PRIMARY KEY可以是INTEGER PRIMARY KEY(行别名)或外部PRIMARY KEY。
SQLite允许将NULL值存储在PRIMARY KEY列中。但是,会话扩展会忽略所有此类行。会话模块不会记录任何影响PRIMARY KEY列中具有一个或多个NULL值的行的更改。
会话模块围绕创建和操纵变更集展开。变更集是一团数据,它编码对数据库的一系列变更。变更集中的每个变更都是以下之一:
一个INSERT。INSERT更改仅包含一行以添加到数据库表中。INSERT更改的有效负载由新行的每个字段的值组成。
一个DELETE。DELETE更改表示由其主键值标识的要从数据库表中删除的行。DELETE更改的有效负载由已删除行的所有字段的值组成。
一个更新。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操作而记录的每个主键,会话模块还将检查表中具有匹配主键的行。如果可以找到一个,但是一个或多个非主键字段与原始记录值不匹配,则将UPDATE添加到变更集。或者,如果根本没有带有指定主键的行,则将DELETE添加到变更集。如果该行确实存在,但是没有修改任何非PRIMARY KEY字段,则不会将任何更改添加到变更集。
上面的含义是,如果在单个会话中进行了更改,然后又未进行更改(例如,如果插入一行,然后再次将其删除),则会话模块根本不会报告任何更改。或者,如果一行在同一会话中被多次更新,则所有更新将合并为任何变更集或补丁集Blob中的单个更新。
本节提供了示例,演示了如何使用会话扩展。
下面的示例代码演示了在执行SQL命令时捕获变更集所涉及的步骤。总之:
通过调用sqlite3session_create() API函数来创建会话对象(sqlite3_session *类型)。
单个会话对象通过单个sqlite3 *数据库句柄监视对单个数据库(即“主”,“临时”或附加数据库)所做的更改。
会话对象配置有一组表以监视更改。
默认情况下,会话对象不监视任何数据库表上的更改。在此之前,必须对其进行配置。有三种方法可以配置表集以监视更改:
下面的示例代码使用上面列举的第二种方法-它监视所有数据库表上的更改。
通过执行SQL语句对数据库进行更改。会话对象记录这些更改。
使用对sqlite3session_changeset()的调用(或者,如果使用补丁集,则对sqlite3session_patchset()函数的调用)从会话对象中提取变更集blob 。
可以使用对sqlite3session_delete() API函数的调用来删除会话对象 。
从会话集中提取变更集或补丁集后,无需删除会话对象。可以将其保留在数据库句柄上,并将像以前一样继续监视已配置表上的更改。但是,如果 再次在会话对象上调用sqlite3session_changeset()或sqlite3session_patchset(),则更改集或补丁集将包含自创建会话以来在连接上进行的所有更改。换句话说,通过调用sqlite3session_changeset()或sqlite3session_patchset()不会重置会话对象或将其清零。
/ * **参数zSql指向包含要执行的SQL脚本的缓冲区 **针对作为第一个参数传递的数据库句柄。也 **执行SQL脚本,此函数收集变更集记录 **对“主”数据库文件进行的所有更改。假设没有错误发生, **输出变量(* ppChangeset)和(* pnChangeset)设置为point **到包含变更集和变更集大小的缓冲区中 **字节,然后返回SQLITE_OK。在这种情况下,这是责任 调用者的**最终通过将变更集Blob传递给来释放它 ** sqlite3_free函数。 ** **或者,如果确实发生错误,则返回SQLite错误代码。决赛 **(* pChangeset)和(* pnChangeset)的值在这种情况下未定义。 * / int sql_exec_changeset( sqlite3 * db, / *数据库句柄* / const char * zSql, / *要执行的SQL脚本* / int * pnChangeset, / * OUT:变更集blob的大小(以字节为单位)* / void ** ppChangeset / * OUT:指向变更集的指针Blob * / ){ sqlite3_session * pSession = 0; int rc; / *创建一个新的会话对象* / rc = sqlite3session_create(db,“ main”,&pSession); / *配置会话对象以记录对所有表的更改* / if(rc == SQLITE_OK)rc = sqlite3session_attach(pSession,NULL); / *执行SQL脚本* / if(rc == SQLITE_OK)rc = sqlite3_exec(db,zSql,0,0,0); / *收集变更集* / if(rc == SQLITE_OK){ rc = sqlite3session_changeset(pSession,pnChangeset,ppChangeset); } / *删除会话对象* / sqlite3session_delete(pSession); 返回rc; }
将变更集应用于数据库比捕获变更集更简单。通常,只需调用一次sqlite3changeset_apply()(如下面的示例代码所示)就足够了。
在复杂的情况下,应用变更集的麻烦在于解决冲突。有关详细信息,请参阅上面链接的API文档。
/ * ** apply_changeset()使用的冲突处理程序回调。见下文。 * / static int xConflict(void * pCtx,int eConflict,sqlite3_changset_iter * pIter){ int ret =(int)pCtx; 返回ret } / * **应用blob pChangeset中包含的变更集,大小为nChangeset字节, **传递给作为第一个参数传递的数据库句柄的主数据库。 **如果成功则返回SQLITE_OK,如果错误则返回SQLite错误代码 **发生。 ** **如果参数bIgnoreConflicts为true,则任何冲突的更改 变更集中的**只是被忽略。或者,如果bIgnoreConflicts为 **否,如果变更集,则此调用失败,并显示SQLTIE_ABORT错误 **遇到冲突。 * / int apply_changeset( sqlite3 * db, / *数据库句柄* / int bIgnoreConflicts, / *忽略冲突的更改为True * / int nChangeset, / *变更集的大小(以字节为单位)* / void * pChangeset / *指向变更集blob的指针* / ){ 返回sqlite3changeset_apply( D b, 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。
/ * **将变更集的内容打印到stdout。 * / 静态int print_changeset(void * pChangeset,int nChangeset){ int rc; sqlite3_changeset_iter * pIter = 0; / *创建一个迭代器以遍历变更集* / rc = sqlite3changeset_start(&pIter,nChangeset,pChangeset); if(rc!= SQLITE_OK)返回rc; / *对于更改集中的每个更改,此循环运行一次* / while(SQLITE_ROW == sqlite3changeset_next(pIter)){ const char * zTab; / *表更改适用于* / int nCol; / *表zTab中的列数* / int op; / * SQLITE_INSERT,UPDATE或DELETE * / sqlite3_value * pVal; / *打印操作类型及其所在的表* / rc = sqlite3changeset_op(pIter,&zTab,&nCol,&op,0); if(rc!= SQLITE_OK)转到exit_print_changeset; printf(“表%s上的%s \ n”, op == SQLITE_INSERT?“ INSERT”:op == SQLITE_UPDATE?“ UPDATE”:“ DELETE”, 标签 ); / *如果这是UPDATE或DELETE,则打印旧的。*值* / if(op == SQLITE_UPDATE || op == SQLITE_DELETE){ printf(“旧值:”); for(i = 0; i <nCol; i ++){ rc = sqlite3changeset_old(pIter,i,&pVal); if(rc!= SQLITE_OK)转到exit_print_changeset; printf(“%s”,pVal?sqlite3_value_text(pVal):“-”); } printf(“ \ n”); } / *如果这是UPDATE或INSERT,则打印新值。*值* / if(op == SQLITE_UPDATE || op == SQLITE_INSERT){ printf(“新值:”); for(i = 0; i <nCol; i ++){ rc = sqlite3changeset_new(pIter,i,&pVal); if(rc!= SQLITE_OK)转到exit_print_changeset; printf(“%s”,pVal?sqlite3_value_text(pVal):“-”); } printf(“ \ n”); } } / *清理变更集并返回错误代码(或SQLITE_OK)* / exit_print_changeset: rc2 = sqlite3changeset_finalize(pIter); if(rc == SQLITE_OK)rc = rc2; 返回rc; }
大多数应用程序将仅使用上一节中描述的会话模块功能。但是,以下附加功能可用于变更集和补丁集blob的使用和操纵:
可以使用sqlite3changeset_concat()或sqlite3_changegroup接口组合两个或多个变更集/补丁集 。
可以使用sqlite3changeset_invert() API函数“反转”变更集。反向变更集会撤消原始变更集。如果变更集C +是变更集C的逆,那么对数据库应用C再对C +进行应用应使数据库保持不变。