Демо I. Автоматическая генерация SyncAdapterBuilder   

   

Демо I. Автоматическая генерация SyncAdapterBuilder

 
Это первый демонстрационный проект, представляющий Synchronization Services для ADO.NET. Здесь предлагается самая простая программа серии, однако, так как она служит для демонстрации логики двунаправленной синхронизации, ее не стоит считать простейшим примером того, что вообще можно построить. Самыми простыми сценариями, с которых можно было бы начать ознакомление с программным интерфейсом Synchronization Services были бы копирование экрана и скачивание, но я решил сразу перейти к более интересным вещам – к двунаправленной синхронизации!

Давайте возьмемся за дело. Цель представленного здесь приложения состоит в извлечении данных из двух таблиц (orders и order_details) оффлайн. Обе таблицы хранятся на сервере, который представляет собой SQL Server 2005. Так что, никаких сюрпризов! При локальном кэшировании данных пользователи моего приложения могут вносить изменения, не обращая внимание на наличие соединения. В определенный момент пользователь может щелкнуть на кнопке синхронизации для соединения с сервером и синхронизации с ним локального КЭШа.

Данное демонстрационное приложение имеет простой пользовательский интерфейс, показанный на следующем рисунке:

 

Главное диалоговое окно показывает текущее содержимое таблиц orders и order_details. Обычно видно только локальное содержимое, но для удобства я добавил опцию для отображения базы данных сервера. Таким образом, вам не придется переключаться на SQL Management Studio, для того чтобы посмотреть колонки сервера и сравнить их с тем, что находится у клиента.

Забегая вперед, замечу, что клиент использует Synchronization Services Framework is SQLCE 3.5, что является частью пакета CTP (транспортный протокол без установления физического соединения). SQLCE не является новым членом семейства SQL Server, однако, его использование до настоящего времени было ограничено только уровнем устройств. Теперь же он работает и с интерактивной средой, имея встроенную поддержку синхронизации. Откровенно говоря, SQLCE – это настоящая находка. Возможно, я и преувеличиваю, но посудите сами: это маленькая, легкая встроенная база данных. Ее можно использовать, не утруждая себя установкой SQL Express и т.д. Чего вам еще нужно?

ВНИМАНИЕ: на момент написания этого текста первый CTP сервисов синхронизации имел известную проблему интеграции с SQL Management Studio и SQLCE 3.5. Любое изменение, осуществленное через SSMS в файле базы данных SqlCe не загрузится при попытке синхронизации с сервером. Это происходит, потому что SSMS ссылается на старую DLL. Эта особенность усложняет процедуру внесения изменений в локальную базу данных, которую мы будем использовать для экспериментов с загрузкой (upload).  По этой причине я добавил три кнопки справа под сеткой. Первая кнопка служит для вставки случайной строки, вторая – для обновления ноля или более строк, а третья – для удаления ноля и более строк.

Теперь, когда вы готовы к синхронизации, просто нажмите кнопку «Synchronize», и появится новое диалоговое окно, показывающее степень завершенности процесса синхронизации. Вот еще один рисунок:


Не самый красивый индикатор, но идею он передает.

 В данной демонстрационной программе есть два основных аспекта:

 
Первый: Построение очень простого рабочего слежения за изменениями

Файл demo.sql, расположенный в папке установки, содержит набор утверждений TSQL, направленных на установку слежения за изменениями для каждой таблицы на сервере. Слежение за изменениями является ключевой частью разработки любой системы синхронизации. В своей основе это инфраструктура, которая позволяет найти ответ на такой вопрос как «что изменилось с момента последней синхронизации?» Здесь можно многое сказать, но я приберегу это для следующих демо-программ. В этом же простом подходе слежения мне будет необходимо несколько вещей для каждой таблицы:

  1. update_originator_id: это колонка значений типа int, которая показывает кто сделал последнее изменение в строке.
  2. update_timestamp: колонка служит для записи времени обновления строки.
  3. create_timestamp: служит для записи времени, когда была вставлена строка
  4. tombstone_table: служит для сохранения удаленной строки
  5. update trigger: для сохранения update_originator_id после операции обновления
  6. delete trigger: для копирования удаленной строки в специальную таблицу

Вот как выглядит код простого слежения за изменениями:

--
-- Add tracking columns
-- 1. Create update_originator_id column defaulted to 0 to indicate server change
ALTER TABLE pub..orders add update_originator_id int null default 0
ALTER TABLE pub..order_details add update_originator_id int null default 0
go
   
-- Add last update timestamp column
ALTER TABLE pub..orders add update_timestamp timestamp
ALTER
TABLE pub..order_details add update_timestamp timestamp
go

-- Add create timestamp column (use bigint since one one timestamp column type is allowed per table)ALTER TABLE pub..orders add create_timestamp bigint default @@DBTS+1
ALTER TABLE pub..order_details add create_timestamp bigint default @@DBTS+1
go   
 

--
-- Create tombstone tables to store deletes
--
CREATE TABLE pub..orders_tombstone(
   
order_id int NOT NULL primary key,
    order_date datetime NULL,
    update_originator_id int default 0, 
    update_timestamp timestamp,
    create_timestamp bigint)

CREATE TABLE pub..order_details_tombstone(
   
order_id int NOT NULL primary key,
    order_details_id int NOT NULL,
    product nvarchar(100) NULL,
    quantity int NULL,
    update_originator_id int default 0, 
    update_timestamp timestamp,
    create_timestamp bigint)
go

--
-- Create Update and Delete Triggers
-- Since there will be changes on the server outside of the sync application
-- we need triggers to fix up update_originator_id back to 0 which designated
-- for server change

-- update triggers
use pub
go
CREATE TRIGGER orders_update_trigger on orders for update
as
    declare @key int
    select @key = order_id from inserted
    if
not UPDATE(update_originator_id)
        update orders set
update_originator_id = 0 where order_id = @key
go
 

CREATE TRIGGER order_details_update_trigger on order_details for update
as
    declare
@key int
    select
@key = order_id from inserted
    if
not UPDATE(update_originator_id)
        update orders set update_originator_id = 0 where order_id = @key
go
 

-- delete triggers
use pub
go
CREATE TRIGGER orders_delete_trigger on orders for delete
as
    insert
into pub..orders_tombstone (order_id, order_date, create_timestamp, update_originator_id)     select order_id, order_date, create_timestamp, 0 from deleted
go     

CREATE TRIGGER order_details_delete_trigger on order_details for delete
as
    insert into pub..order_details_tombstone (
        order_id,
        order_details_id,
        product, quantity,
        create_timestamp,
        update_originator_id)
    select

        order_id,
        order_details_id,
        product,
        quantity,
        create_timestamp,
        0
    from
deleted
go   

Несмотря на то, что мне не нужно хранить что-то кроме PK для удаленных строк, я предпочел сохранить их содержимое. Почему? Это будет ясно из последующих демонстрационных программ, но уже сейчас вам стоит знать, что это имеет отношение к разрешению конфликтов.

 
Второй: чудо под названием SyncAdapterBuilder

 Сервисы синхронизации были спроектированы по типу ADO.NET DataAdapter. Но в отличие от DataAdapter у SyncAdapter всего восемь команд. Адаптеры добавлены в коллекцию с классом ServerSyncProvider, который в свою очередь дает еще две команды. Таким образом, если у вас две таблицы, как в нашей программе, вам нужно написать (8 * 2) + 2 = 18 команд! А это немало.

SyncAdapterBuilder служит для облегчения трудов разработчика, особенно когда речь идет о новом интерфейсе. Позже ручное программирование SyncAdapter, которое позволяет добиваться поразительных результатов, станет привязанностью (как это случилось со мной), а «билдер» будет казаться слишком примитивным. Теперь давайте посмотрим, как я создал объекты SyncAdapter для таблиц orders и order_details:

//
// orders table
//
SqlSyncAdapterBuilder ordersBuilder = new SqlSyncAdapterBuilder();               
ordersBuilder.Connection = serverConnection;
ordersBuilder.SyncDirection = SyncDirection.Bidirectional;

// base table
ordersBuilder.TableName = "orders";
ordersBuilder.DataColumns.Add("order_id");
ordersBuilder.DataColumns.Add("order_date");

// tombstone table
ordersBuilder.TombstoneTableName = "orders_tombstone";
ordersBuilder.TombstoneDataColumns.Add("order_id");
ordersBuilder.TombstoneDataColumns.Add("order_date");

// trackingsync columns
ordersBuilder.CreationTrackingColumn = @"create_timestamp";
ordersBuilder.UpdateTrackingColumn = @"update_timestamp";
ordersBuilder.DeletionTrackingColumn = @"update_timestamp";
ordersBuilder.UpdateOriginatorIdColumn = @"update_originator_id";

SyncAdapter ordersSyncAdapter = ordersBuilder.ToSyncAdapter();
serverSyncProvider.SyncAdapters.Add(ordersSyncAdapter);

//
// order_details table
//
 
SqlSyncAdapterBuilder orderDetailsBuilder = new SqlSyncAdapterBuilder();
orderDetailsBuilder.SyncDirection = SyncDirection.Bidirectional;
orderDetailsBuilder.Connection = serverConnection;

// base table
orderDetailsBuilder.TableName = "order_details";
orderDetailsBuilder.DataColumns.Add("order_id");
orderDetailsBuilder.DataColumns.Add("order_details_id");
orderDetailsBuilder.DataColumns.Add("product");
orderDetailsBuilder.DataColumns.Add("quantity");

// tombstone table
orderDetailsBuilder.TombstoneTableName = "order_details_tombstone";
orderDetailsBuilder.TombstoneDataColumns.Add("order_id");
orderDetailsBuilder.TombstoneDataColumns.Add("order_details_id");
orderDetailsBuilder.TombstoneDataColumns.Add("product");
orderDetailsBuilder.TombstoneDataColumns.Add("quantity");

// trackingsync columns
orderDetailsBuilder.CreationTrackingColumn = @"create_timestamp";
orderDetailsBuilder.UpdateTrackingColumn = @"update_timestamp";
orderDetailsBuilder.DeletionTrackingColumn = @"update_timestamp";
orderDetailsBuilder.UpdateOriginatorIdColumn = @"update_originator_id";

SyncAdapter orderDetailsSyncAdapter = orderDetailsBuilder.ToSyncAdapter();
serverSyncProvider.SyncAdapters.Add(orderDetailsSyncAdapter);

Как видите, я добавил созданные адаптеры в SyncServerProvider. Теперь единственное, что остается сделать – это определить команду для получения нового указателя. Эта команда provider wide и берется из папки provider.

//
// 6. Setup provider wide commands
// SelectNewAnchorCommand: Returns the new high watermark for current sync, this value is
// stored at the client and used the low watermark in the next sync
//

// select new anchor command
SqlCommand anchorCmd = new SqlCommand();
anchorCmd.CommandType = CommandType.Text;
anchorCmd.CommandText = "SELECT @@DBTS";               
serverSyncProvider.SelectNewAnchorCommand = anchorCmd;

Ну вот и все. Время синхронизировать…

 Шаги инсталляции приложения OfflineAppDemo:

  • Запустите SQL Server и загрузите файл demo.sql
  • Запустите скрипт до маркера "test sample"
  • Загрузите решение VS (OfflineAppDemo-Builder Project)
  • Скомпилируйте проект
  • Все готово