首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

The Session Extension

1.介绍

1.1. 典型用例

1.2. 获得会话扩展

1.3. 限制

2.概念

2.1. 变更集和补丁集

2.2. 冲突

2.3. 更改设置

3. 使用会话扩展

3.1. 捕获变更集

3.2. 将更改集应用于数据库

3.3. 检查变更集的内容

4. 扩展功能

会话扩展提供了一种机制,用于记录对SQLite数据库中部分或全部rowid表的更改,并将这些更改打包到“changeset”或“patchset”文件中,稍后可用于将相同的一组更改应用于另一个数据库具有相同的模式和兼容的起始数据。“changeset”也可能被倒置并用于“undo”会话。

本文档是对会话扩展的介绍。接口的细节在单独的会话扩展C语言接口文档中。

1.1. 典型用例

假设SQLite被用作特定设计应用程序的应用程序文件格式。两个用户,Alice和Bob,每个用户的基线设计大小都是千兆字节。他们整天都在同时工作,每个人都对设计进行自定义和调整。在一天结束时,他们希望将他们的变化合并为一个统一的设计。

会话扩展通过记录对Alice和Bob数据库的所有更改并将这些更改写入更改集或补丁集文件来实现此目的。在一天结束时,Alice可以将她的变更集发送给Bob,并且Bob可以将其应用到他的数据库中。结果(假设没有冲突)是Bob的数据库包含他的变更和Alice的变更。同样,Bob可以将其工作变更集发送给Alice,并且可以将其更改应用到她的数据库中。

换句话说,会话扩展为SQLite数据库文件提供了一个类似于unix修补程序(https://en.wikipedia.org/wiki/Patch_(Unix%29))实用程序或“merge”功能的工具的版本控制系统,如FossilGitMercurial

1.2. 获得会话扩展

版本3.13.0(2016-05-18)以来,会话扩展已包含在SQLite合并源分布中。默认情况下,会话扩展被禁用。要启用它,请使用以下编译器开关构建:

代码语言:javascript
复制
-DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_PREUPDATE_HOOK

或者,如果使用autoconf构建系统,则将--enable-session选项传递给configure脚本。

1.3. 限制

  • 在SQLite版本3.17.0之前,会话扩展只能使用rowid表,而不能使用WITHOUT ROWID表。从3.17.0开始,支持rowid和WITHOUT ROWID表。
  • 不支持虚拟表。没有捕获虚拟表格的变化。
  • 会话扩展仅适用于具有声明的PRIMARY KEY的表。表的PRIMARY KEY可能是INTEGER PRIMARY KEY(rowid别名)或外部PRIMARY KEY。
  • SQLite允许将NULL值存储在PRIMARY KEY列中。但是,会话扩展会忽略所有这些行。会话模块不记录任何影响具有PRIMARY KEY列中的一个或多个NULL值的行的更改。

2.1. 变更集和补丁集

会话模块围绕创建和操作变更集进行。变更集是对数据库的一系列更改进行编码的一组数据。变更集中的每个更改都是以下之一:

  • 一个INSERT。INSERT更改包含要添加到数据库表的单个行。INSERT更改的有效负载由新行的每个字段的值组成。
  • 一个DELETE。DELETE更改表示一个由其主键值标识的行,以从数据库表中删除。DELETE更改的有效内容由已删除行的所有字段的值组成。
  • 一个UPDATE。UPDATE更改表示修改数据库表中由单个行的一个或多个非PRIMARY KEY字段标识的PRIMARY KEY字段。UPDATE更改的有效内容包括:
代码语言:txt
复制
-  The PRIMARY KEY values identifying the modified row, 
-  The new values for each modified field of the row, and 
-  The original values for each modified field of the row. 

UPDATE更改不包含有关未被更改修改的非PRIMARY KEY字段的任何信息。UPDATE更改不能指定对PRIMARY KEY字段的修改。

单个变更集可能包含适用于多个数据库表的更改。对于每个更改集至少包含一个更改的表,它还会编码以下数据:

  • 数据库表的名称,
  • 表格的列数,以及
  • 哪些列是PRIMARY KEY列。

更改集只能应用于包含与存储在变更集中的上述三个条件相匹配的表的数据库。

补丁集与变更集类似。它比变更集略微更紧凑,但提供更有限的冲突检测和分辨率选项(有关详细信息,请参阅下一节)。补丁集和变更集之间的差异在于:

  • 对于DELETE更改,有效负载只包含PRIMARY KEY字段。其他字段的原始值不作为补丁集的一部分存储。
  • 对于UPDATE更改,有效负载仅包含PRIMARY KEY字段和修改字段的新值。修改字段的原始值不作为修补程序集的一部分存储。

2.2. 冲突

将更改集或补丁集应用于数据库时,会尝试为每个INSERT更改插入一个新行,为每个DELETE更改删除一行并为每个UPDATE更改修改一行。如果目标数据库与记录变更集的原始数据库处于相同状态,则这是一件简单的事情。但是,如果目标数据库的内容不完全处于此状态,则应用更改集或修补程序集时可能会发生冲突。

处理INSERT更改时,可能会发生以下冲突:

  • 目标数据库可能已经包含具有与INSERT更改所指定的PRIMARY 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.1. 捕获变更集

下面的示例代码演示了执行SQL命令时捕获变更集的步骤。综上所述:

  1. 会话对象(类型为sqlite3_session *)通过调用sqlite3session_create()API函数来创建。单个会话对象监视通过单个sqlite3 *数据库句柄对单个数据库(即“main”,“temp”或附加数据库)所做的更改。
  2. 会话对象配置有一组表来监视变化。默认情况下,会话对象不会监视任何数据库表上的更改。在此之前,它必须进行配置。有三种方法可以配置一组表来监视变化:
代码语言:txt
复制
-  By explicitly specifying tables using one call to [sqlite3session\_attach()](session/sqlite3session_attach) for each table, or 
-  By specifying that all tables in the database should be monitored for changes using a call to [sqlite3session\_attach()](session/sqlite3session_attach) with a NULL argument, or 
-  By configuring a callback to be invoked the first time each table is written to that indicates to the session module whether or not changes on the table should be monitored. 

下面的示例代码使用上面枚举的第二个方法 - 它监视所有数据库表上的更改。

  1. 通过执行SQL语句对数据库进行更改。会话对象记录这些更改。
  2. 使用对sqlite3session_changeset()的调用从会话对象中提取变更集团块(或者,如果使用补丁集,则调用sqlite3session_patchset()函数)。
  3. 使用对sqlite3session_delete()API函数的调用删除会话对象。在从中提取变更集或补丁集后,不需要删除会话对象。它可以保留附加到数据库句柄,并将继续监视配置表上的更改。但是,如果在会话对象上第二次调用sqlite3session_changeset()或sqlite3session_patchset(),则更改集或补丁集将包含自会话创建以来连接上发生的所有更改。换句话说,会话对象不会通过调用sqlite3session_changeset()或sqlite3session_patchset()来重置或归零。
代码语言:javascript
复制
/*
** 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文档。

代码语言:javascript
复制
/*
** 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.检查变更集的内容

下面的示例代码演示了用于迭代并提取与变更集中所有更改相关的数据的技术。总结:

  1. 调用sqlite3changeset_start()API来创建和初始化迭代器以遍历变更集的内容。最初,迭代器根本没有指向任何元素。
  2. 在迭代器上对sqlite3changeset_next()的第一次调用将它移动到指向变更集中的第一个变更(如果变更集完全为空,则指向EOF)。如果sqlite3changeset_next()将迭代器移动到指向有效条目时返回SQLITE_ROW,如果将迭代器移动到EOF,则返回SQLITE_DONE;如果发生错误,则返回SQLite错误代码。
  3. 如果迭代器指向有效条目,则sqlite3changeset_op()API可用于确定迭代器指向的更改类型(INSERT,UPDATE或DELETE)。此外,可以使用相同的API来获取更改所适用的表的名称及其预期的列数和主键列数。
  4. 如果迭代器指向有效的INSERT或UPDATE条目,则sqlite3changeset_new()API可用于获取更改有效内容中的new.*值。
  5. 如果迭代器指向有效的DELETE或UPDATE条目,则sqlite3changeset_old()API可用于获取更改有效内容中的new.*值。
  6. 使用对sqlite3changeset_finalize()API的调用删除迭代器。如果迭代时发生错误,则返回SQLite错误代码(即使sqlite3changeset_next()已经返回相同的错误代码)。或者,如果没有发生错误,则返回SQLITE_OK。
代码语言:javascript
复制
/*
** 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函数“inverted”。一个倒置的变化集解除了原来的变化。如果changeset C +与changeset C相反,那么将C和C +应用于数据库应该保持数据库不变。

扫码关注腾讯云开发者

领取腾讯云代金券