Articolo Intro to ADO.NET Sync Services pubblicato su Computer Programming di Marzo 2008.
Autore: Roberto Brunetti
Con l’uscita del Framework .NET 3.5 Microsoft propone il primo tassello di un nuovo framework di sincronizzazione informazioni. Questo primo assaggio, denominato ADO.NET Synchronization Services, semplifica la scrittura di codice per replicare/sincronizzare strutture e dati di un database SQL Server verso client remoti che necessitano di lavorare offline.
L’esigenza di replicare il database o porzioni di esso nasce con l’avvento dei portatili e dei palmari a metà degli anni 90: si pensi al classico scenario di raccolta ordini sul campo in cui il venditore scarica sul suo pc/palmare il listino prodotti (tabelle in solo download), inserisce gli ordini su un database locale (in tabelle di solo upload) e magari può modificare alcune informazioni della tabella clienti preventivamente scaricata e successivamente sincronizzata con il database centrale.
Le prime implementazioni su palmare si sono appoggiate ai meccanismi di replica nativi di SQL CE che, sin dalla sua prima versione, propone due tecniche di replica dei dati: Remote Data Access (RDA) e Merge Replication. La prima tecnica consente allo sviluppatore di indicare la query da eseguire server-side e la tabella in locale in cui accogliere i dati tramite il metodo Pull. Una volta scaricati i dati SQL CE tiene monitorate le modifiche effettuate ai dai in locale e consente con un semplice metodo (Push) di inviare al server solo tali modifiche. Tutte le operazioni vengono avviate da codice e non è necessario ne modificare la struttura ne configurare in modo particolare il database server. Diametralmente opposta come approccio troviamo la seconda tecnica nativa di sincronizzazione dei dati presente sia in SQL CE 2.0 che in SQL Server 2000: la Merge Replication richiede una configurazione del database server che comprende la pubblicazione degli oggetti database da replicare con i client, le modalità di replica, l’assegnazione di range per i campi identity, la definizione dei profili di replica (ad esempio il tipo di compressione o il numero di modifiche da inviare in singoli chunk transazionali al client).
Occupandomi di sviluppo mobile dal 1997 ho lavorato molto sia con RDA, scelta preferita nelle casistiche più semplici, sia con la Merge Replication per scenari più “corposi”. Le innumerevoli possibilità di configurazione della Merge Replication la rendono adatta a tutti gli scenari possibili a patto di avere SQL Server (dalla 2000 in poi) come database server e un server IIS che faccia da tramite http per le richieste che arrivano dal client.
In pratica la Merge Replication
· E’ una tecnologia focalizzata sulla configurazione del server
· E’ una tecnologia che fornisce soluzioni end-to-end
· Non è developer-oriented: il codice lato client indica solamente i parametri da usare per quel particolare tipo di client e connessione
· E’ orientata allo scambio di dati e non all’utilizzo di servizi
Per questi motivi il framework .NET in versione 3.5 propone ADO.NET Sync Services, meccanismo più leggero e più gestibile da codice rispetto alla Merge Replication che meglio si adatta a scenari semplici in cui lo sviluppatore, con un impatto minimo sulla struttura delle tabelle, può controllare la replica dei dati. In pratica questa tecnologia consente di avere una cache locale dei dati sul client e i metodi per poter sincronizzare lo store locale con il database server.
I punti fondamentali da chiarire subito sono i seguenti
· La tecnologia si basa su ADO.NET e quindi può lavorare con varie tipologie di database server side
· Lato client viene usato SQL CE 3.5 come store delle informazioni
· Visual Studio 2008 espone wizard e designer per semplificare lo sviluppo
· Il deployment può essere effettuato con ClickOnce
· L’impatto sul server è minimo
· Nativamente lavora single-tier in client-server oppure multi-tier in client-server
· Non è incluso in questa prima versione il supporto nel .NET Compact Framework 3.5
· Il tutto è estendibile per lavorare via servizi ASMX o WCF
Iniziamo con lo schema architetturale delle varie componenti.
Lato client troviamo le classi denominate Client Synchroniztion Provider che si appoggiano al Synchronization Agent per la sincronizzazione delle informazioni. Il Sync Agent controlla la definizione e la modalità di sincronizzazione delle singole tabelle (Sync Table). Al lancio della sincronizzazione il Sync Agent contatta la parte server (per default in-process nella classica topologia client-server) la quale lancia le query sul database per recuperare i dati modificati dall’ultima sincronizzazione e modificare i dati inviati dal client.
Come si può notare dalla slide del nostro corso vengono utilizzati 3 assembly .NET in forma di dll
· Microsoft.Synchronization.Data.SqlServerCe.dll: è la dll utilizzata lato client per iniziare la fase di sincronizzazione. Le classi che si utilizzano derivano dalle classi base definite in questa dll. La versione di tale dll è 3.5, allineata appunto con SQL CE 3.5 che, come abbiamo accennato, viene utilizzato per lo store lato client.
· Microsoft.Synchronization.Data.dll: è in versione 1.0 e rappresenta la parte client del meccanismo di sincronizzazione dei dati. Ogni tabella da sincronizzare espone, sempre tramite derivazione da una classe base definita in questa dll, il tipo di replica da effettuare (ad esempio la tabella è in solo download piuttosto che in upload/download incrementale).
· Microsoft.Synchronization.Data.Server.dll: sempre in versione 1.0 rappresenta la parte server che viene invocata dalla parte client durante ogni processo di sincronizzazione dei dati: anche in questo caso lo sviluppatore deve creare classi derivate da classi base esposte da questa dll per definire i comandi da lanciare sul database server per recuperare i dati o aggiornare le modifiche effettuate dal client.
In pratica, lo sviluppatore deve quindi aderire a questo modello architetturale, creando semplicemente delle classi derivate che rappresentano le tabelle da replicare e i comandi da lanciare sul database. Tutte le dll fanno parte del Framework .NET versione 3.5 e quindi non richiedono installazioni separate. Questa tecnologia non è utilizzabile con il .NET Compact Framework e quindi esclude di fatto l’utilizzo su device basati su Windows CE (e ovviamente Windows Mobile visto che il sistema operativo è sempre Windows CE).
Per facilitare questo compito allo sviluppatore Visual Studio 2008 espone un nuovo item da inserire in progetti desktop per automatizzare la crezione di queste classi: il nuovo item prende il nome di Local Database Cache ed è utilizzabile in qualunque progetto desktop.
Procediamo con il wizard di Visual Studio 2008 per capire le tipologie di replica possibili, gli impatti sul server e il codice generato lato client.
Aggiungiamo un nuovo item al progetto:
Una volta aggiunto il file .sync è possibile aprirlo con un nuovo editor che facilita la configurazione e la creazione delle classi derivate che abbiamo accennato. La prima parte del designer consente di specificare la stringa di connessione verso il database server e verso il database client SQLCE.
Una volta scelte le connessioni viene creato, se non esistente, il file .SDF di SQLCE all’interno del progetto. Nella parte advanced è possibile scegliere in quali progetti della solution corrente dovranno essere create le classi per la sincronizzazione e se tutte le tabella devono essere sincronizzate in un unica transazione. Queste impostazioni si riflettono sulle proprietà delle classi che Visual Studio crea nei progetti indicati: sono quindi impostazioni modificabili in seguito riaprendo il file .sync oppure modificandoli da codice.
Per ogni tabella (nel nostro caso abbiamo solo la tabella tabFeedback) occorre indicareil tipo di sincronizzazione e se presenti i campi che consentono di individuare le modifiche da scaricare sul client ad ogni replica. Il tutto si effettua semplicemente scegliendo la tabella dal designer:
La prima operazione da eseguire è scegliere la modalità di replica: nella sezione client configuration si puà scegliere se creare una nuova tabella o utilizzare una tabella esistente nel database SQLCE per accogliere i dati che arrivano dal server. Nella prima combo-box invece si indica il tipo di replica da effettuare. Le opzioni sono le seguenti
· Snapshot: ogni operazioni di sincronizzazione sulla tabella scarica l’intero set dei dati
· Download-only: la tabella viene solamente sincronizzata dal server verso il client; vengono scaricate solo le modifiche effettuate dall’utlima sincronizzazione
· Upload-only: la tabella viene solamente sincronizzata dal client verso il server; vengono inviate solo le modifiche effettuate dall’utlima sincronizzazione
· Bidirectional: viene effettuato prima l’upload incrementale e poi il download delle modifiche server.
Nella sezione Server Configuration è fondamentale indicare i campi della tabella sul database server da utilizzare per individuare gli insert e le modifiche: le query generate infatti si basano su una clausola WHERE per individuare solo gli aggiornamenti effettuati dopo l’ultima sincronizzazione da parte del client: le ancore agli ultimi valori sono supportati nativamente da SQL CE fin dalla versione 1.0 come vedremo più avanti.
Sempre nella sezione Server Configuration è importante indicare la tabella in cui verranno accolte le cancellazioni effettuate server side e il campo da utilizzare per individuare queste eliminazioni di record.
Dopo aver premuto OK il database server si presenta così:
Nella tabella tabFeedbacks sono stati aggiunti due campi LastEditDate e CreationDate (indicati nel designer nella sezione Server Configuration) per memorizzare la data di modifica e di creazione dei recordo: tali campi vengono valorizzati dai due trigger omonimi come si può vedere dalla figura sopra. Viene creata, sempre secondo i valori indicati nel designer, la tabella _Tombstone, alimentata anch’essa dal trigger DeletionTrigger a fronte di ogni cancellazione effettuata.
Sui campi citati vengono montati degli indici per rendere efficiente la ricerca delle modifiche effettuate quando viene avviata la fase di sincronizzazione.
Il database lato client, creato e sincronizzato subito al termine del wizard, si presenta così:
Oltre ai campi sincronizzati troviamo alcuni campi di sistema (iniziano con un doppio “_”) che consentono al motore di SQLCE di tenere traccia delle modifiche lato client. Da sempre l’engine di SQLCE utilizza campi di sistema per tenere traccia delle modifiche nativamente, visto che su SQL CE non è possibile creare dei trigger: non occorre in ogni caso modificare da codice tali valori.
Su l database SQLCE vengono create anche alcune tabelle:
· __sysOCSDeletedRows
· __sysOCSTrackedObjects
· __sysSyncArticles
· __sysTxCommitSequence
La prima tabella tiene le righe cancellate lato client, la seconda contiene l’elenco degli oggetti da tenere sotto controllo durante le normali operazioni sul DB, la terza tabella, ben conosciuta a chi ha usato la merge replication, tiene l’elenco delle tabella sotto replica, mentre l’ultima tiene traccia della sequenza delle operazioni transazionali andate a buon fine: sono tutte tabelle di sistema non visibili dall’interfaccia di amministrazione di SQL CE (per elencare le tabelle di sistema si può utilizzare select * from information_schema.tables, mentre per vederne il contenuto è sufficiente una semplice select).
Per effettuare una sincronizzazione è sufficiente utilizzare il codice seguente:
LocalDataCacheDevCon2006_FeedbackSyncAgent syncAgent = new LocalDataCacheDevCon2006_FeedbackSyncAgent();
Microsoft.Synchronization.Data.SyncStatistics syncStats = syncAgent.Synchronize();
La prima riga di codice crea l’oggetto syncAgent (la classe è stata generata in automatico dal designer di Visual Studio) e la seconda riga invoca il metodo Synchronize. Tale metodo restituisce un oggetto di tipo SyncStatistics da cui è possibile capire il numero delle modifiche inviate, il numero delle modifiche ricevute per ogni tabella sincronizzata.
E’ possibile modificare, come accennato all’inizio dell’articolo, il comportamento di replica agendo sulle proprietà della classe derivata da SyncAgent. Ad esempio prima di lanciare la replica è possibile impostare la direzione della replica anche da codice:
LocalDataCacheDevCon2006_FeedbackSyncAgent syncAgent = new LocalDataCacheDevCon2006_FeedbackSyncAgent();
// Decido la direzione
syncAgent.tabFeedbacks.SyncDirection =
Microsoft.Synchronization.Data.SyncDirection.Snapshot;
Microsoft.Synchronization.Data.SyncStatistics syncStats = syncAgent.Synchronize();
In questo caso, a prescindere dai parametri indicati nel designer di Visual Studio, stiamo forzando una sincronizzazione di tipo SnatpShot ovvero la ricrezione della tabella lato client e lo scarico di tutti i dati della tabella server.
Come abbiamo indicato precedentemente, Visual Studio 2008, crea anche le classi SyncAdapter server side; il compito di tali classi è specificare i comandi da lanciare sul DB per gestire gli inserimenti, le modifiche e il recupero (incrementale o meno) delle righe modificate. Ecco l’estratto della classe SyncAdapter relativo alla nostra tabella tabFeedbacks:
public partial class tabFeedbacksSyncAdapter : Microsoft.Synchronization.Data.Server.SyncAdapter {
private void InitializeCommands()
{
// tabFeedbacksSyncTableInsertCommand command.
this.InsertCommand = new System.Data.SqlClient.SqlCommand();
this.InsertCommand.CommandText = @" SET IDENTITY_INSERT
dbo.tabFeedbacks ON INSERT INTO dbo.tabFeedbacks ([idFeedback], [FeedbackGUID], [FeedbackTechnology], [FeedbackLevel], [FeedbackSlot], [FeedbackComment], [LastEditDate], [CreationDate]) VALUES (@idFeedback, @FeedbackGUID, @FeedbackTechnology, @FeedbackLevel, @FeedbackSlot, @FeedbackComment, @LastEditDate, @CreationDate” ;
// tabFeedbacksSyncTableDeleteCommand command.
this.DeleteCommand = new System.Data.SqlClient.SqlCommand();
this.DeleteCommand.CommandText = "DELETE FROM dbo.tabFeedbacks WHERE ([idFeedback] = @idFeedback) SET @sync_row_cou" +
"nt = @@rowcount";
// tabFeedbacksSyncTableUpdateCommand command.
this.UpdateCommand = new System.Data.SqlClient.SqlCommand();
this.UpdateCommand.CommandText = @"UPDATE dbo.tabFeedbacks SET [FeedbackGUID] = @FeedbackGUID, [FeedbackTechnology] = @FeedbackTechnology, [FeedbackLevel] = @FeedbackLevel, [FeedbackSlot] = @FeedbackSlot, [FeedbackComment] = @FeedbackComment, [LastEditDate] = @LastEditDate, [CreationDate] = @CreationDate WHERE ([idFeedback] = @idFeedback) SET @sync_row_count = @@rowcount";
// selectIncrementalInsertsCommand command.
this.SelectIncrementalInsertsCommand = new System.Data.SqlClient.SqlCommand();
this.SelectIncrementalInsertsCommand.CommandText = "SELECT * FROM [tabFeedbacks]";
this.SelectIncrementalInsertsCommand.CommandType = System.Data.CommandType.Text;
// tabFeedbacksSyncTableSelectConflictDeletedRowsCommand command.
this.SelectConflictDeletedRowsCommand = new System.Data.SqlClient.SqlCommand();
this.SelectConflictDeletedRowsCommand.CommandText = "SELECT [idFeedback], [DeletionDate] FROM [tabFeedbacks_Tombstone] WHERE ([idFeedback] = @idFeedback)";
// tabFeedbacksSyncTableSelectConflictUpdatedRowsCommand command.
this.SelectConflictUpdatedRowsCommand = new System.Data.SqlClient.SqlCommand();
this.SelectConflictUpdatedRowsCommand.CommandText = "SELECT [idFeedback], [FeedbackGUID], [FeedbackTechnology], [FeedbackLevel], [FeedbackSlot], [FeedbackComment], [LastEditDate], [CreationDate] FROM dbo.tabFeedbac" +
"ks WHERE ([idFeedback] = @idFeedback)";
// tabFeedbacksSyncTableSelectIncrementalInsertsCommand command.
this.SelectIncrementalInsertsCommand = new System.Data.SqlClient.SqlCommand();
this.SelectIncrementalInsertsCommand.CommandText = @"SELECT [idFeedback], [FeedbackGUID], [FeedbackTechnology], [FeedbackLevel], [FeedbackSlot], [FeedbackComment], [LastEditDate], [CreationDate] FROM dbo.tabFeedbacks WHERE ([CreationDate] > @sync_last_received_anchor AND [CreationDate] <= @sync_new_received_anchor)";
// tabFeedbacksSyncTableSelectIncrementalDeletesCommand command.
this.SelectIncrementalDeletesCommand = new System.Data.SqlClient.SqlCommand();
this.SelectIncrementalDeletesCommand.CommandText = "SELECT [idFeedback], [DeletionDate] FROM [tabFeedbacks_Tombstone] WHERE (@sync_initialized = 1 AND [DeletionDate] > @sync_last_received_anchor AND [DeletionDate]" +
" <= @sync_new_received_anchor)";
// tabFeedbacksSyncTableSelectIncrementalUpdatesCommand command.
this.SelectIncrementalUpdatesCommand = new System.Data.SqlClient.SqlCommand();
this.SelectIncrementalUpdatesCommand.CommandText = @"SELECT [idFeedback], [FeedbackGUID], [FeedbackTechnology], [FeedbackLevel], [FeedbackSlot], [FeedbackComment], [LastEditDate], [CreationDate] FROM dbo.tabFeedbacks WHERE ([LastEditDate] > @sync_last_received_anchor AND [LastEditDate] <= @sync_new_received_anchor AND [CreationDate] <= @sync_last_received_anchor)";
}
}
Formattazione complicata per un articolo a parte, i nomi dei comandi sono chiari e non necessita di spiegazione: vengono creati i comandi per esegure le INSERT, gli UPDATE, i DELETE rispetto alle modifiche inviate dai client e i tre comandi per rintracciare, a fronte di una operazioni di sincronizzazione, le modifiche effettuate sul server da inviare al client in modo incrementale.
In base al tipo di sincronizzazione scelto nel designer di Visual Studio vengono generati solo i comandi necessari: ad esempio, se la sincronizzazione è di tipo upload-only non vengono generati i comandi per rintracciare le modifiche effettuate al server, così come se la sincronizzazione è di tipo download-only non vengono generati i comandi per applicare le modifiche al server.
Vista la natura dei comandi generati (non sono stored procedure, le modifiche vengono rintracciate per data di modifica, i dati non vengono partizionati a priori) ADO.NET Sync Service non si adatta a scenari con centinaia di migliaia di record oppure a scenari in cui le modifiche vengono partizionate per zone o per client (territorialità dei dati) che vengono invece brillantemente risolte da tecnologie più corpose e complete come la Merge Replication. Il messaggio che mi preme far passare è che ADO.NET Sync Service è il primo passo di una lunga strada e quindi si adatta benissimo a scenari semplici in cui lo sviluppatore vuole avere il controllo completo e dove non è possibile utilizzare la Merge Replication. Per scenari più “corposi” in cui occorre partizionare molte tabelle con molti record in base al client occorrono tecniche più efficienti di utilizzo dei dati (non si può lanciare una query su 100.000 record per estrarre solo le modifiche destinate ad un agente di vendita ma occorre partizionarle a priori, gestendo anche l’eventuale, ma frequente cambio di territorialità dei dati), tecniche che la Merge Replication espone in modo completo.
Per questo primo articolo introduttivo è tutto, ci risentiamo presto con un articolo di approfondimento sulle classe generate e sulle tecniche per raggruppare i dati, gestire dati per singolo client e passare parametri al motore di replica.
Roberto Brunetti
MCP, MCSD.NET, MCSE + I, MCT
roberto@devleap.it
http://blogs.devleap.com/rob
www.thinkmobile.it