Microsoft Sync Framework je kolekce tříd a logiky, která nám umožní velmi jednoduše zajišťovat synchronizaci různých obsahů. Ať již pouze dva body, či do hvězdy nebo každý s každým. V minulém díle jsme si ukázali jednoduchou synchronizaci dvou adresářů. V dnešním pokračování se podíváme na databáze. Synchronizace databází je velmi často diskutované témata a patří ke snad nejčastějším věcem, které se synchronizují.
Pravděpodobně každý dokáže vymyslet důvody, proč se databáze synchronizují. Může se jednat o prosté zajištění proti výpadku, možnost pracovat offline či různé možnosti v odlehčení HW. Topologií, které se provozují, najdeme také nespočet. Velmi častý je takovýto požadavek. Existuje jedna „centrální“ databáze a k ní se připojují klienti, z nichž někteří nemohou být stále připojeni. Klasickou ukázkou je pracovník na cestách. Všechna data (nebo alespoň některá) potřebuje mít u sebe, provádět v nich změny a po návratu zpět do kanceláře je synchronizovat zpět s centrální databází.
Není těžké domyslet, že většina synchronizací bude znamenat změny v struktuře tabulek a že ne vše půjde lehce, především kvůli velké rozmanitosti oblastí, které databáze obsluhují. Jedinou výjimkou je případ, kdy chceme na klienta stáhnout všechna data bez ohledu na předchozí synchronizace. Potom není žádná změna nutná. Nutné změny shrnuje níže uvedená tabulka.
| |
PK |
Update stamp |
Insert stamp |
Delete stamp |
ID klienta (updaty) |
ID klienta (inserty) |
ID klienta (mazání) |
| Stažení snapshotu dat na klienta |
Ne |
Ne |
Ne |
Ne |
Ne |
Ne |
Ne |
| Stažení inkrementálních insertů a updatů na klienta |
Ano |
Ano |
Ano1 |
Ne |
Ne |
Ne |
Ne |
| Stažení inkrementálních insertů, updatů a mazání na klienta |
Ano |
Ano |
Ano1 |
Ano |
Ne |
Ne |
Ne |
| Nahrání insertů na server |
Ano |
Ne |
Ne |
Ne |
Ne |
Ne2 |
Ne |
| Nahrání insertů a updatů na server |
Ano |
Ne |
Ne |
Ne |
Ne2 |
Ne2 |
Ne |
| Nahrání insertů, updatů, a mazání na server |
Ano |
Ne |
Ne |
Ne |
Ne2 |
Ne2 |
Ne |
| Obousměrné inserty a updaty s detekcí konfliktů |
Ano |
Ano |
Ano1 |
Ne |
Ano3 |
Ano3 |
Ne |
| Obousměrné inserty, updaty a mazání s detekcí konfliktů |
Ano |
Ano |
Ano1 |
Ano |
Ano3 |
Ano3 |
Ano3 |
1 Vyžadováno pokud potřebujeme rozlišit inserty a updaty.
2 Vyžadováno pokud více klientů může změnit řádek a chceme tyto změny rozlišit.
3 Vyžadováno pokud nechceme změny propagovat zpět na klienta, který je provedl.
4 Primární klíč musí být unikátní. Smazané hodnoty se nesmí znovu používat (vhodné používat autoincrement, GUID atp.).
Vidíme, že změna není nějak drastická, nicméně ani triviální. Pokud máme aplikaci správně napsánu, neměl by to být příliš velký zásah (ne větší než vlastní řešení).
My se rovnou vrhneme na případ, kdy chceme synchronizovat obousměrně a synchronizovat vše (změny, mazání, přidávání včetně konfliktů). Jak jsem na začátku naznačil, synchronizace databáze je velmi obtížná oblast a proto Microsoft Sync Framework nemusí vždy být nápomocen hned a bude potřeba hledat nebo napsat vlastní řešení/providery.
Pro náš první příklad budeme mít tabulku nesoucí název produkty a mající sloupce #id, nazev, cena. Tabulka je uložena na „velkém“ SQL Server. Synchronizovat budeme na SQL Server Compact Edition. Scénář tedy odpovídá stavu, kdy na cestách má pracovník data např. na PDA. Tabulku produkty rozšíříme o sloupce update_orig_id [int], update_stamp [datetime], insert_orig_id [int], insert_stamp [datetime]. Pokud si vzpomínáte, v prvním dílu jsem mluvil o tzv. náhrobcích – tombstones, které slouzí k udržování informací o smazaných položkách. Přidáme proto tabulku produkty_tombstones se sloupci id, nazev, cena a delete_orig_id [int], delete_stamp [datetime]. Obecně by stačil pouze primární klíč id a delete_orig_id, delete_stamp, ale přidáme si i další sloupce, které nám pomohou později při řešení konfliktů.
Zbývá ještě přidat spouště, které nám dodají trochu funkcí. Pro update je třeba nastavit update_stamp (např. na aktuální datum) a update_orig_id (např. na 0, kterým si označíme „centrální“ server). Pro delete stačí pouze překopírovat sloupce id, nazev, cena a podobně nastavit delete_stamp a delete_orig_id. V podstatě tedy poznamenání dané položky do tabulky smazaných řádků. Zde je mimo jiné vidět, proč nemohou být hodnoty primárního klíče tabulky produkty znovu používány – musíme mít (zpětnou) identifikaci smazaných záznamů. Celý skript SQL najdete na konci článku.
Co se týká změn v databázi, máme hotovo. Zbývá nastartovat Microsoft Sync Framework a využít jeho potenciálu. Začneme s jednodušší aplikací. Nejprve si popíšeme, jaké objekty budeme používat.
- Základem je SyncAgent, kterýžto zajišťuje celou synchronizaci zavoláním metody Synchronize().
- Další objekt, který použijeme je tzv. ServerSyncProvider pro nás konkrétně DbServerSyncProvider. DbServerSyncProvider je dodáván společně s ADO.NET Synchronization Services a umožňuje připojit se k libovolné databázi, která podporuje ADO.NET interfacy. DbServerSyncProvider má dvě zajímavé property: SelectNewAnchorCommand a SelectClientIdCommand. Druhá jmenovaná je volitelná a mapuje GUID klienta, kterým jsou implicitně označeni v MSF klienti, na běžný integer, s nímž se přece jen lépe pracuje. První property nám určuje horní hranici pro synchronizaci. Tato hranice se poté, pro další synchronizaci, stává hranicí dolní, takže nejsou zbytečně přenášena již synchronizovaná data.
- Na druhé straně barikády se nachází ClientSyncProvider pro nás konkrétně SqlCeClientSyncProvider. Tento objekt představuje úložiště lokální straně. My použijeme SQL Server CE, avšak je možné napsat si vlastní provider pro oblíbené úložiště (nemusí se nutně jednat ani o DB).
- Předposledním (prozatím) zajímavým objektem je SyncAdapter. Jedná se o objekt, který obsahuje osm příkazů, které „řídí“ synchronizovaná data – vybírají nová, ukazují smazaná, určují updaty atp. Nemusíte se bát, že všechny příkazy budete muset vymýšlet. Za prvé, ne všechny jsou vždy třeba (podle typu synchronizace). A za druhé existuje magická věc SqlSyncAdapterBuilder. Tento objekt nám pomůže sestavit SyncAdapter na základě několika předaných informací. Je však třeba upozornit, že právě kvůli zjišťování metadat má jistou režii a proto bych doporučil pro finální nasazení např. vygenerované příkazy zanést přímo do kódu. Mimo jiné má tento objekt také property SyncDirection, která ovlivňuje, jaké příkazy budou vygenerovány (jakou synchronizaci budeme provádět).
- A konečně poslední objekt, který budeme potřebovat, je SyncTable. SyncTable není nic jiného, než určení jakou tabulku chceme synchronizovat. Umožňuje nám nastavit, jak chceme synchronizovat (např. obousměrně, download, upload, snapshot). Nemůžeme však nastavit „vyšší“ akci, než jaké máme příkazy v SyncAdapteru, případně jaké necháváme generovat pomocí SqlSyncAdapterBuilderu. SyncTable také umožňuje definovat, jak se má kód chovat, v případě, že tabulka (ne)existuje.
Máme vše připraveno; hurá na věc:
1: SyncAgent agent = new SyncAgent();
2:
3: #region Pripojeni ke zdroji (SQL Server)
4: DbServerSyncProvider serverSyncProvider = new DbServerSyncProvider();
5: agent.RemoteProvider = serverSyncProvider;
6: SqlConnection conn = new SqlConnection(_dbSQL);
7: serverSyncProvider.Connection = conn;
8:
9: // nastaveni horni hranice
10: SqlCommand anchorCmd = new SqlCommand();
11: anchorCmd.CommandType = CommandType.Text;
12: anchorCmd.CommandText = "select @" + SyncSession.SyncNewReceivedAnchor + " = GETUTCDATE()";
13: anchorCmd.Parameters.Add("@" + SyncSession.SyncNewReceivedAnchor, SqlDbType.DateTime).Direction = ParameterDirection.Output;
14: serverSyncProvider.SelectNewAnchorCommand = anchorCmd;
15: anchorCmd.Connection = conn;
16:
17: // nastaveni ID klienta
18: SqlCommand clientIdCmd = new SqlCommand();
19: clientIdCmd.CommandType = CommandType.Text;
20: clientIdCmd.CommandText = "select @" + SyncSession.SyncOriginatorId + " = 1";
21: clientIdCmd.Parameters.Add("@" + SyncSession.SyncOriginatorId, SqlDbType.Int).Direction = ParameterDirection.Output;
22: serverSyncProvider.SelectClientIdCommand = clientIdCmd;
23: #endregion
24:
25: #region Lokalni uloziste (SQL Server CE)
26: SqlCeClientSyncProvider clientSyncProvider = new SqlCeClientSyncProvider(_dbSQLCE);
27: agent.LocalProvider = clientSyncProvider;
28: #endregion
29:
30: #region Tabulky k synchronizaci
31: SyncTable tableProdukty = new SyncTable("produkty");
32: tableProdukty.CreationOption = TableCreationOption.DropExistingOrCreateNewTable;
33: tableProdukty.SyncDirection = SyncDirection.Bidirectional;
34: agent.Configuration.SyncTables.Add(tableProdukty);
35: #endregion
36:
37: # region SyncAdapterBuilder
38: SqlSyncAdapterBuilder syncAdapterBuilder = new SqlSyncAdapterBuilder();
39: syncAdapterBuilder.Connection = conn;
40: syncAdapterBuilder.SyncDirection = SyncDirection.Bidirectional;
41:
42: // zakladni tabulky a sloupce
43: syncAdapterBuilder.TableName = "produkty";
44: syncAdapterBuilder.DataColumns.Add("id");
45: syncAdapterBuilder.DataColumns.Add("nazev");
46: syncAdapterBuilder.DataColumns.Add("cena");
47: syncAdapterBuilder.TombstoneTableName = "produkty_tombstones";
48: syncAdapterBuilder.TombstoneDataColumns.Add("id");
49: syncAdapterBuilder.TombstoneDataColumns.Add("nazev");
50: syncAdapterBuilder.TombstoneDataColumns.Add("cena");
51:
52: // sloupce pro synchronizaci
53: syncAdapterBuilder.CreationOriginatorIdColumn = "insert_orig_id";
54: syncAdapterBuilder.CreationTrackingColumn = "insert_stamp";
55: syncAdapterBuilder.DeletionOriginatorIdColumn = "delete_orig_id";
56: syncAdapterBuilder.DeletionTrackingColumn = "delete_stamp";
57: syncAdapterBuilder.UpdateOriginatorIdColumn = "update_orig_id";
58: syncAdapterBuilder.UpdateTrackingColumn = "update_stamp";
59:
60: // pridat
61: SyncAdapter produktySyncAdapter = syncAdapterBuilder.ToSyncAdapter();
62: serverSyncProvider.SyncAdapters.Add(produktySyncAdapter);
63: #endregion
64:
65: agent.Synchronize();
|
Pokud si tento kód spustíte, uvidíte, že všechny změny se přenesly na jednu i druhou stranu (pokud nevznikl konflikt). Pokud jste pozorně přečetly předcházející popisy objektů, mělo by výt vše jasno a konkrétní detaily dokreslí kód. Případně můžete využít diskuzi pod článkem.
Ačkoli přípravy byly poněkud obsáhlejší, při dalším přidávání by již měla jít práce více od ruky.
Pozorný čtenář si jistě všiml, že (zatím) např. neřešíme konflikty, více klientů nebo synchronizaci tabulek provázaných referenční integritou. Tyto a další případy si probereme v dalším dílu, kde náš kód vylepšíme.
1: create table produkty (id int not null primary key, nazev nvarchar(100) not null, cena int not null);
2:
3: alter table produkty add update_orig_id int default 0;
4: alter table produkty add update_stamp datetime default GetUTCDate();
5: alter table produkty add insert_orig_id int default 0;
6: alter table produkty add insert_stamp datetime default GetUTCDate();
7:
8: create table produkty_tombstones (id int not null primary key, nazev nvarchar(100) not null, cena int not null, delete_orig_id int, delete_stamp datetime);
9:
10: create trigger produkty_U on produkty for update
11: as
12: begin
13: if not update(update_orig_id)
14: update produkty set update_orig_id = 0 where id in (select id from inserted);
15: if not update(update_stamp)
16: update produkty set update_stamp = GetUTCDate() where id in (select id from inserted);
17: end
18: ;
19:
20: create trigger produkty_D on produkty for delete
21: as
22: begin
23: insert into produkty_tombstones (id, nazev, cena, delete_orig_id, delete_stamp)
24: select id, nazev, cena, 0, GetUTCDate() from deleted;
25: end
26: ;
27:
28: insert into produkty (id, nazev, cena) values (1, 'televize', 100);
29: insert into produkty (id, nazev, cena) values (2, 'krabice', 200);
|