Réplication SQL pour les projets mobiles : Gestion des conflits et résolveurs - PARTIE 2/2 (Rédaction d'un résolveur personnalisé en C#)
Introduction :
Dans la première partie de cet article, nous avons étudié les généralités de la gestion des conflits pour la réplication de fusion SQL. Nous allons voir ici un exemple de création d’un résolveur de conflits personnalisé en C# pour répondre à des besoins spécifiques.
La rédaction d’un résolveur en .NET est rendu possible grâce à une API Microsoft qui permet d'écrire une logique métier personnalisée pour gérer les différents événements qui se produisent au cours du processus de synchronisation de la réplication de fusion.
Cette API est la DLL “Microsoft.SqlServer.Replication.BusinessLogicSupport” qui se situe par défaut dans le répertoire “C:\Program Files\Microsoft SQL Server\100\COM”.
D’une manière globale, voici quelles sont les étapes de cette personnalisation :
-
On écrit et on génère une assembly .NET (DLL) qui implémente et “override” les méthodes nécessaires de la classe “BusinessLogicSupport”.
-
On inscrit la DLL créée sur le serveur de distribution.
-
On déploie la DLL créée sur le serveur où l’agent de fusion s’exécute.
-
On édite les propriétés d’un article pour choisir le résolveur de conflits que l’on vient de créer.
A titre d’exemple, nous allons imaginer que le système de réplication que nous avons implémenté met en jeu une table “CLIENT” dans laquelle nous stockons toutes les données relatives aux clients telles que les noms, prénoms, adresses, numéros de téléphone, etc…
Une application mobile, liée à une base mobile répliquée, permet de modifier l’adresse du client via un écran spécifique.
L’article “CLIENT” de notre solution de réplication peut donc être soumis à des éventuels conflits lors d’une synchronisation, si l’adresse d’un client particulier est modifiée à la fois sur le serveur et sur le terminal mobile synchronisant.
Nous allons également imaginer une règle simple à mettre en place en cas de conflit sur l’adresse d’un client :
“Si l’adresse du client modifiée par l'abonné est correctement formatée (numéro + rue + ville), on doit la garder! Sinon on doit systématiquement conserver celle modifiée par le publisher”.
Nous allons voir maintenant, étape par étape, comment nous devons procéder pour créer un résolveur de conflits qui applique cette règle !
Création d’une DLL de résolveur :
Pour commencer à écrire notre résolveur, nous allons partir d’un nouveau projet de type « Class library » dans Visual Studio. De cette manière, nous pourrons facilement compiler une DLL et non un fichier exécutable.
Avant toutes choses, il va être également nécessaire d’ajouter une référence à la DLL “Microsoft.SqlServer.Replication.BusinessLogicSupport”. Parcourez alors le système de fichier pour trouver cette DLL située par défaut dans le répertoire “C:\Program Files\Microsoft SQL Server\100\COM”.
Pour commencer, nous allons ajouter les “using” nécessaires :
using System.Data;
using Microsoft.SqlServer.Replication.BusinessLogicSupport;
Puis, dans un second temps, nous allons spécifier que notre classe “ResolverPersoCLIENT” hérite de la classe “BusinessLogicModule”. L’héritage de la classe “BusinessLogicModule” rend obligatoire l’implémentation de la propriété override “HandledChangeStates” puisque cette dernière est notée “virtuelle” dans la classe mère “BusinessLogicModule”.
Voici à quoi doit donc ressembler le squelette de départ de notre résolveur :
public class ResolverPersoCLIENT : BusinessLogicModule
{
public ResolverPersoCLIENT()
{
}
public override ChangeStates HandledChangeStates
{
}
}
En héritant de la classe “BusinessLogicModule”, il est possible de “overrider” de nombreuses méthodes afin d’implémenter une logique métier personnalisée sur les différents événements qui se produisent au cours du processus de synchronisation de la réplication de fusion.
L’ensemble des méthodes qu’il est possible de définir et de personnaliser sont les suivantes :
-
CommitHandler : Pour définir des traitements particuliers lors de la validation de données lors des synchros.
-
DeleteErrorHandler : Pour définir des traitements particuliers lorsqu’une erreur se produit suite à une instruction DELETE.
-
DeleteHandler : Pour définir des traitements particuliers lorsque des instructions DELETE sont exécutées par le processus de réplication.
-
InsertErrorHandler : Pour définir des traitements particuliers lorsqu’une erreur se produit suite à une instruction INSERT.
-
InsertHandler : Pour définir des traitements particuliers lorsque des instructions INSERT sont exécutées par le processus de réplication.
-
UpdateConflictsHandler : Pour définir des traitements particuliers lorsque des instructions UPDATE génèrent des conflits.
-
UpdateDeleteConflictHandler : Pour définir des traitements particuliers lorsque des instructions UPDATE entrent en conflit avec des instructions DELETE.
-
UpdateErrorHandler : Pour définir des traitements particuliers lorsqu’une erreur se produit suite à une instruction UPDATE.
-
UpdateHandler : Pour définir des traitements particuliers lorsque des instructions UPDATE sont exécutées par le processus de réplication.
De cette manière, nous sommes capables de réécrire complètement les logiques de traitements sur chacun des évènements qui apparaissent lors des phases de synchronisations ! Pour définir un traitement particulier, il suffit “d’overrider” la méthode liée à l’évènement concerné et de personnaliser le traitement en y implémentant son propre code.
Evidemment, nous ne sommes pas obligés de réécrire et de personnaliser l’entièreté de ces méthodes.
Dans notre exemple, ce qui nous intéresse est de proposer une règle particulière de résolution de conflits pour les conflits de type “update”.
C’est pourquoi, nous allons commencer par déclarer quels sont les types de modifications que nous allons gérer dans la propriété override “HandledChangeStates”. Il est obligatoire de spécifier ce que nous allons personnaliser et traiter pour pouvoir capturer correctement les évènements choisis lors des phases de synchronisations.
Nous allons donc spécifier que nous nous intéressons spécifiquement aux évènements de conflits de mises à jour à l’aide de l’énumération “ChangeStates” qui contient l’ensemble des types d’évènements :
public override ChangeStates HandledChangeStates
{
get
{
return ChangeStates.UpdateConflicts;
}
}
Comme nous avons choisi d’intercepter et de personnaliser les modifications de type “conflits de mises à jour”, nous devons à présent “overrider” la méthode “UpdateConflictsHandler” qui va nous permettre de spécifier les règles particulières à appliquer lorsque ce type de conflit est levé.
La méthode override “UpdateConflictsHandler” doit respecter une signature particulière. Les arguments de la méthode correspondent, entre autres, aux éléments importants ci-dessous :
-
Un “dataset” qui correspond aux données modifiées par le publisher
-
Un “dataset” qui correspond aux données modifiées par le subscriber (abonné)
-
Une référence sur un “dataset” personnalisé qui va permettre de personnaliser l’ensemble des données, suite à un conflit particulier, avant de les renvoyer.
public override ActionOnUpdateConflict UpdateConflictsHandler(DataSet publisherDataSet,
DataSet subscriberDataSet, ref DataSet customDataSet,
ref ConflictLogType conflictLogType, ref string customConflictMessage,
ref int historyLogLevel, ref string historyLogMessage)
{
// ON DEFINIT ICI LES TRAITEMENTS PERSONNALISES !
}
A présent, à l’intérieur de cette méthode, nous devons écrire la logique de traitement associée. Pour rappel, nous devons appliquer la règle suivante :
“Si l’adresse du client modifiée par l'abonné est correctement formatée (numéro + rue + ville), on doit la garder! Sinon on doit systématiquement conserver celle modifiée par le publisher”.
Le code ressemblera donc à ceci :
public override ActionOnUpdateConflict UpdateConflictsHandler(DataSet publisherDataSet,
DataSet subscriberDataSet, ref DataSet customDataSet,
ref ConflictLogType conflictLogType, ref string customConflictMessage,
ref int historyLogLevel, ref string historyLogMessage)
{
// On récupère l'adresse du client modifiée par l'abonné:
string adresseAbo = subscriberDataSet.Tables[0].Rows[0]["Adresse"].ToString();
// Si le format de l'adresse, modifié par l'abonné est correct, on la garde,
// sinon, on prend systématiquement celle modifiée par le publisher !
if (FormatCorrect(adresseAbo) == true)
{
// On copie les valeurs du dataset publisher dans un dataset personnalisable:
customDataSet = publisherDataSet.Copy();
// On prend en compte l'adresse modifiée par l'abonné:
customDataSet.Tables[0].Rows[0]["Adresse"] = adresseAbo;
// Renvoi d'un message personnalisé:
customConflictMessage = "Conflit résolu et traité comme il se doit !";
// On valide les modifications effectuées:
return ActionOnUpdateConflict.AcceptCustomConflictData;
}
else
{
// On valide toutes les modifications du publisher:
return ActionOnUpdateConflict.AcceptPublisherData;
}
}
Explications :
-
Dans un premier temps, nous récupérons l’adresse conflictuelle à partir du dataset de l’abonné.
-
Ensuite, nous vérifions si son format est correct.
-
Si le format de l’adresse est correct, on spécifie explicitement que l’on garde les modifications effectuées par le publisher.
-
Sinon, on copie les données modifiées du publisher dans le dataset personnalisé et on modifie juste la valeur du champ correspondant à l’adresse par la valeur récupérée de l’abonné. Enfin, on précise explicitement que l’on valide les données contenues dans le dataset personnalisé.
-
A noter que tous les conflits d'un article qui ne sont pas gérés explicitement par la nouvelle logique métier personnalisée seront gérés par le programme de résolution par défaut pour l'article.
Nous pouvons à présent compiler notre DLL et l’inscrire au niveau du serveur de distribution.
Inscription de la DLL créée sur le serveur de distribution :
La DLL correspondante au résolveur .NET personnalisé étant écrite et générée, nous pouvons à présent la référencer sur le serveur de distribution.
Cette opération peut être également effectuée en C# grâce à l’API “Microsoft.SqlServer.Replication.BusinessLogicSupport”, cependant il est plus facile d’exécuter une procédure stockée prévue à cet effet, directement sur la base de distribution.
L’exécution de la procédure ci-dessous, avec les bons paramètres, permet d’inscrire la DLL sur le serveur de distribution :
USE distribution
GO
EXEC sp_registercustomresolver
@article_resolver = 'Resolveur Perso pour larticle CLIENT',
@is_dotnet_assembly = 'true',
@dotnet_assembly_name =
'C:\DATA\Temp\ResolverPerso\ResolverPerso\bin\Debug\ResolverPerso.dll',
@dotnet_class_name = 'ResolverPerso.ResolverPersoCLIENT'
GO
Il est important de spécifier les bons paramètres :
-
@article_resolver : correspond à la description du résolveur.
-
@is_dotnet_assembly : doit être à “true” si la DLL a été écrite en .NET.
-
@dotnet_assembly_name : correspond au chemin exact de la DLL résolveur à inscrire.
-
@dotnet_class_name : correspond au nom de la classe qui comporte le code personnalisé du résolveur.
L’exécution de cette procédure stockée avec les bons paramètres permet donc d’inscrire convenablement le résolveur personnalisé sur le serveur de distribution.
Affectation du nouveau résolveur sur un article particulier :
La dernière étape consiste donc à affecter ce nouveau résolveur à un ou plusieurs articles de la publication.
Pour cela, il suffit de retourner une nouvelle fois sur les propriétés d’un article et de se positionner sur l’onglet “Resolver”.
Le nouveau résolveur personnalisé qui vient d’être créé doit normalement apparaitre dans la liste “custom resolver” :
Il suffit de le sélectionner pour l’article “CLIENT” puis de cliquer sur “OK” pour le prendre en compte.
Pi-R.
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :