SQL Server, Change Tracking и Visual Studio 2010 – практический пример

В данной статье я хочу показать, как с помощью SQL Server, Change Tracking и Visual Studio 2012 создать небольшое приложение, которое будет иметь свою локальную базу данных и сможет выполнять двухстороннюю синхронизацию с основным сервером. Это как раз основное предназначение технологии. В качестве примера я буду использовать базу данных из своей предыдущей статьи (также скрипт создания базы можно скачать по этой ссылке). В качестве среды разработки буду использовать Visual Studio 2010 и C#, в качестве основного сервера баз данных MS SQL Server установленный локально на моем ноутбуке.

  1. Шаг первый: выполняем скрипт создания базы данных на локальном SQL Server.
  2. Запускаем Visual Studio и создаем новый проект Windows Forms.

  3. В меню Project -> Add New Item выбираем Local Database Cache и добавляем его в текущий проект.

  4. В проект будет добавлен файл .sync и откроется окно Configure Data Synchronization.

  5. Для установки Server connection нажимаем кнопку New и создаем подключение с нашим локальным SQL Server.

  6. Параметр Client Connection можно настроить на уже существующий файл SQL Server Compact Edition. Если нет локальной базы данных, можно оставить настройку TrackingChanges_test.sdf (new) по умолчанию для создания новой базы данных в проекте. Имя новой базы данных будет основываться на имени базы данных на сервере.
  7. Опцию Use SQL Server change tracking оставляем включенной по умолчанию.
  8. В раздел Cached Tables с помощью кнопки OK добавляем таблицы, для которых мы хотим настроить лакольное кэширование. Появляется окно Configure Tables for Offline Use. Значение параметра Data to download оставляем по умолчанию New and incremental changes after first synchronization. Наше приложение будет извлекать из сервера записи, которые были изменены с момента последней синхронизации. Во время первой синхронизации будет загружена вся таблица. Если выбрать значение Entire table each time, то при синхронизации локальная копия таблицы будет заменяться ее версией с сервера баз данных.

  9. Теперь в окне Configure Data Synchronization развернем раздел Advanced. С помощью опции Synchronize tables in a single transaction мы можем задать, будут ли таблицы синхронизированы по отдельности или в пределах одной транзакции. По умолчанию этот флажок не установлен, и все таблицы будут синхронизироваться по отдельности. Если возникнут ошибки, только таблицы с ошибками откатят свои изменения. Если выбран этот параметр, все таблицы синхронизируются в одной транзакции. Если обнаружены ошибки, все модификации для всех таблиц откатываются.
  10. Для параметра Create synchronization components оставляем значение по умолчанию Client and Server, т.к. мы хотим создать приложение, которое будет выполнять двухстороннюю синхронизацию между кэшем и основной базой данных.
  11. Если мы нажмем ссылку Show Code Example, то нам будет приведен пример кода для нашего приложения, который будет делать синхронизацию. Скопируем его в буфер обмена, т.к. он нам еще пригодится.

  12. Нажимаем кнопку OK и закрываем окно Configure Data Synchronization. Теперь перед нами появляется Data Source Configuration Wizard.

  13. Оставляем по умолчанию значение Dataset и жмем Next.
  14. В появившемся окне выбираем таблицы clients и orders, задаем имя нашему DataSet и жмем Finish.

  15. В принципе на этом настройка локального кэша в нашем приложении закончена. Давайте теперь добавим несколько элементов в наше приложение, чтобы визуально посмотреть, как все будет работать. В первую очередь добавим элемент DataGridView и в качестве Data Source я выберу таблицу clients из моего MyDataSet.

    Также я оставлю возможность редактировать записи прямо в моем DataGridView и для удобства переименую его в dataGridView_clients.

  16. Аналогичным образом я добавляю еще один DataGridView для таблицы orders. Также мне потребуются 3 кнопки: Sync, Submit Changes и Refresh Data.
  17. Кнопка Refresh Data будет обновлять наши элементы DataGridView записями из локального кэша. В обработчик на нажатие этой кнопки я добавлю следующий код.
  18. try
    {
        this.ordersTableAdapter.Fill(this.myDataSet.orders);
        this.clientsTableAdapter.Fill(this.myDataSet.clients);
    }
    catch (System.Exception ex)
    {
        System.Windows.Forms.MessageBox.Show(ex.Message);
    }
  19. Задача кнопки Submit Changes – передавать и сохранять измененения в элементах DataGridView в наш локальный кэш приложения. На ней соответственно будет исполнятся следующий код.
  20. try
    {
        this.Validate();
     
        this.clientsBindingSource.EndEdit();
        this.ordersBindingSource.EndEdit();
     
        this.clientsTableAdapter.Update(this.myDataSet.clients);
        this.ordersTableAdapter.Update(this.myDataSet.orders);
     
        MessageBox.Show("Update successful");
    }
    catch (System.Exception ex)
    {
        MessageBox.Show("Update failed: " + ex.Message);
    }
  21. И наконец с помощью кнопки Sync будет выполняться синхронизация нашего локального кэша с основной базой данных.
  22. try
    {
        MyLocalDataCacheSyncAgent syncAgent = new MyLocalDataCacheSyncAgent();
        Microsoft.Synchronization.Data.SyncStatistics syncStats = syncAgent.Synchronize();
     
        MessageBox.Show("Changes downloaded: " + syncStats.TotalChangesDownloaded.ToString() + Environment.NewLine + "Changes uploaded: " + syncStats.TotalChangesUploaded.ToString());
    }
    catch (System.Exception ex)
    {
        System.Windows.Forms.MessageBox.Show(ex.Message);
    }
  23. Также для того, чтобы обеспечить именно двухстороннюю синхронизацию, в окне Solution Explorer на MyLocalDataCache.sync жмем правой клавишей мыши, выбираем View Code и задаем такой код:
  24. namespace ChangeTrackingSyncApp_example {
     
        public partial class MyLocalDataCacheSyncAgent {
     
            partial void OnInitialized(){
                this._clientsSyncTable.SyncDirection = Microsoft.Synchronization.Data.SyncDirection.Bidirectional;
             this._ordersSyncTable.SyncDirection = Microsoft.Synchronization.Data.SyncDirection.Bidirectional;
            }
        }
    }

Вот в принципе и все. На этом маленьком и максимально упрощенном примере я хотел показать, как просто можно создать приложение с локальным кэшем и использовать Change Tracking для двухсторонней синхронизации данных. В дополнение к статье я выложу видео, в котором я проделаю те же самые шаги, что и в статье, а также покажу, как будет работать наше приложение. Оставайтесь на связи, я планирую опубликовать еще одну заметку по Change Tracking и затем перейти к технологии Change Data Capture.

  • Доброго дня вам. Меня очень понравился ваша статья. Оно очень детально обсуждает change tracking. У меня вопрос такой – как можно синхронизировать SQL Database (файл mdf) с SQL Sever, так как это делается с помщью чанге тракинг?

    Like or Dislike: Thumb up 0 Thumb down 0

  • Не совсем понял вас. Вы имеете ввиду подключаемый файл mdf в своем проекте Visual Studio? Если да, то например на SQL Server вы можете вручную включить Change Tracking и использовать обычнее его команды, чтобы получать список изменений с сервера. Только придется всю синхронизацию вручную прописывать в коде. А вот если хотите обратную синхронизацию, то сходу сразу не скажу: насколько я помню, это в принципе обычная БД, которая подключаетсяотключается к локальному экземпляру. А вообще использование именно mdf файла в вашем проекте чем мотивировано?

    Like or Dislike: Thumb up 0 Thumb down 0

  • Спасибо за ответ. в моем проекте я использую Service-based Database из за Database Diagram, что бы прeдoвтартить добавление неправильних данных, то есть не связанных(relationship), а также предотвращение удаления связанных данных.

    Like or Dislike: Thumb up 0 Thumb down 0

  • Но тот же SQL Server Compact поддерживает Foreign Keys, но правда там нет диаграмм, хотя мне кажется это не повод делать выбор. Кстати, если уж вы не хотиет использовать Compact Edition, то почему бы на клиенте не реализовать связку SQL Express и ваше приложение, вместо подключаемой базы?

    Like or Dislike: Thumb up 0 Thumb down 0

  • Потому что есть много клиентов и один сервер. Кожди клиент должен работать offline а после работы синхронизировать данные с сервером.

    Like or Dislike: Thumb up 0 Thumb down 0

  • Все верно, на клиенте будет SQL Express и SQL Express будет синхронизовываться в обе стороны с основным сервером. Тогда легко сможете использовать change tracking.

    Like or Dislike: Thumb up 0 Thumb down 0

  • Давайте я поясну. Пишу программу “рабочее место кассира”. Ну вот, есть много касс(клиенты) которие свoи данные, например наменкалатура, палучают из сервера, а обратную синхронизацию отправляют только документы чеков, тоесть продажа. У каждого чека уникальный номер, чтобы на сервере не запутать с чеками из других касс. База виброна SQL Datatabase (Service-based database) о чем я прежне говорил. База данных содаётса и каждая касcа работает нармально в локальном. Остается синхронизировать данные. Change Tracking работает только с Compact SQL(sdf), а тyт нужно синхронизация с файлом mdf.

    Like or Dislike: Thumb up 0 Thumb down 0

  • Change Tracking работает с любой версией, только вот я не уверен, как заставить его работать с Serice-based database. Но т.к. она из себя представляет просто подключаему базу данных, то почему бы вместо нее не использовать SQL Express на клиенте (все равно на клиенте он должен быть установлен для работы service-based database). Далее можно организовать синхронизацию разными способами, в т.ч. и change tracking. Вот только использовать мастеры для его настройки не получится, придется написать процедуры для синхронизации самому и запускать это все во время соединения с главным сервером.

    Like or Dislike: Thumb up 0 Thumb down 0

  • Я написал вот такой класс для синхронизаци, но тут оказалась есть проблема, что кады раз во время синхронизаци я должен буту удалить все синхонируымие таблицы из клиента, чтобы синхронизация работала правильно, этого моон делать когда клиенты полачают данные, но нелезя, когда они отрправлают чеки на сервер. на сервер мы не можем очистить таблица чеков.

    using System;
    using System.Data;
    using System.Data.SqlClient;
    using Microsoft.Synchronization;
    using Microsoft.Synchronization.Data;
    using Microsoft.Synchronization.Data.SqlServer;

    namespace ExecuteExpressSync
    {
    public class PosSyncAgent
    {
    private SyncOperationStatistics _syncstat;
    private string[] _tablenames;

    private const string Serverconnstring = @”Data Source=ART-PCSQLEXPRESS;” +
    “Initial Catalog=POSDataBase;Persist ” +
    “Security Info=True;User ID=sa;Password=111″;

    private const string Clientconnstring = @”Data Source=.SQLEXPRESS;” +
    @”AttachDbFilename=C:UsersArtDesktopposdb.mdf;” +
    “Integrated Security=True;Connect Timeout=30;User Instance=True”;

    public PosSyncAgent()
    {
    _tablenames = new[]
    {
    ” Users; “,
    ” UserTypes; “,
    ” ProductBarcodes; “,
    ” ProductsList; “,
    ” Units; “,
    ” DiscountCard; “,
    ” CustomersList; “,
    ” CardTypes;”
    };
    }

    #region Public Properties
    public SyncOperationStatistics DownloadStat
    {
    get { return _syncstat; }
    }
    #endregion

    public void SetupDBSynchronization(bool includeserverside)
    {
    var serverConn = new SqlConnection(Serverconnstring);
    var clientConn = new SqlConnection(Clientconnstring);

    try
    {
    clientConn.Open();

    using (var cmd = clientConn.CreateCommand())
    {
    foreach (var t in _tablenames)
    {
    cmd.CommandText = “delete from” + t;
    cmd.ExecuteNonQuery();
    }
    }

    var productScope = new DbSyncScopeDescription(“TableForUploads”);
    productScope.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable(“dbo.UserTypes”, serverConn));
    productScope.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable(“dbo.Users”, serverConn));
    productScope.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable(“dbo.Units”, serverConn));
    productScope.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable(“dbo.ProductsList”, serverConn));
    productScope.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable(“dbo.ProductBarcodes”, serverConn));
    productScope.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable(“dbo.CardTypes”, serverConn));
    productScope.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable(“dbo.CustomersList”, serverConn));
    productScope.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable(“dbo.DiscountCard”, serverConn));
    if (includeserverside)
    {
    var serverProvision = new SqlSyncScopeProvisioning(serverConn, productScope);

    if (!serverProvision.ScopeExists(“TableForUploads”))
    serverProvision.Apply();
    }

    var clientProvision = new SqlSyncScopeProvisioning(clientConn, productScope);

    if (!clientProvision.ScopeExists(“TableForUploads”))
    clientProvision.Apply();

    productScope = new DbSyncScopeDescription(“TableForDownloads”);
    productScope.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable(“dbo.InvoiceHeader”, serverConn));
    productScope.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable(“dbo.InvoiceData”, serverConn));

    if (includeserverside)
    {
    var serverProvision = new SqlSyncScopeProvisioning(serverConn, productScope);

    if (!serverProvision.ScopeExists(“TableForDownloads”))
    serverProvision.Apply();
    }

    clientProvision = new SqlSyncScopeProvisioning(clientConn, productScope);

    if (!clientProvision.ScopeExists(“TableForDownloads”))
    clientProvision.Apply();
    }
    catch (Exception ex)
    {
    throw new Exception(“Synchronization configuration error. ” + ex.Message);
    }
    finally
    {
    serverConn.Close();
    serverConn.Dispose();
    clientConn.Close();
    clientConn.Dispose();
    }
    }

    public void DownloadData()
    {
    if (!TableExists(“ProductsList_tracking”))
    {
    throw new Exception(“Database synchronization not created.”);
    }

    var serverConn = new SqlConnection(Serverconnstring);
    var clientConn = new SqlConnection(Clientconnstring);

    try
    {
    var syncOrchestrator = new SyncOrchestrator();
    var serverProvider = new SqlSyncProvider(“TableForUploads”, serverConn);
    var clientProvider = new SqlSyncProvider(“TableForUploads”, clientConn);
    syncOrchestrator.LocalProvider = serverProvider;
    syncOrchestrator.RemoteProvider = clientProvider;
    syncOrchestrator.Direction = SyncDirectionOrder.Upload;
    _syncstat = syncOrchestrator.Synchronize();
    }
    catch (Exception ex)
    {
    throw new Exception(“Database synchronization error. ” + ex.Message);
    }
    finally
    {
    serverConn.Close();
    serverConn.Dispose();
    clientConn.Close();
    clientConn.Dispose();
    }
    }

    public void UploadData()
    {
    if (!TableExists(“InvoiceData_tracking”))
    {
    throw new Exception(“Database synchronization not created.”);
    }

    var serverConn = new SqlConnection(Serverconnstring);
    var clientConn = new SqlConnection(Clientconnstring);

    try
    {
    var syncOrchestrator = new SyncOrchestrator();
    var serverProvider = new SqlSyncProvider(“TableForDownloads”, serverConn);
    var clientProvider = new SqlSyncProvider(“TableForDownloads”, clientConn);
    syncOrchestrator.LocalProvider = serverProvider;
    syncOrchestrator.RemoteProvider = clientProvider;
    syncOrchestrator.Direction = SyncDirectionOrder.DownloadAndUpload;
    _syncstat = syncOrchestrator.Synchronize();
    }
    catch (Exception ex)
    {
    throw new Exception(“Database synchronization error. ” + ex.Message);
    }
    finally
    {
    serverConn.Close();
    serverConn.Dispose();
    clientConn.Close();
    clientConn.Dispose();
    }
    }

    public void Deprovision(bool includeserverside)
    {
    var serverConn = new SqlConnection(Serverconnstring);
    var clientConn = new SqlConnection(Clientconnstring);

    try
    {
    clientConn.Open();

    using (var cmd = clientConn.CreateCommand())
    {
    foreach (var t in _tablenames)
    {
    cmd.CommandText = “delete from” + t;
    cmd.ExecuteNonQuery();
    }
    }

    if (!TableExists(“ProductsList_tracking”))
    {
    return;
    }

    var serverSqlDepro = new SqlSyncScopeDeprovisioning(serverConn);
    var clientSqlDepro = new SqlSyncScopeDeprovisioning(clientConn);

    if (includeserverside)
    {
    serverSqlDepro.DeprovisionScope(“TableForUploads”);
    serverSqlDepro.DeprovisionStore();
    }

    clientSqlDepro.DeprovisionScope(“TableForUploads”);
    clientSqlDepro.DeprovisionStore();

    if (TableExists(“InvoiceData_tracking”))
    {
    if (includeserverside)
    {
    serverSqlDepro.DeprovisionScope(“TableForDownloads”);
    serverSqlDepro.DeprovisionStore();
    }

    clientSqlDepro.DeprovisionScope(“TableForDownloads”);
    clientSqlDepro.DeprovisionStore();
    }
    }
    catch (Exception ex)
    {
    throw new Exception(“Deprovision error. ” + ex.Message);
    }
    finally
    {
    serverConn.Close();
    serverConn.Dispose();
    clientConn.Close();
    clientConn.Dispose();
    }
    }

    public string [] TablesRowCounts()
    {
    var clientConn = new SqlConnection(Clientconnstring);
    var tablenames = new string[_tablenames.Length];

    try
    {
    clientConn.Open();

    using (var cmd = clientConn.CreateCommand())
    {
    for (int i = 0; i < _tablenames.Length; i++)
    {
    cmd.CommandText = "select count(*) from" + _tablenames[i];

    using (var sqlrdr = cmd.ExecuteReader())
    {
    if (sqlrdr.Read())
    {
    tablenames[i] = _tablenames[i] + "t : " + sqlrdr.GetInt32(0);
    }
    }
    }
    }
    }
    catch (Exception ex)
    {
    throw new Exception("Tables data getting error. " + ex.Message);
    }
    finally
    {
    clientConn.Close();
    clientConn.Dispose();
    }

    return tablenames;
    }

    public bool TableExists(string tablename)
    {
    var clientConn = new SqlConnection(Clientconnstring);

    try
    {
    using (var command = new SqlCommand("Select * from " + tablename, clientConn))
    {
    command.CommandType = CommandType.Text;
    clientConn.Open();
    command.ExecuteNonQuery();
    return true;
    }
    }
    catch{}
    finally
    {
    clientConn.Close();
    clientConn.Dispose();
    }

    return false;
    }
    }
    }

    Like or Dislike: Thumb up 0 Thumb down 0

  • Олег

    День добрый. Решил использовать Change Tracking в своем проекте, но столкнулся с проблемкой в Visual Studio 2012 нет такого шаблона. Возник вопрос, почему его убрали, и может появилось что-то новое ему на замену?

    Like or Dislike: Thumb up 0 Thumb down 0