Bewise

Nous développons... votre avance

Chapitre IV : Sync. Services for ADO.NET : Custom Resolver

SLF
DGD
16/10/2008 - Sébastien Pertus
Télécharger les sources

Introduction

Dans toute synchronisation, il existe un élément à prendre en compte le plus rapidement possible : les conflits.

Ceux-ci sont la fondation même de toute synchronisation, qui se doit de fusionner deux sources de données, pour arriver à un référentiel identique à un instant T.

Que se passe-t-il si ces deux référentiels, appelons les «  le client » et « le serveur », évoluent en même temps. Qui gagne et qui perd lors de la prochaine synchronisation ?

Sync Services for ADO.Net nous fournit l’ensemble des évènements et objets nécessaires à une résolution des conflits efficace, que ce soit du côté serveur comme du côté client.

Cet article fait suite aux précédents, que je vous conseille de lire avant d’entamer celui-ci :

· Chapitre I : Introduction à Sync Services For Ado.Net

· Chapitre II : Synchronisation manuelle, personnalisation

· Chapitre III : Sync Sercices For Ado.Net et WCF

Le Projet

Présentation

Nous allons reprendre la suite de l'article précédent, où nous terminions une synchronisation avec WCF. Enfin presque…

A l'époque la synchronisation s'effectue sur une base de données SQL Server 2005 où nous utilisions Triggers et autre table tombeau.

Aujourd'hui l'avènement de SQL Server 2008 et la mise en place du Service Pack 1 de Visual Studio 2008 nous permettent d'utiliser le Change Tracking SQL SERVER 2008 tout en gardant l'utilisation du Designer Sync de Visual studio 2008, celui-ci ayant été mis à jour pour l'occasion :

 ChangeTracking

Architecture de notre solution

La solution s'articule autour de 3 projets:

1. Un Client : L'application dans les mains de notre utilisateur final, permettant de modifier, visualiser et supprimer des données à synchroniser, et présentes dans une base de données locale (sous SQL Server Compact Edition)

2. Un Service WCF serveur : Ce service se trouve potentiellement sur le serveur, que ce soit sur un réseau local, ou over http. Vive WCF.

3. Un projet interface : Ce projet regroupe l’ensemble des interfaces accessibles via le client et le serveur. Seule petite modification apportée après la génération du code du SyncDesigner de VS. Cette partie concerne principalement l’architecture WCF.

 Architecture

Vous trouverez en plus un projet ServerSpy, uniquement destiné à modifier les données coté serveur, et permettant ainsi de générer (facilement) des conflits.

Synchronisation type

Les deux rôles principaux lors d’une synchronisation sont répartis entre le fournisseur de synchronisation coté client d’un coté et le fournisseur de synchronisation coté serveur de l’autre coté.

  • ClientSyncProvider : Dans notre cas, un SqlCeClientSyncProvider
  • ServerSyncProvider : Dans notre cas, un DbServerSyncProvider

Chacun de ces fournisseurs de synchronisation a en charge deux principales fonctionnalités :

  • Récupérer les derniers changements à l’instant T depuis la dernière synchronisation.
  • Appliquer des changements survenus à l’instant T depuis la dernière synchronisation.

Ces deux fournisseurs étant gérés et ordonnancés dans le bon ordre par notre SyncAgent.

Les principales méthodes nous concernant sont ApplyChanges et GetChanges, que nous retrouvons dans la définition de chacune des classes abstraites de ces fournisseurs :

Définition d’un fournisseur client :

public abstract class ClientSyncProvider : SyncProvider { protected ClientSyncProvider(); public abstract SyncContext ApplyChanges( SyncGroupMetadata groupMetadata, DataSet dataSet, SyncSession syncSession); public abstract SyncContext GetChanges( SyncGroupMetadata groupMetadata, SyncSession syncSession); public abstract Guid ClientId { get; set; } public abstract void BeginTransaction(SyncSession syncSession); public abstract void CreateSchema(SyncTable syncTable, SyncSchema syncSchema); public abstract void EndTransaction(bool commit, SyncSession syncSession); public abstract SyncAnchor GetTableReceivedAnchor(string tableName); public abstract SyncAnchor GetTableSentAnchor(string tableName); public abstract void SetTableReceivedAnchor(string tableName, SyncAnchor anchor); public abstract void SetTableSentAnchor(string tableName, SyncAnchor anchor); }

Définition d’un fournisseur serveur :

public abstract class ServerSyncProvider : SyncProvider { protected ServerSyncProvider(); public abstract SyncContext ApplyChanges( SyncGroupMetadata groupMetadata, DataSet dataSet, SyncSession syncSession); public abstract SyncContext GetChanges( SyncGroupMetadata groupMetadata, SyncSession syncSession); public abstract SyncSchema GetSchema(Collection<string> tableNames, SyncSession syncSession); public virtual SyncServerInfo GetServerInfo(SyncSession syncSession); }

Lors d’une synchronisation, notre orchestreur de synchronisation, le SyncAgent, va appeler successivement chacune de ces méthodes, dans un ordre bien précis :

  • GetChanges : Récupération des changements coté client.
  • ApplyChanges : Depuis le serveur, application des changements provenant du client.
  • GetChanges : Récupération des changements coté serveur.
  • ApplyChanges : Depuis le client, application des changements provenant du serveur.

DeroulementSynchro

Nous pouvons énumérer les arguments de chacune de ces méthodes :

  • SyncGroupMetadata : Regroupe l’ensemble des tables à synchroniser, suivant la SyncDirection que vous avez spécifiée, ainsi que diverses informations comme les ancres de chacune des tables.
  • DataSet : Le DataSet contenant les modifications, suppression, insertions à appliquer.
  • SyncSession : Contient les paramètres supplémentaires, l’identifiant de l’appelant, l’identifiant de la session en cours.

Les conflits surviendront lors des phases d’application des changements (ApplyChanges) que ce soit coté serveur comme coté client.

Nous allons voir dans la suite de l’article, que la plupart des cas de résolution s’effectueront coté serveur.

Le conflit dans une synchronisation

Contrairement à ce que l'on pourrait croire, les conflits ne sont pas gérés par le SyncAgent de notre synchronisation. Celui-ci étant l'orchestreur de la synchronisation, c'est le premier vers qui nous aurions tendance à nous tourner.

Mais notre SyncAgent est présent uniquement sur le client et ne pourrait donc pas agir coté serveur.

Les conflits sont directement gérés par les SyncProviders coté client et coté serveur.

Type de conflit

Avant d’aller plus loin, il est de bon ton de présenter les types de conflits que nous pouvons rencontrer.

Il existe une énumération, qui reprend l’ensemble des conflits possibles, entre un client et un serveur : ConflictType

public enum ConflictType { Unknown = 0, ErrorsOccurred = 1, ClientUpdateServerUpdate = 2, ClientUpdateServerDelete = 3, ClientDeleteServerUpdate = 4, ClientInsertServerInsert = 5, }

ConflictType : Cette énumération décrit le type de conflit survenu:

  • ClientUpdateServerUpdate : Le type de conflit le plus commun et répandu : Un client met à jour un enregistrement en local, et le serveur a lui-même mis à jour cet enregistrement.
  • ClientUpdateServerDelete : Un client met à jour un enregistrement, alors que celui-ci a été supprimé coté serveur.
  • ClientDeleteServerUpdate : Un enregistrement client a été supprimé alors que celui-ci a été mis à jour coté serveur
  • ClientInsertServerInsert : Surement un conflit très rare, si nous utilisons des GUID pour générer nos clés primaires : Un enregistrement est à la fois inséré coté client et coté serveur.

Par expérience la plupart des conflits sont contenus dans les valeurs 2, 3 et 4.

SyncConflict

Un SyncConflict contient, outre le type de conflit ConflictType, l’ensemble des informations nécessaires à sa résolution.

Nous possédons le type de conflit, d'une part, mais nous avons aussi la possibilité de connaître l'étape de synchronisation lorsque ce conflit survient, grâce à l'énumération SyncStage

  • SyncStage : Enumération décrivant les différentes étapes de synchronisation:
  • ReadingSchema : Lecture du schéma serveur
  • CreatingSchema : Création du schéma coté client (souvent lors de la première synchronisation)
  • ReadingMetadata : Lecture des méta données: Quelles tables à synchroniser, quelles tables à uploader, quelles tables à downloader
  • CreatingMetadata : Ecriture des ces métas données.
  • DeletingMetadata : Suppression des ces métas données.
  • UploadingChanges : Envoi des données à synchroniser du client vers le serveur, ou du serveur vers le client
  • DownloadingChanges : Récupération des données à synchroniser provenant du serveur sur le client, ou provenant du client sur le serveur
  • ApplyingInserts : Application des insertions, coté client ou coté serveur
  • ApplyingUpdates : Application des mises à jour, coté client ou coté serveur
  • ApplyingDeletes : Application des suppressions, coté client ou coté serveur
  • GettingInserts : Phase de récupération des insertions, coté client ou coté serveur
  • GettingUpdates : Phase de récupération des mises à jour, coté client ou coté serveur
  • GettingDeletes : Phase de récupération des suppressions, coté client ou coté serveur

L'objet SyncConflict contient aussi deux DataTables, contenant l'ensemble des modifications, suppressions, insertion, et ce coté serveur comme coté client:

  • ServerChange : Ensemble des modifications en conflit coté Serveur.
  • ClientChange : Ensemble des modifications en conflit coté Client.

Exemple de conflit

Pour illustrer un conflit, nous générons volontairement un conflit de type Update – Update :

 ConflitUpdateUpdate

Nous avons mis à jour l’adresse d’une personne de type « client »

Lors de la synchronisation, Sync Services va lever un conflit, indiquant que chacun des fournisseurs a mis à jour un même enregistrement. Il ne reste plus qu’à savoir qui des deux a raison ou tort !

Si l’on reprend le schéma directeur des opérations menées lors d’une synchronisation, le premier fournisseur qui va lever le conflit sera notre fournisseur serveur, pendant la phase N°2 : ApplyChanges.

C’est ici que nous devrons résoudre le conflit :

 ConflitSurServeur

Mais avant de résoudre ce conflit, reste à définir la règle métier à appliquer. Cette règle métier vient de votre gestion, de votre cahier des charges et des règles métiers de votre application.

Bref, c’est à vous de choisir.

Résolution d’un conflit coté serveur

La solution de résolution la plus simple reste le choix totalitaire entre la version cliente ou la version serveur.

Dans la plupart des cas, le référentiel restant le serveur, on peut même dire que le choix le plus simple reste que le serveur a toujours raison lors de la levée d’un conflit, bien que Sync. Services nous permet d’adopter une attitude différente sur chacun des conflits possibles.

Lors d’un conflit, chacun des providers va lever un évènement lorsque celui-ci survient sur leur source de données respectives : ApplyChangedFailed.

Il vous donne la main par la suite pour apporter une solution.

Vous aurez alors la possibilité de gérer le ou les conflits survenus, avec entre autre la possibilité de :

  • Forcer le changement, et passer outre le conflit.
  • Retenter d' appliquer le changement (la ligne en conflit peut très bien évoluée entre temps, même si ce cas est très improbable).
  • Résoudre le conflit, par votre propre code et forcer le changement.
  • Ignorer la ligne en conflit.

Cette résolution est ce que l’on nomme l’action à mener. Il existe une énumération que nous aurons à manipuler pour donner l’action à réaliser :

public enum ApplyAction { Continue = 0, RetryApplyingRow = 1, RetryWithForceWrite = 2, }
  • L’action Continue stipule que le conflit est déjà résolu, sous entendu c’est la version du fournisseur en cours qui gagne.
  • L’action RetryApplyingRow tente de réappliquer la ligne en conflit, sait-on jamais ! Attention, on peut vite partir en boucle infini, si le conflit ne résout pas de « lui-même » (croyez moi sur parole, j’ai donné !)
  • L’action RetryWithForce écrase la version du fournisseur en cours, avec la version venant de l’autre fournisseur, sous entendu la version du fournisseur en cours perd.

Règles de résolution

Voici les règles de résolution que nous allons imposer sur notre projet :

1) Lors de la suppression d’un enregistrement, si celui-ci a été modifié de l’autre coté, la suppression l’emporte toujours.

Ce qui se traduit par :

  • ClientUpdateServerDelete : Le serveur l’emporte
  • ClientDeleteServerUpdate : Le client l’emporte

2) Lors d’une mise à jour d’un enregistrement des deux cotés, l’enregistrement qui prime sera celui provenant du serveur

Ce qui se traduit par :

  • ClientUpdateServerUpdate : Le serveur l’emporte

3) Si par hasard, par le plus grand des hasards, un enregistrement est inséré en double des deux cotés, , l’enregistrement qui prime sera celui provenant du serveur

Ce qui se traduit par :

  • ClientInsertServerInsert : Le serveur l’emporte

ApplyAction.Continue

Nous ouvrons, coté Serveur WCF, le code de notre objet sync.

Le designer rajoute un fichier code, nous permettant de ne pas toucher au code généré.

L’apport des méthodes partielles nous permet de plus d’itérer sur le OnInitialized de la classe.

ServeurSourceCode

partial void OnInitialized() { this.ApplyChangeFailed += ServerApplyChangeFailed; } /// <summary> /// Resolver de conflits /// Les règles de résolutions sont : /// * ClientDeleteServerUpdate : Le client l'emporte. La suppression coté client est maintenue. /// * ClientUpdateServerDelete : Le Serveur l'emporte. La mise à jour coté client est annulée. /// * ClientInsertServerInsert : Le Serveur l'emporte. L'insertion client est annulée. /// * ClientUpdateServerUpdate : Le Serveur l'emporte. La mise à jour coté client est annulée. /// </summary> void ServerApplyChangeFailed(object sender, ApplyChangeFailedEventArgs e) { if (e.TableMetadata.TableName == "Client") ResolveTableClientConflict(e); } private static void ResolveTableClientConflict(ApplyChangeFailedEventArgs e) { }

Notez que déjà nous différencions l’origine des conflits par table. Ce renseignement nous ait donné via la propriété TableMetadata de notre EventArgs de type ApplyChangeFailedEventArgs.

Maintenant, il nous faut, dans la méthode ResolveTableClientConflict, distinguer les types de conflits que nous pouvons rencontrer. Le conflit en cours, et son type, étant aussi une propriété de notre EventArgs :

private static void ResolveTableClientConflict(ApplyChangeFailedEventArgs e) { if (e.Conflict.ConflictType == ConflictType.ClientDeleteServerUpdate) { } if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerUpdate) { } if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerDelete) { } if (e.Conflict.ConflictType == ConflictType.ClientInsertServerInsert) { } }

Occupons nous du cas ClientUpdateServerUpdate. Nous avions établi que le serveur gagnait toujours dans ce cas là.

Nous nous trouvons coté serveur, l’action à mener sur ce conflit est donc ApplyAction.Continue :

Nous n’appliquons pas de modification à la ligne serveur.

// Ici le serveur l'emporte // Le serveur ayant raison, on applique pas la modification provenant du client // Nous continueons donc. // Les changements coté client seront écrasé en retour if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerUpdate) { e.Action = ApplyAction.Continue; }

Que va-t-il se passer lors du retour vers le client ?

Et bien une ligne coté serveur est bien en modification, et va être rapatriée coté client. Celui-ci (le fournisseur client) va mettre à jour la ligne comme n’importe quelle autre ligne qui aurait été modifiée coté serveur.

 EtapeResolveConflitServeur

Notre conflit est résolu !

Analysons maintenant la résolution du conflit si nous avions utilisé un autre type d’action:

ApplyAction.RetryWithForce

Dans ce cas de figure, la ligne coté client prime sur la ligne coté serveur. La ligne va donc être mise à jour sur le serveur, écrasant donc la version Serveur.

Celle-ci existant donc en bonne version sur le client, elle n’a pas d’intérêt à revenir coté client.

 EtapeResolveConflitServeur2

ApplyAction.RetryApplyingRow

Ce cas de figure est, à mon avis, à bannir. Le fournisseur serveur va tenter de réappliquer la ligne, sans toucher ou sans forcer sa mise à jour. Résultat : une boucle infinie.

 EtapeResolveConflitServeur3

Voici le code de résolution complète coté serveur, si nous suivons correctement les règles imposées par nos règles métiers :

private static void ResolveTableClientConflict(ApplyChangeFailedEventArgs e) { // // ClientUpdateServerUpdate // // Ici le serveur l'emporte // Les changements devront bien être appliqués coté client // Le serveur ayant raison, on applique pas la mise à jour, on continue donc. if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerUpdate) { e.Action = ApplyAction.Continue; } // // ClientDeleteServerUpdate // // Ici le client l'emporte // La suppression coté cliente l'emporte, les données modifiées coté serveur sont effacées // On force donc la suppression if (e.Conflict.ConflictType == ConflictType.ClientDeleteServerUpdate) { e.Action = ApplyAction.RetryWithForceWrite; return; } // // ClientUpdateServerDelete // // Ici le serveur l'emporte // Les changements devront bien être appliqués coté client // Le serveur ayant raison, on applique pas la mise à jour, on continue donc. if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerDelete) { e.Action = ApplyAction.Continue; return; } // // ClientInsertServerInsert // // Ici le serveur l'emporte // Les changements devront bien être appliqués coté client // Le serveur ayant raison, on ne garde que l'insertion coté serveur if (e.Conflict.ConflictType == ConflictType.ClientInsertServerInsert) { e.Action = ApplyAction.Continue; return; } }

Résolution d’un conflit coté client

Que nous reste-t-il coté client ?

Si l’on regarde bien les types de conflits et les types de résolution, on se rend compte que les conflits sont tous gérés coté serveur. Et c’est tant mieux. Il est plus que préférable que le serveur sache résoudre les conflits sans le client, un autre client pouvant lui aussi demander une synchronisation.

Mais alors, que doit-on faire côté Client ? Où plutôt doit on gérer un conflit coté client ?

Première constatation, il existe bel et bien l’ensemble des évènements de gestion de conflits coté client.

Alors à quoi servent ‘ils ?

La raison est à allée chercher du côté de l’équipe Sync. Services elle-même qui nous donne les raisons d’une telle mise en œuvre

« Une synchronisation coté client peut entrer en conflit si un enregistrement est modifié entre le moment où celui est envoyé au serveur et le moment où la réponse du serveur revient »

On peut schématiser cette génération de conflit de la façon suivante :

 EtapeResolveConflitClient

A vous de gérer les conflits à l’identique, coté client.

Dans la plupart des cas, les interfaces client ne permettent pas ce genre de conflits (synchronisation à la connexion, petite animation d’attente de fin de synchro etc…)

Le fournisseur client de synchronisation simplifie encore plus ce mécanisme en nous proposant uniquement les attitudes à adopter en amont d’un conflit, en supposant de la réaction du serveur. En effet, à priori, lors de l’envoi de ses données, le client ne sait pas s’il va y avoir conflit ou non.

Nous allons donc juste lui indiquer « l’attitude à adopter », via l’énumération ResolveAction :

partial void OnInitialized(){ this.Client.SyncDirection = SyncDirection.Bidirectional; BewiseSyncClientSyncProvider syncClientProvider = this.LocalProvider as BewiseSyncClientSyncProvider; // Cas où le client l'emporte : Suppression d'un enregistrement coté client syncClientProvider.ConflictResolver.ClientDeleteServerUpdateAction = ResolveAction.ClientWins; // Pour le reste, le serveur est le référent syncClientProvider.ConflictResolver.ClientUpdateServerDeleteAction = ResolveAction.ServerWins; syncClientProvider.ConflictResolver.ClientInsertServerInsertAction = ResolveAction.ServerWins; syncClientProvider.ConflictResolver.ClientUpdateServerUpdateAction = ResolveAction.ServerWins; syncClientProvider.ConflictResolver.StoreErrorAction = ResolveAction.ServerWins; syncClientProvider.ApplyChangeFailed += BewiseSyncSyncAgent_ApplyChangeFailed; }

Cas particulier : ClientDeleteServerUpdate

Il existe pourtant bien un cas particulier coté client qui nécessite de résoudre un conflit :

  • Suppression d’une ligne coté client
  • Mise à jour coté serveur, qui gagne le conflit coté serveur (ApplyAction.Continue)
  • Retour coté client d’une ligne supprimée à « ré insérer »

Pourtant si nous nous référons à ce qui a été énoncé précédemment, la ligne qui revient du serveur n’est rien d’autre qu’une ligne dont la date est supérieure à la dernière date de synchronisation et qui doit être mis à jour coté client. Une ligne parfaitement … normale !

Oui mais … la ligne qui revient coté serveur est une ligne marquée comme ligne à … mettre à jour, et celle-ci n’existe plus coté client !

Levé d’un conflit coté client !

Le client ne sait pas exécuter une requête de type Update sur une ligne qui n’existe plus !

Si on ne modifie par le code précédent, ce conflit peut même passer inaperçu, et s’auto gérer seul. Mais on peut aussi lever un évènement coté client et gérer le cas.

Note : Ce cas n’arrive pas dans notre exemple, puisque nous sommes partis du principe qu’une ligne supprimée coté client doit l’être aussi coté serveur, quand bien même celle-ci aurait été mise à jour coté serveur.

Résolution Complexe ou conditionnelle

Premier cas : Tout blanc, tout noir

Supposons que vous souhaitiez établir une règle de priorité, conditionnée par les données elle-même.

Vous devez établir qui a raison, du client ou du serveur, suivant les données en conflit.

Dans le cas Update-Update, que nous étudions, changeons la règle métier qui devient :

« L’adresse mise à jour est celle contenant le plus de caractères »

Dans ce cas là, la résolution devient « conditionnelle ».

Par « tout blanc, tout noir » nous sous-entendons, la résolution du conflit sera soit la version cliente, soit la version serveur. Par lui suite nous établirons le cas « mi-teinte » où les données seront fusionnées.

Mais revenons à notre cas de résolution conditionnelle, simple : Il va nous falloir jouer sur l’action à appliquer suivant les données en entrées.

Pour cela, à notre disposition :

  • La ligne en conflit provenant du client : e.Conflict.ClientChange.Rows[0]
  • La ligne en conflit déjà mise à jour sur le serveur : e.Conflict.ServerChange.Rows[0]
  • La ligne complète, résultat, qui doit (ou pas être mise à jour) :
  • DataRow drToChange = serveurChanges.Select(String.Format("ClientId = '{0}'", drClient["ClientId"].ToString()))[0];
  • L’action à effectuer : e.Action

Les étapes de résolution, et de prise de position consistent à comparer les deux enregistrements, décider qui des deux a la priorité, de reporter le résultat dans la ligne complète, et d’appliquer la bonne action : ApplyAction.RetryWithForce si nous décidons que le client a raison, ou ApplyAction.Continue, si nous décidons que c’est le serveur qui l’emporte.

La résolution, par code devient :

// // ClientUpdateServerUpdate // // Ici Résolution conditionnelle // Le retour ou non vers le client d'une ligne en conflit dépend d'une règle métier imposée if (e.Conflict.ConflictType == ConflictType.ClientUpdateServerUpdate) { DataRow drClient = e.Conflict.ClientChange.Rows[0]; DataRow drServeur = e.Conflict.ServerChange.Rows[0]; // Récupération du Prénom et du Nom à remplacer string adresseFromClient = drClient["Adresse"] as string; string adresseFromServeur = drServeur["Adresse"] as string; // Récuparation de la table qui va être mise à jour coté serveur DataTable serveurChanges = e.Context.DataSet.Tables[0]; // Puis la ligne DataRow drToChange = serveurChanges.Select( String.Format("ClientId = '{0}'", drClient["ClientId"].ToString()))[0]; // Mise à jour if (adresseFromClient.Length > adresseFromServeur.Length) { // Ici le client gagne, on force l'application coté serveur // Rien ne remontera coté client puisque l'enregistrement est déjà correct. drToChange["Adresse"] = adresseFromClient; e.Action = ApplyAction.RetryWithForceWrite; } else { // Dans ce cas, le serveur gagne, la ligne reviendra en état de modification coté client e.Action = ApplyAction.Continue; } return; }

Résolution conditionnelle terminée !

Deuxième cas : finalement, c’est mi-teinte !

Que se passe-t-il si notre règle métier devient : « Prenez la concaténation de la version cliente + la version serveur, en précisant d’où vient quoi »

Exemple sur notre adresse :

  • L’adresse renseignée depuis le client est : « 1 Avenue Charles 1er »
  • L’adresse renseignée coté serveur est « 2 Avenue Charles 1er »
  • Le résultat doit être « Fusion de champs : Client [1 Avenue Charles 1er] , Serveur [2 Avenue Charles 1er ] A VERIFIER »

Nous avons une problématique à résoudre sur l’action a mener, une fois la fusion de la ligne principale effectuée

  • Si nous appliquons l’action ApplyAction.RetryWithForce, la ligne sera bien mise à jour coté serveur, mais celle-ci n’étant pas renvoyée coté client, nous avons deux versions différentes de chaque coté.
  • Si nous appliquons ApplyAction.Continue, rien ne se passe et nous perdons notre fusion.

Aujourd’hui, il n’existe pas de solution miracle, intégrée à Sync. Services sur ce genre de conflit.

Nous allons devoir nous adapter, en adoptant une solution mixte :

  • Premièrement, l’action à appliquer sera bien ApplyAction.RetryWithForce. De ce fait, la ligne fusionnée est bien écrite en base de données coté serveur.
  • Deuxièmement, pour s’assurer que cette ligne sera renvoyée au client ; une fois l’ensemble des mises à jour effectuées, nous allons faire un update « à blanc » de la ligne pour faire évoluer le critère de dernière mise à jour (Timestamp ou Long) pour que la ligne soit vue comme une ligne à mettre à jour coté client.

Nous avons à notre disposition un évènement levé par le fournisseur de synchronisation coté serveur qui nous notifie lorsque l’ensemble des modifications ont été apportées.

/// <summary> /// Nous mettons à jour les lignes pour générer des lignes à renvoyer coté client /// </summary> private void ServerChangesApplied(object sender, ChangesAppliedEventArgs e) { }

De plus nous avons en argument, la connexion en cours ainsi que la transaction courante. Tout va bien, nous sommes dans un contexte transactionnel, notre modification fera entièrement parti d’un groupe de changement atomique.

Avant de remplir notre méthode, nous nous abonnons à l’évènement levé par le fournisseur de synchronisation, et nous créons une liste de guids contenant les guids à mettre à jour :

/// <summary> /// Stocke les id à mettre à jour (Exemple 2 Update-Update ) /// </summary> private List<Guid> lstClientIdToUpdate; partial void OnInitialized() { this.ApplyChangeFailed += ServerApplyChangeFailed; // Utile pour la résolution de conflit conditionnelle avec fusion des champs (Exemple 2 Update-Update ) this.ChangesApplied += ServerChangesApplied; this.lstClientIdToUpdate = new List<Guid>(); }

Nous résolvons le conflit dans la méthode ServerChangedFailed, sans oublier de bien remplir la liste de Guids :

DataRow drClient = e.Conflict.ClientChange.Rows[0]; DataRow drServeur = e.Conflict.ServerChange.Rows[0]; // Récupération du Prénom et du Nom à remplacer string adresseFromClient = drClient["Adresse"] as string; string adresseFromServeur = drServeur["Adresse"] as string; // Récuparation de la table qui va être mise à jour coté serveur DataTable serveurChanges = e.Context.DataSet.Tables[0]; // Puis la ligne DataRow drToChange = serveurChanges.Select( String.Format("ClientId = '{0}'", drClient["ClientId"].ToString()))[0]; String newAddress = string.Format("CONFLIT : Version client : {0} Version Serveur : {1} ", adresseFromClient, adresseFromServeur); drToChange["Adresse"] = newAddress; e.Action = ApplyAction.RetryWithForceWrite; // Ajout de cette ligne à la liste des lignes à mettre à jour this.lstClientIdToUpdate.Add((Guid)drClient["ClientId"]);

Enfin lors de la levée d’évènement de fin de mise à jour, nous effectuons notre mise à jour à blanc :

/// <summary> /// Nous mettons à jour les lignes pour générer des lignes à renvoyer coté client /// </summary> private void ServerChangesApplied(object sender, ChangesAppliedEventArgs e) { if (lstClientIdToUpdate.Count > 0) { foreach (Guid clientId in lstClientIdToUpdate) { SqlCommand updateTable = new SqlCommand(); updateTable.Connection = (SqlConnection)e.Connection; updateTable.Transaction = (SqlTransaction)e.Transaction; updateTable.CommandText = "UPDATE dbo.Client SET ClientId = @ClientId Where ClientId = @ClientId"; SqlParameter p = new SqlParameter("@ClientId", SqlDbType.UniqueIdentifier); p.Value = clientId; updateTable.Parameters.Add(p); updateTable.ExecuteNonQuery(); } } }

La ligne deviant une ligne de mise à jour à envoyer au client, elle est renvoyée sur le GetChanges serveur, notre conflit est résolu !

A noter :

Les deux exemples précédents sont remarquable sur un point particulier, qui me semble trés important :

Quelque soit la solution de conflit adopter, nous n’avons pas eu besoin de mettre à jour le client !

Vous êtes donc capable, sans faire évoluer votre outil client, de résoudre des conflits via par exemple un moteur de règle évolutif, par exemple.

Conclusion

Nous venons de résoudre la plupart des cas de conflits qui peuvent survenir lors d’une synchronisation grâce à Sync. Services et l’ensemble des services que celui-ci nous met à disposition.

N’oubliez pas que la résolution de conflit repose surtout sur la maitrise de vos règles métiers :

« Doit-on , ou non supprimer la fiche d’un client appartenant à un employé alors que celui-ci est en congés et se synchronise le lundi matin suivant alors que sa secrétaire a mis directement à jour le dit client en apportant des données nouvelles de son contrat le vendredi soir ? »

Bigre !

Enfin, après moult discussions, vous appliquerez l’action correcte, Continue ou RetryWithForce !

Dans le prochain article, nous attaquerons la synchronisation depuis un Device, un Desktop et un serveur. Wait & see !

Bonne Synchronisation !

> Tous les articles

Commentaires

aucun commentaire
Page 1/1
   
Connexion
  • Accueil
  • Plan du site
  • Contact
Bewise TV, Blog technique, Webcasts...

Votre carrière et nous

  • Nos offres
  • Votre candidature
Ignorer les liens de navigation > Accueil > Nos Métiers > Solutions Langages et Framework > Détail Article
Ignorer les liens de navigation
Nous
Nos Métiers
Vous Former
Nos Evènements
Nos Références
Nos Activités
Nos Certifications
Nos Chiffres
Le Groupe
Nos Partenaires
On Parle de Nous
Nous contacter
Votre Carrière et Nous
Défiler vers le haut
Défiler vers le bas
Administration, Système et Communication
Architecture, Méthodes, Industrialisation
Décisionnel et Gestion des Données
Nouvelles Interfaces Utilisateurs
Portail et Travail Collaboratif
Solutions Langages et Framework
Solutions Web Avancées
Défiler vers le haut
Défiler vers le bas
Nos cours
Le Planning
Offres promotionnelles
Défiler vers le haut
Défiler vers le bas
Bewise Day Conference 2011
Nos Expresso
Défiler vers le haut
Défiler vers le bas
  • Infos légales
  • Lettre du Regional Director
  • Revue de presse