Il existe dans SQL SERVER CE un système de tracking des changements.

Il est utilisé dans la réplication SQL SERVER 2005 – 2008 tout comme dans le framework Sync Services for ADO.NET

Seulement ce système de change tracking n’est PAS accessible pour nous pauvre développeurs non Microsoft (c’est balot)

A chaque demande “Donnez nous l’accès au Change Tracking sur Sql Server CE”, la réponse était immanquablement “Euh … non, vous en avez pas besoin, dites nous votre problème, on vous montrera comment s’en passer”

Frustrant ….

Aujourd’hui bonne nouvelle, au détour d’un vieux Post qui date de l’année dernière dans mon anglais incertain où je demandais (supplier :) ) cette feature, Sean Kelly, Program Manager de Sync Services et SQL Server Compact, annonce la disponibilité de cette fonctionnalité dans une future release de Visual Studio 2010 !

Bonne nouvelle du jour, donc.

,

Tiens un truc très sympa comme addin à récupérer absolument si vous travaillez avec SQL SERVER CE : Sql Compact SSMS Addin

Tout d’abord, si vous n’avez jamais travaillé avec Compact Edition, dans SSMS, sachez que vous pouvez oublier le clic droit sur une table, pour voir les données ou scripter un Select ..
Et ne parlons même pas du Script Datas …

A la rigueur, dans VS.Net, ça fonctionne, mais bref, c’est pas la joie…

Bizarre me direz vous, et je dirais que vous avez raison ! (Fini à l’arrache le truc hein …)

Bref, voici un superbe addon qui comble cet écueil.

Voici quelques screenshots :

ver2

Ah, quand on le voit comme ça c’est pas très impressionnant hein, en fait c’est surtout la première fois que vous utilisez SSMS et Compact Edition “sans” ça que ça choque :)

Bref, que du bonheur

addin1

Cette petite merveille vous génère aussi toutes les données et datas de votre base de données SQL COMPACT CE.

Steve Lasker vient d'écrire un superbe post récapitulant pas mal de posts, démos et slides sur Sql Server Compact et Sync. Services.

http://blogs.msdn.com/stevelasker/archive/2008/11/25/demos-presentations-links-screencasts-and-videos-for-sql-server-compact.aspx

Je vous conseille particulièrement la démo sur le gain de performance avec Sql Server Compact, avec un "simili" pool de connection et cache de commandes.

C'est assez bluffant..

SQL Server Compact Bulk Insert Performance Test Harness (Compares Compact & Express)

Vous avez un screencast sur cette partie, qu'il a présenté lors de la PDC 2008.

https://sessions.microsoftpdc.com/public/timeline.aspx (Faites une recherche sur Sql Server Compact, la session s'appelle Sql Server Compact : Embedding in desktop and device applications)

image

Sur le screenshot précédent, l'application de test qui génère des insertions en base de données (Sql Server CE)

On fait un test entre une méthode vraiment "cracra" où on crée, ouvre, ferme la connexion et la commande sur chaque ligne importée et une version "plus mieux" où la connexion est réutilisée et la commande mis en cache

Résultat sans appel :

  1. 1ere méthode : 6 Transactions / seconde
  2. 2eme méthode : 6000 Transactions / seconde

Vous trouverez aussi dans la solution fournie un exemple d'assembly qui gère une sorte de pool de connexion ,via une Queue, et une collection de command mises en cache.

,

Ce post fait suite au post d'hier, au sujet du multi threading avec Sql Server CE

Dans l'exemple, nous avons démontrer que nous ne pouvions pas faire de multi threading et que nous remontions de vieilles exceptions pas extraordinaire.

D'ailleurs, j'étais conforté par cette idée, au vu d'un post de Ayende quand celui ci cherchait une base de donnée embarquée.

 

En fait il existe une solution.

Sql Server CE accepte le multi threading, à partir du moment où chaque thread possède sa propre SqlCeConnection.

Mais me direz vous, ça parait normal que chaque opération crée une nouvelle Connection, réflexe d'utilisation de Sql Server et son pool de connexion.

Nous n'en avons pas sur Sql Server CE.

Et là où c'est balot, c'est que ce n'est ni Sql Server CE, ni mes Services asynchrones qui étaient en erreur .. mais Linq to Sql et ma petite classe static Manager, qui est chargée de me rapatrier une connexion SqlCeConnection.

Pour expliquer ce geste de folie, un DataContext Linq nécessite en constructeur une connexion (SqlCeConnection dans mon cas)

J'avais donc écrit un truc du genre:

  private static SqlCeConnection configurationDbConnection;
 /// <summary>
///
Get Connection From Configuration File if any
/// </summary>
public static SqlCeConnection ConfigurationDbConnection
    {
        get
     
{
            if(configurationDbConnection != null)
                return configurationDbConnection;

            ConnectionStringSettings connection =

           ConfigurationManager.ConnectionStrings["LocalDatabaseConnection"] 
           as ConnectionStringSettings;

     if (connection == null && ConfigurationManager.ConnectionStrings.Count > 1 &&
        ConfigurationManager.ConnectionStrings[1].ProviderName == "Microsoft.SqlServerCe.Client.3.5")
        connection = ConfigurationManager.ConnectionStrings[1] as ConnectionStringSettings;

     if (connection == null) 
       throw new ApplicationException("No Connection String in application configuration file");

     configurationDbConnection = new SqlCeConnection(connection.ConnectionString);

     return configurationDbConnection;
 }
}

 

On voit que je stocke la connection créée dans une variable statique.

Monumentale ERREUR !!

Chacun des threads créant son propre DataContexte, va utiliser la MÊME Connexion qu' un autre thread (static inside)

Pour être sûr que chaque DataContexte crée sa propre connexion, il suffit juste d' appeler son constructeur avec un paramètre non pas une connexion, mais juste une chaîne de connexion.

Un petit coup de reflector, nous montre qu'il va alors bien créer une nouvelle connexion :

image

provider.CreateConnection() appellant le DbProviderFactory de SqlServerCe, celui ci retournant un joli new SqlCeConnection().

Du coup, mon petit code de mon Manager devient :

private static ConnectionStringSettings connectionStringSettings;
/// <summary>
/// Get Connection From Configuration File if any
/// </summary>
public static SqlCeConnection GetConfigurationDbConnection()
{
    if (connectionStringSettings == null)
    {
        connectionStringSettings = 
                  ConfigurationManager.ConnectionStrings["LocalDatabaseConnection"] 
                  as ConnectionStringSettings;

        if (connectionStringSettings == null && ConfigurationManager.ConnectionStrings.Count > 1 &&
                ConfigurationManager.ConnectionStrings[1].ProviderName == "Microsoft.SqlServerCe.Client.3.5")
            connectionStringSettings = ConfigurationManager.ConnectionStrings[1] as ConnectionStringSettings;

        if (connectionStringSettings == null) 
           throw new ApplicationException("No Connection String in application configuration file");
    }

    return new SqlCeConnection(connectionStringSettings.ConnectionString);
}

Et là, miracle (ou pas ) tout fonctionne…

Conclusion

Alors, pour faire du multi threading avec Sql Server CE, pensez à une chose importante :

Chaque Thread doit avoir sa propre connexion.

A partir de là, tout roule.

Et si on reprend les caractéristiques de Sql Server CE, on peut créer 256 connexion simultanées, ce qui vous laisse de la marge !

Et voilà, je viens d'y passer une journée (encore …) et non, Sql Server CE, n'est PAS multi thread safe…

Alors voilà soyons clair :

Feature

SQL CE

Size of total deployment

1,834 KB

Number of concurrent connections

256

Concurrent process connections

1

Database Size Limit

4GB

Max CPUs Supported

1

Alors la conclusion est consternante, mais :

  • Oui, vous pouvez ouvrir plein de connections simultannément
  • Non, vous ne pouvez pas ouvrir 2 connections sur 2 threads séparés..

Maintenant, un exemple que je viens de faire.

Une petite application où je teste 3 cas. Dans chacun des cas, je fais 1 appels sur 2 services différents. Chaque service me renvoyant des données provenant d'une base de donnée Sql Server CE.

les 3 cas que je veux tester :

  1. Apel des 2 services en synchrone, histoire de vérifier que ça marche dans un cas classique mono thread
  2. Appel de mes 2 services par WCF, en monde Asynchrone (via les proxys générés et leurs appels asynchrones générés)
  3. Appel de mes 2 services directement, et utilisation de BackgroundWorkers

L'application est architecturée de manière classique.

  1. Une UI
  2. Un Service WCF
  3. Un projet Entité, partagé par le service et l'UI

 image

Je fais un peu de WCF, mes services sont exposés et je génère les proxys des services via un "Add Service Reference" :

image

Note : Je demande à générer les appels asynchrones, sinon aucun intéret de tester le multi-threading :)

Petit Hack dans mon architecture : Pour le 3eme cas, je référence directement mon assembly service dans mon projet UI (Rhoooo c'est pas bien; mais c'est pour l'exemple :) )

Pour information, j'ai utilisé Linq To Sql pour générer le dbml dans le projet Entités, mais pour être absolument sûr du résultat, j'utilise une bonne vielle connection ADO.NET et je génère les commandes à la main.

Cas 1 : WCF Synchrone

Je crée mes 2 services (mes proxys) :

// For Exemple 1 and 2 : use of WCF Services
public ClientProxy.ClientServicesClient ClientServices { get; set; }
public ParameterProxy.ParameterServicesClient ParameterServices{ get; set; }

Je les instancie au démarrage :

ClientServices = new SqlServerCE_WCFAsync.ClientProxy.ClientServicesClient();
ParameterServices = new SqlServerCE_WCFAsync.ParameterProxy.ParameterServicesClient();

J'appel mes 2 services :

this.dataGridView1.DataSource  = ClientServices.GetClients2();

var p = ParameterServices.GetParameter("AddressMail");
if (p != null)
{
    label1.Text = p.Value;
}

Bon, ben ça, ça fonctionne…

Continuons..

Cas 2 : WCF Asynchrone

Mes 2 services sont créés (voir Cas 1)

Je configure le service Client, pour qu'il utilise son proxy de manière asynchrone :

ClientServices.GetClients2Completed += ClientServices_GetClients2Completed;

je crée la méthode Completed :

void ClientServices_GetClients2Completed(object sender, 
             SqlServerCE_WCFAsync.ClientProxy.GetClients2CompletedEventArgs e)
  {
      this.dataGridView1.DataSource = e.Result;
  }

Et je tente d'appeller ce service en asynchrone, alors que j'appel le service Parameter en synchrone lui :

ClientServices.GetClients2Async();
var p = ParameterServices.GetParameter2("AddressMail");
if (p != null)
{
    label1.Text = p.Value;
}

Et là ça se vautre lamentablement avec une exception qui ferait frémir un mamouth congelé :

"

A first chance exception of type 'System.AccessViolationException' occurred in System.Data.SqlServerCe.dll
Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

"

Cas 3 : Utilisation de BackGroundWorker

Ici, je me passe de WCF (et donc je référence directement l'assembly service dans mon projet UI)

et je tente d'utiliser un BackGroundWorker sur le service ClientServices et je fais un appel direct sur le service ParameterServices

Configuration du BackGroundWorker :

bgw1 = new BackgroundWorker();
bgw1.DoWork += new DoWorkEventHandler(bgw1_DoWork);
bgw1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgw1_RunWorkerCompleted);

Configuration du travail à effectuer :

void bgw1_DoWork(object sender, DoWorkEventArgs e)
 {
     SqlServerCE_WCFServices.ClientServices service = new SqlServerCE_WCFServices.ClientServices();

     e.Result = service.GetClients2();

 }

Configuration du completed du BackGround Worker :

void bgw1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
  {
      if (e.Error != null)
          Trace.WriteLine("Erreur in User Interface in GetClients. Exception : " + e.Error.Message);

      if (e.Cancelled)
          Trace.WriteLine("Action cancelled in User Interface in GetClients");

      this.dataGridView1.DataSource = e.Result as List<Client>;

  }

Appel :

bgw1.RunWorkerAsync();

SqlServerCE_WCFServices.ParameterServices service = new SqlServerCE_WCFServices.ParameterServices();
var p = service.GetParameter2("AddressMail");
if (p != null)
{
    label1.Text = p.Value;
}

Et là, c'est le drame …

"

A first chance exception of type 'System.AccessViolationException' occurred in System.Data.SqlServerCe.dll
Attempted to read or write protected memory. This is often an indication that other memory is corrupt
.

"

Conclusion

Alors voilà, conclusion:

  • Oui, utilisez un BackGround Worker pour faire des appels asynchrone et rendre votre application moins freezée et plus agréable
  • Non, n'utilisez pas d'appel asynchrone sur plusieurs threads…

Et là, je demande, comment faire avec WCF ?? Le monsieur génère un Thread par Service Proxy… c'est balôt !

Et je demande encore, comment faire une opération (de maintenance, ou de synchronisation) en arrière plan dans mon application desktop sous SQL Server CE ? Je demande à mon utilisateur d'être sympa et de pas faire de requêtes en attendant ??

Je laisse le code à disposition si le coeur vous tente :)