如何使用自定义变更跟踪系统

许多应用程序都要求在服务器数据库中对变更进行跟踪,以便这些变更可以在后续同步会话中传递给客户端。本主题介绍变更跟踪系统的要求,并说明如何创建可由 Sync Framework 使用的自定义系统。在某些情况下自定义变更跟踪适用。但要注意,自定义变更跟踪确实会增加复杂性并且可能会影响服务器数据库的性能。如果使用的是 SQL Server 2008,建议您使用 SQL Server 变更跟踪功能。有关更多信息,请参见如何使用 SQL Server 变更跟踪

同步方案的服务器要求

Sync Framework 的设计将对服务器数据库的影响降至最小。因此,需要在服务器数据库中对变更跟踪进行的修改与您要在应用程序中实现的功能级别成正比。请记住以下注意事项:

  • 一种极端情况是数据的仅下载快照。这种情况不需要进行任何变更。

  • 而另一种极端情况则是使用完全变更跟踪和冲突检测的双向同步。

下表总结了可以使用 Sync Framework 的方式,并且标识了服务器数据库的对应要求。

方案 主键或唯一列1 跟踪更新时间 跟踪插入时间 跟踪删除时间 跟踪更新操作的客户端 ID 跟踪插入操作的客户端 ID 跟踪删除操作的客户端 ID

将数据快照下载到客户端。

将增量插入和更新下载到客户端。

是2

将增量插入、更新和删除下载到客户端。

是2

将插入上载到服务器。

否3

将插入和更新上载到服务器。

否3

否3

将插入、更新和删除上载到服务器。

否3

否3

否3

带有冲突检测的双向插入和更新。

是2

是4

是4

带有冲突检测的双向插入、更新和删除。

是2

是4

是4

是4

1 主键在所有节点上必须是唯一的,而且一定不能重复使用:即使删除了某行,也不应将该行的主键用于其他行。对于分布式环境,标识列通常不是适宜的选择。有关主键的更多信息,请参见为分布式环境选择适宜的主键

2 如果要区分插入和更新,则是必需的。有关更多信息,请参见本主题后面的“确定要下载到客户端的数据变更”。

3 如果可能有多个客户端变更某一行,并且想要标识出执行该变更的客户端,则是必需的。有关更多信息,请参见本主题中的“标识执行数据变更的客户端”。

4 如果不希望将所做变更发送回执行变更的客户端,则是必需的。有关更多信息,请参见本主题中的“标识执行数据变更的客户端”。

备注

除了上面描述的变更之外,您可能会创建存储过程以进行数据访问。本文档中的大多数示例均使用内联 SQL,因为这样更易于演示代码中发生的活动。在成品应用程序中应使用存储过程,理由如下:存储过程封装了代码,通常性能更佳,而且可以提供比内联 SQL 更高的安全性(如果编写正确)。

确定要下载到客户端的数据变更

在仅下载同步和双向同步中,必须跟踪服务器上的变更,以便 Sync Framework 可以确定应将哪些变更下载到客户端。虽然 Sync Framework 没有明确定义支持变更跟踪的方法,但是可以通过一种常见的方法来实现这一目的。对于要同步的每个表,可以采取以下方法:

  • 添加一个用于跟踪行在服务器数据库中的插入时间的列。

  • 添加一列(在某些情况下是一个触发器),用于跟踪行在服务器数据库中的上一次更新时间。

  • 添加一个“逻辑删除表”**和一个触发器,用于跟踪行从服务器数据库中删除的时间。如果不希望从服务器中删除数据,但是必须将删除信息发送到客户端,则可以在基表中跟踪逻辑删除:使用一列(通常为 bit 类型)来指示某行已删除,并使用另一列来跟踪删除发生的时间。

这些列和逻辑删除表与“定位点”**一起使用,以确定要下载的插入、更新和删除。定位点仅仅是用来定义一组要同步的变更的一个时间点。请考虑以下查询:

  • SelectIncrementalInsertsCommand 属性指定的查询。此查询从 Sync Framework 示例数据库中的 Sales.Customer 表下载增量插入,如下所示:

    SELECT CustomerId, CustomerName, SalesPerson, CustomerType FROM
    Sales.Customer WHERE InsertTimestamp > @sync_last_received_anchor
    AND InsertTimestamp <= @sync_new_received_anchor
    

    有关此属性以及与同步命令相关的其他属性的更多信息,请参见如何指定快照同步、下载同步、上载同步和双向同步

  • SelectNewAnchorCommand 属性指定的查询。此查询检索一个时间点值。InsertTimestamp 列中存储时间戳值。因此,查询将使用 SQL Server 2005 Service Pack 2 中引入的 Transact-SQL MIN_ACTIVE_ROWVERSION 函数从服务器数据库检索时间戳值,如下所示:

    SELECT @sync_new_received_anchor = MIN_ACTIVE_ROWVERSION - 1
    

    MIN_ACTIVE_ROWVERSION 返回当前数据库中最小的活动 timestamp(也称为 rowversion)值。尚未提交的事务中使用的 timestamp 值处于活动状态。如果数据库中没有活动的值,MIN_ACTIVE_ROWVERSION 将返回与 @@DBTS + 1 相同的值。MIN_ACTIVE_ROWVERSION 对于诸如使用 timestamp 值将多组变更分组在一起的数据同步等情况非常有用。如果应用程序在其定位点命令中使用的是 @@DBTS 而不是 MIN_ACTIVE_ROWVERSION,则可能会丢失那些在进行同步时处于活动状态的变更。

如果是首次同步 Sales.Customer 表,则会执行以下过程:

  1. 执行新的定位点命令。该命令返回值 0x0000000000000D49。此值存储在客户端数据库中。该表从未进行过同步。因此,客户端数据库中原本没有存储任何从先前同步操作所得到的定位点值。在本例中,Sync Framework 使用 SQL Server timestamp 数据类型可用的最小值:0x0000000000000000。Sync Framework 执行的查询如下所示。此查询从表中下载架构和所有行。

    exec sp_executesql N'SELECT CustomerId, CustomerName, SalesPerson,
    CustomerType FROM Sales.Customer WHERE (InsertTimestamp >
    @sync_last_received_anchor AND InsertTimestamp <=
    @sync_new_received_anchor)',N'@sync_last_received_anchor timestamp,
    @sync_new_received_anchor timestamp',
    @sync_last_received_anchor=0x0000000000000000,
    @sync_new_received_anchor=0x0000000000000D49
    
  2. 在第二次进行同步时,执行新的定位点命令。自上一次同步以来已经插入了某些行。因此,该命令返回值 0x0000000000000D4C。该表以前已进行过同步。因此,Sync Framework 可以检索定位点值 0x0000000000000D49。此值存储在先前同步操作的客户端数据库中。执行的查询如下所示。该查询只从表中下载在两个定位点值之间插入的行。

    exec sp_executesql N'SELECT CustomerId, CustomerName, SalesPerson,
    CustomerType FROM Sales.Customer WHERE (InsertTimestamp >
    @sync_last_received_anchor AND InsertTimestamp <=
    @sync_new_received_anchor)', N'@sync_last_received_anchor timestamp,
    @sync_new_received_anchor timestamp',
    @sync_last_received_anchor=0x0000000000000D49,
    @sync_new_received_anchor=0x0000000000000D4C
    

有关更新和删除命令的示例,请参见如何将增量数据变更下载到客户端如何在客户端和服务器之间交换双向增量数据变更

如上所述,用于检索定位点值的命令取决于服务器数据库中跟踪列的数据类型。本文档中的示例使用 SQL Server timestamp(也称为 rowversion)。若要使用 SQL Server datetime 列,新定位点命令的查询应类似于如下所示的查询:

SELECT @sync_new_received_anchor = GETUTCDATE()

若要确定对某一定位点使用哪种数据类型,应该权衡应用程序的要求并且考虑可以在多大程度上变更服务器数据库架构。如果数据库处于开发阶段,则可以精确指定要添加的列和触发器。如果数据库已经投入使用,您的选择可能较为有限。请考虑以下准则:

  • 同步组中的所有表应使用相同的数据类型和新的定位点命令。如果可以,请为所有组使用相同数据类型和命令。

  • datetime 数据类型易于理解,表经常使用这种类型的列跟踪行的修改时间。但是,如果客户端位于不同时区,使用此数据类型可能会出现问题。如果使用这种数据类型,在选择了增量变更时,可能会丢失事务。

  • timestamp 数据类型很精确,而且不依赖于时钟时间。但是,SQL Server 数据库中的每个表只能包含一个这种数据类型的列。因此,如果必须区分插入和更新,则可以添加一个不同数据类型的列(例如 binary(8)),并在该列中存储时间戳值。有关示例,请参见 用于数据库提供程序帮助主题的安装脚本。如果从备份还原服务器数据库,则 timestamp 数据类型可能是一个问题。有关更多信息,请参见Sync Framework 支持的数据库对象。如上所述,建议在选择新定位点的命令中使用 MIN_ACTIVE_ROWVERSION。

标识执行数据变更的客户端

之所以要标识执行数据变更的客户端,主要有两个原因:

  • 在仅进行上载的同步和双向同步中为冲突检测和冲突解决提供支持。

    如果服务器和某个客户端(或多个客户端)可以变更某个给定行,您可能希望标识出变更的执行者。此信息有助于您编写代码,例如,编写确定哪一个变更具有更高的优先级的代码。如果没有此信息,将保留对该行的最近一次变更。

  • 在双向同步期间,防止将变更回送给客户端。

    Sync Framework 首先将变更上载到服务器,然后将变更下载到客户端。如果不跟踪执行变更的客户端的标识,该变更将上载到服务器,然后在同一个同步会话中下载回到该客户端。某些情况下,回送变更是可以接受的,但是在另一些情况下并非如此。

与变更跟踪一样,Sync Framework 没有明确定义如何支持标识跟踪,但是,可以通过一种常见的方法来达到这一目的。对于要同步的每个表,可以采取以下方法:

  • 在基表中添加一列,用于跟踪每次插入操作的执行者。

  • 在基表中添加一列,用于跟踪每次更新操作的执行者。

  • 在逻辑删除表中添加一列,用于跟踪每次删除操作的执行者。

这些列和表与 ClientId 属性一起使用,以确定每个插入、更新或删除操作是由哪个客户端执行的。在任何表使用除快照同步以外的方法进行第一次同步时,Sync Framework 会在客户端上存储一个 GUID 值以标识该客户端。然后将此 ID 传递给 DbServerSyncProvider,以便能够由每个 SyncAdapter 中的选择和更新查询使用。可以通过 ClientId 属性获得该 ID 值。请考虑以下 Transact-SQL 查询:

SELECT CustomerId, CustomerName, SalesPerson, CustomerType FROM
Sales.Customer WHERE InsertTimestamp > @sync_last_received_anchor AND
InsertTimestamp <= @sync_new_received_anchor AND InsertId <>
@sync_client_id

此查询类似于前面用来跟踪在服务器上所做插入操作的查询。WHERE 子句中的语句可确保只下载那些不是由当前进行同步的客户端所做的插入。

Sync Framework 还允许应用程序通过在服务器上使用一个整数而不是 GUID 值来标识客户端。有关更多信息,请参见如何使用会话变量

有关服务器准备的示例

下面的示例演示如何使用跟踪基础结构设置 Sync Framework 示例数据库中的 Sales.Customer 表,以处理最复杂的应用方案:具有冲突检测的双向插入、更新和删除操作。复杂程度较小的方案不需要使用整个基础结构。有关更多信息,请参见本主题前面的“同步方案的服务器要求”。有关创建本示例中的对象及其他对象的完整脚本,请参见 用于数据库提供程序帮助主题的安装脚本。有关如何使用这些对象的更多信息,请参见如何指定快照同步、下载同步、上载同步和双向同步

本节中的示例在准备服务器的过程中执行以下步骤:

  1. 验证 Sales.Customer 架构。确定该表是否具有主键和任何可以用于变更跟踪的列。

  2. 添加列以跟踪执行插入和更新的时间和位置。

  3. 创建一个逻辑删除表,并向 Sales.Customer 表添加一个触发器以填充该逻辑删除表。

验证 Sales.Customer 的架构

下面的代码示例演示 Sales.Customer 表的架构。该表在 CustomerId 列上有一个主键,并且没有可用于跟踪变更的列。

CREATE TABLE SyncSamplesDb.Sales.Customer(
    CustomerId uniqueidentifier NOT NULL PRIMARY KEY DEFAULT NEWID(), 
    CustomerName nvarchar(100) NOT NULL,
    SalesPerson nvarchar(100) NOT NULL,
    CustomerType nvarchar(100) NOT NULL)

添加列以跟踪插入和更新操作

下面的代码示例添加四列:UpdateTimestampInsertTimestampUpdateIdInsertIdUpdateTimestamp 列是一个 SQL Server timestamp 列。此列在更新行时会自动进行更新。如上所述,一个表只能有一个 timestamp 列。因此,InsertTimestamp 列是一个 binary(8) 列,其默认值为 @@DBTS + 1。该示例加上 @@DBTS 返回的值,以便 UpdateTimestampInsertTimestamp 列在执行插入后具有相同的值。如果不这样做,则看起来就像是每一行在插入之后都进行了更新。

由 Sync Framework 为每个客户端创建的 ID 是 GUID;因此,两个 ID 列均为 uniqueidentifier 列。这两个列的默认值为 00000000-0000-0000-0000-000000000000。此值指示服务器执行的操作是更新还是插入。后面的示例在逻辑删除表中包括一个 DeleteId 列。

ALTER TABLE SyncSamplesDb.Sales.Customer 
    ADD UpdateTimestamp timestamp
ALTER TABLE SyncSamplesDb.Sales.Customer 
    ADD InsertTimestamp binary(8) DEFAULT @@DBTS + 1
ALTER TABLE SyncSamplesDb.Sales.Customer 
    ADD UpdateId uniqueidentifier NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'
ALTER TABLE SyncSamplesDb.Sales.Customer 
    ADD InsertId uniqueidentifier NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'

现在,已经添加了这些列,下面的示例代码将添加索引。示例代码中的这些索引和其他索引都是在同步期间所查询的列上创建的。添加索引的目的在于强调:当您在服务器数据库中实现变更跟踪的时候应当考虑到索引问题。确保您在服务器性能和同步性能之间获得平衡。

CREATE NONCLUSTERED INDEX IX_Customer_UpdateTimestamp
ON Sales.Customer(UpdateTimestamp)

CREATE NONCLUSTERED INDEX IX_Customer_InsertTimestamp
ON Sales.Customer(InsertTimestamp)

CREATE NONCLUSTERED INDEX IX_Customer_UpdateId
ON Sales.Customer(UpdateId)

CREATE NONCLUSTERED INDEX IX_Customer_InsertId
ON Sales.Customer(InsertId)

添加逻辑删除表以跟踪删除操作

下面的代码示例创建一个逻辑删除表,该表具有一个聚集索引和一个用来填充表的触发器。当 Sales.Customer 表中发生删除操作时,触发器便会在 Sales.Customer_Tombstone 表中插入一行。在触发器执行插入操作之前,触发器会检查 Sales.Customer_Tombstone 表是否已包含主键与已删除行的主键相同的行。如果先从 Sales.Customer 中删除一行,再重新插入该行,然后再次删除它,便会出现上述情况。如果在 Sales.Customer_Tombstone 中检测到这样的一行,触发器会删除然后重新插入该行。Sales.Customer_Tombstone 中的 DeleteTimestamp 列可能也会进行更新。

CREATE TABLE SyncSamplesDb.Sales.Customer_Tombstone(
    CustomerId uniqueidentifier NOT NULL PRIMARY KEY NONCLUSTERED, 
    CustomerName nvarchar(100) NOT NULL,
    SalesPerson nvarchar(100) NOT NULL,
    CustomerType nvarchar(100) NOT NULL,
    DeleteId uniqueidentifier NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000',
    DeleteTimestamp timestamp)

CREATE TRIGGER Customer_DeleteTrigger 
ON SyncSamplesDb.Sales.Customer FOR DELETE 
AS 
BEGIN 
    SET NOCOUNT ON
    DELETE FROM SyncSamplesDb.Sales.Customer_Tombstone 
        WHERE CustomerId IN (SELECT CustomerId FROM deleted)
    INSERT INTO SyncSamplesDb.Sales.Customer_Tombstone (CustomerId, CustomerName, SalesPerson, CustomerType) 
    SELECT CustomerId, CustomerName, SalesPerson, CustomerType FROM deleted
    SET NOCOUNT OFF
END

CREATE CLUSTERED INDEX IX_Customer_Tombstone_DeleteTimestamp
ON Sales.Customer_Tombstone(DeleteTimestamp)

CREATE NONCLUSTERED INDEX IX_Customer_Tombstone_DeleteId
ON Sales.Customer_Tombstone(DeleteId)

请参阅

概念

跟踪服务器数据库中的变更