V našem povídání o MSF jsme minule pokročili k databázím resp. části MSF, která nám umožňuje ve spolupráci s ADO.NET synchronizovat obsahy tabulek. Základní představu o tom, jak „to“ celé funguje, máme z minulého dílu, kde možná mohlo být informací k pochopení trochu více, a proto se dnešní díl ponese v trochu oddychovém vánočně-novoročním rytmu.
reklama
V našem povídání o MSF jsme minule pokročili k databázím resp. části MSF, která nám umožňuje ve spolupráci s ADO.NET synchronizovat obsahy tabulek. Základní představu o tom, jak „to“ celé funguje, máme z minulého dílu, kde možná mohlo být informací k pochopení trochu více, a proto se dnešní díl ponese v trochu oddychovém vánočně-novoročním rytmu.
První věc, kterou jsme, pro zjednodušení, opomenuli, byla správa více klientů. Pro náš případ jsme vždy přiřadili identifikátoru klienta číslo jedna. Samozřejmě můžeme používat implicitní, automaticky generovaný GUID, avšak práce s ním není tak pohodlná a zabírá více místa (16 bytes oproti 4 bytes u integer). DbServerSyncProvider proto nabízí property SelectClientIdCommand, kterou můžeme mapování upravit k obrazu svému. Aby se nám celé kolo pěkně točilo, přidáme si do databáze tabulku clients (id int identity primary key, client uniqueidentifier not null unique) (pojmenování nehraje roli), do které budeme ukládat spojení identifikace klienta a jeho čísla. Výkonnou část nám zajistí uložená procedura, která pokud najde číslo klienta, vrátí jej, v opačném případě pro klienta vytvoří nový záznam a vrátí vzniknuvší číslo. Výsledek může vypadat například takto:
create procedure GetClientID
(
@sync_client_id uniqueidentifier,
@sync_originator_id int out
)
as
begin
if not exists (select 1 from clients where client = @sync_client_id)
begin
insert into clients (client) values (@sync_client_id);
select @sync_originator_id = cast(Scope_Identity() as int);
end
else
begin
select @sync_originator_id = id from clients where client = @sync_client_id;
end
end
Názvy parametrů jsou pojmenovány stejně, jako používá MSF, pro jednodušší mapování. Nyní stačí lehce modifikovat kód z minulého dílu:
SqlCommand clientIdCmd = new SqlCommand();
clientIdCmd.CommandType = CommandType.StoredProcedure;
clientIdCmd.CommandText = "GetClientId";
clientIdCmd.Parameters.Add("@" + SyncSession.SyncClientId, SqlDbType.UniqueIdentifier).Direction = ParameterDirection.Input;
clientIdCmd.Parameters.Add("@" + SyncSession.SyncOriginatorId, SqlDbType.Int).Direction = ParameterDirection.Output;
serverSyncProvider.SelectClientIdCommand = clientIdCmd;
A o zbytek se postará „vnitřek“ MSF. Spustíte-li nyní synchronizaci např. do dvou různých souborů databáze SQL Server CE, uvidíte v tabulce clients (alespoň) dva záznamy. A data se samozřejmě během synchronizace „nepomíchají“.
Druhá věc, která nebyla minule ukázána a kterou bude třeba velmi často řešit je referenční integrita resp. cizí klíče. Snad v každé databázi najdete alespoň jednu vazbu (foreign key), nicméně v praxi jich mohou být tisíce. Aby bylo zajištěno, že změny přijdou ve správném pořadí, je třeba objektům SyncTable říci, že patří „k sobě“. Udělejme si proto triviální příklad s dvěma tabulkami master a detail. Jejich vytvoření včetně spouští by neměl být problém, nicméně pro ulehčení je možné najít na konci článku připravený skript. Kód obsluhující MSF zůstane téměř beze změny – přidáme pouze další tabulku a esenci v podobě objektu SyncGroup.
Objekt SyncGroup vlastně určuje, jaké tabulky jsou spojeny spolu a musí být zpracovány společně. Do kódu si proto přidáme (např. za definice SyncTable):
SyncGroup groupMasterDetail = new SyncGroup("master_detail");
tableMaster.SyncGroup = groupMasterDetail;
tableDetail.SyncGroup = groupMasterDetail;
Vytvořením nové „grupy“ a zařazení tabulky do ní (přesněji přiřazení grupy tabulce) dáváme najevo, že chceme dané tabulky zpracovat společně.
Aby byla celá změna lépe patrná, připravil jsem malou aplikaci, která ukazuje co se děje (provede synchronizaci; několik změn; opět synchronizaci) a do konfliktu přímo naběhne. Spustíme-li ji bez zapojení tabulek do SyncGroup vypadá výstup takto:
Client SyncProgres: GettingInserts on: detail
Client SyncProgres: GettingUpdates on: detail
Client SyncProgres: GettingDeletes on: detail
Client SyncProgres: GettingInserts on: master
Client SyncProgres: GettingUpdates on: master
Client SyncProgres: GettingDeletes on: master
Server SyncProgres: GettingInserts on: detail
Server SyncProgres: GettingUpdates on: detail
Server SyncProgres: GettingDeletes on: detail
Server SyncProgres: GettingInserts on: master
Server SyncProgres: GettingUpdates on: master
Server SyncProgres: GettingDeletes on: master
Client SyncProgres: GettingInserts on: detail
Client SyncProgres: GettingUpdates on: detail
Client SyncProgres: GettingDeletes on: detail
Server: ErrorsOccurred
Server SyncProgres: ApplyingInserts on: detail
Server: ErrorsOccurred
Server SyncProgres: ApplyingInserts on: detail
Client SyncProgres: GettingInserts on: master
Client SyncProgres: GettingUpdates on: master
Client SyncProgres: GettingDeletes on: master
Server SyncProgres: ApplyingInserts on: master
Server SyncProgres: GettingInserts on: detail
Server SyncProgres: GettingUpdates on: detail
Server SyncProgres: GettingDeletes on: detail
Client SyncProgres: ApplyingInserts on: detail
Client SyncProgres: ApplyingInserts on: detail
Server SyncProgres: GettingInserts on: master
Server SyncProgres: GettingUpdates on: master
Server SyncProgres: GettingDeletes on: master
Client SyncProgres: ApplyingInserts on: master
Vidíme, že se nejprve zpracuje daná akce na jedné tabulce a poté na další. V našem případě ještě „náhodou“ nešťastně nejprve detail tabulka a až poté master (obecně nedefinované pořadí). Zařadíme-li však do procesu výše uvedený objekt SyncGroup, výstup se rázem změní:
Client SyncProgres: GettingInserts on: detail
Client SyncProgres: GettingInserts on: master
Client SyncProgres: GettingUpdates on: detail
Client SyncProgres: GettingUpdates on: master
Client SyncProgres: GettingDeletes on: detail
Client SyncProgres: GettingDeletes on: master
Server SyncProgres: GettingInserts on: master
Server SyncProgres: GettingInserts on: detail
Server SyncProgres: GettingUpdates on: master
Server SyncProgres: GettingUpdates on: detail
Server SyncProgres: GettingDeletes on: master
Server SyncProgres: GettingDeletes on: detail
Client SyncProgres: GettingInserts on: detail
Client SyncProgres: GettingInserts on: master
Client SyncProgres: GettingUpdates on: detail
Client SyncProgres: GettingUpdates on: master
Client SyncProgres: GettingDeletes on: detail
Client SyncProgres: GettingDeletes on: master
Server SyncProgres: ApplyingInserts on: master
Server SyncProgres: ApplyingInserts on: detail
Server SyncProgres: ApplyingInserts on: detail
Server SyncProgres: GettingInserts on: master
Server SyncProgres: GettingInserts on: detail
Server SyncProgres: GettingUpdates on: master
Server SyncProgres: GettingUpdates on: detail
Server SyncProgres: GettingDeletes on: master
Server SyncProgres: GettingDeletes on: detail
Client SyncProgres: ApplyingInserts on: detail
Client SyncProgres: ApplyingInserts on: detail
Client SyncProgres: ApplyingInserts on: master
Akce nad jednotlivými tabulkami jsou nyní sdruženy, takže nevzniká problém s „opuštěnými“ záznamy.
Vidíme, MSF není ze hry venku ani v případě, že potřebujeme regulérně synchronizovat provázané tabulky. V příštím díle se podíváme na řešení konfliktů.
create table master (id int not null primary key, text nvarchar(100));
alter table master add update_orig_id int default 0;
alter table master add update_stamp datetime default GetUTCDate();
alter table master add insert_orig_id int default 0;
alter table master add insert_stamp datetime default GetUTCDate();
create table master_tombstones (id int not null primary key, text nvarchar(100),
delete_orig_id int, delete_stamp datetime)
create trigger master_U on master for update
as
begin
if not update(update_orig_id)
update master set update_orig_id = 0 where id in (select id from inserted);
if not update(update_stamp)
update master set update_stamp = GetUTCDate() where id in (select id from inserted);
end
create trigger master_D on master for delete
as
begin
insert into master_tombstones (id, text, delete_orig_id, delete_stamp)
select id, text, 0, GetUTCDate() from deleted;
end
create table detail (id int not null primary key, master_id int, text nvarchar(100))
alter table detail add update_orig_id int default 0;
alter table detail add update_stamp datetime default GetUTCDate();
alter table detail add insert_orig_id int default 0;
alter table detail add insert_stamp datetime default GetUTCDate();
alter table detail add constraint master_detail foreign key (master_id) references master(id) on update cascade on delete cascade;
create table detail_tombstones (id int not null primary key, master_id int, text nvarchar(100),
delete_orig_id int, delete_stamp datetime)
create trigger detail_U on detail for update
as
begin
if not update(update_orig_id)
update detail set update_orig_id = 0 where id in (select id from inserted);
if not update(update_stamp)
update detail set update_stamp = GetUTCDate() where id in (select id from inserted);
end
create trigger detail_D on detail for delete
as
begin
insert into detail_tombstones (id, master_id, text, delete_orig_id, delete_stamp)
select id, master_id, text, 0, GetUTCDate() from deleted;
end