DataSet et .NET 1.1 : Attention !
De part ses similitudes avec la structure des données en base, le DataSet (et les classes gravitant autour : DataView, DataTable, DataRow, ...) est un outil de manipulation de données très appréciable. Cependant, son utilisation peut mener à quelques pièges, dont certains peuvent être fatals pour la stabilité de l'application.
Dans une application ASP.NET classique orientée autour d'une base de donnée, nous sommes souvent amenés à afficher des donner dans un tableau. A cet usage, un DataGrid alimenté par un DataSet ou un DataView suffit dans la plupart des cas, et permet de produire un code souple et efficace en peu de temps.
Si la source de données comporte beaucoup de résultats, il devient alors utile d'ajouter un système de pagination. Toujours dans une optique de simplicité, le développeur sera tenté de réaliser cette pagination directement dans le code C#, à l'aide de tous les outils fournis par ASP.NET. Je ne m'attarderais pas aujourd’hui sur les risques d'une telle approche (un indice toutefois : Large Object Heap), pour me concentrer sur un piège plus vicieux et plus dangereux.
Mise en situation
C'est l'histoire d'une fonction de recherche particulièrement lourde (temps d'exécution de l'ordre de la dizaine de secondes) sur une page ASPX appelée "Page Y", d'un ingénieur, et d'un client exigeant. Le client, non satisfait (à raison) de la lenteur de chargement de la page, demande à l'ingénieur de remédier au problème. L'ingénieur, après avoir appliqué toutes les astuces d'optimisation simples sur sa procédure stockée (je vous ferai sans doute un billet sur l'utilisation du plan d'exécution sous SQL Server si ça en intéresse certains), décide que la page Y serait bien plus agréable à utiliser si les données n'étaient pas rechargées systématiquement à chaque opération de l'utilisateur (tri par exemple). Le candidat idéal apparaît alors rapidement : la session. Ni une, ni deux, notre ingénieur place le DataSet stockant le jeu de résultat dans la session, pour le conserver d'un appel à l'autre de notre chère page Y.
Un client heureux
Le problème de performance est corrigé et le client est satisfait, notre ingénieur peut dormir sur ses deux oreilles. Mais d'évolution en évolution, l'application s'embellit, les visiteurs sont de plus en plus nombreux, et les besoins de mémoire vont crescendo. Commencent à apparaître des problèmes graves de mémoire et, à nouveau mécontent, le client refait appel à notre ingénieur.
Celui-ci, après analyse méthodique des logs, découvre que les problèmes de mémoires apparaissent sur la plupart des pages. Une analyse plus poussée montrera une tendance : en début de journée, la page Y est la première à manquer de mémoire, avant que le problème ne s'étende sur le reste de l'application. De plus, les utilisateurs rapportent que se déconnecter et se reconnecter permet de s'affranchir des erreurs pendant un temps.
Mais que se passe-t-il ?
Fort de son expérience, notre ingénieur fait tout de suite le rapprochement entre la session et la déconnexion de l'utilisateur... et retourne donc 'intéresser à la page Y. La taille du DataSet est certes grande (plusieurs dizaines de kilo-octets), mais elle ne suffit pas à expliquer les problèmes de mémoire. En étendant son champs de recherche, l'ingénieur se souvient alors que, cluster de serveurs oblige, la session est stockée dans une base de données. Après quelques recherches pour retrouver dans quelle table est stockée la session, notre ingénieur concocte une petite requête pour déterminer la taille de la session en base, avant de tomber de son siège.
Profitons de ce moment d'hébétement chez notre ingénieur pour comprendre à notre tour ce qu'il se passe. Au début de l'exécution de chaque page, ASP.NET récupère les données de session depuis la base de données, et les renvoie à la source à la fin du traitement. Les données sont stockées en base dans un champ binaire, ASP.NET appelle donc fort logiquement le serializer binaire sur chaque objet avant de les envoyer dans la base, et fait la manipulation inverse avant de les récupérer.
L'étude de l'objet DataSet avec Reflector (ou une petite recherche sur Internet) nous montre que, même par le serializer binaire, le DataSet est stocké en XML. Donc un simple DataSet contenant une colonne "nombre" et cinq lignes peut devenir quelque chose comme :
<NewDataSet>
<Table1>
<nombre>1</nombre>
</Table1>
<Table1>
<nombre>2</nombre>
</Table1>
<Table1>
<nombre>3</nombre>
</Table1>
<Table1>
<nombre>4</nombre>
</Table1>
<Table1>
<nombre>5</nombre>
</Table1>
</NewDataSet>
Fort de cette information, nous pouvons jeter un oeil sur l'écran de notre ingénieur qui se relève péniblement sur sa chaise, et nous constatons que son DataSet occupe la bagatelle de 3 méga-octets dans la base de données. A cela, ajoutons l'espace mémoire supplémentaire nécessaire pour désérialiser puis resérialiser l'objet, multiplions par le nombre moyen de pages visualisées par un utilisateur ayant notre DataSet en session, et nous comprenons rapidement que nous avons un problème.
Que faire alors ?
Déjà, il faut savoir que ce problème a été corrigé dans .NET 2.0 (parait-il en tout cas, j'avoue que je n'ai pas encore vérifié). Donc le problème ne se pose que pour ceux qui utilisent le Framework 1.1. A partir de là, la solution la plus évidente est : ne pas mettre le DataSet en session, et essayer de s'en sortir par des moyens alternatifs (comme le cache, si le jeu de résultats est partagé par tous les utilisateurs). Ce n'est hélas pas toujours possible, et s'il n'est pas possible de s'affranchir de l'utilisation de la session, je recommande l'utilisation de l'objet DataSetSurrogate, téléchargeable à l'adresse suivante :
http://support.microsoft.com/kb/829740
Il s'agit d'un objet récupérant les informations du DataSet, et se sérialisant en binaire. Son utilisation est très simple :
DataSet ds = new DataSet();
// Mise en session
Session["monDataSet"] = new DataSetSurrogate(ds);
// Récupération des données, je passe les contrôles de rigueur
ds = ((DataSetSurrogate) Session["monDataSet"]).ConvertToDataSet();
Un nouveau test dans la base de données montre que le même DataSet que précedememnt occupe maintenant moins de 200 kilo-octets. Ce n'est pas parfait, mais cela devient suffisant pour corriger le problème de mémoire. Le client est à nouveau heureux (je plaisante, c'est un client quand même), et notre ingénieur peut retourner rêver d'un projet en .NET 3.5.
Cet ingénieur, cela aurait pu être moi, et cela aurait pu être vous. Alors ne prenez jamais à la légère l'utilisation des DataSet, surtout en .NET 1.1.
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 :