2EE et .NET sont des frameworks très complets qui n'ont pas lésiné sur les moyens d'accès aux bases de données. Ils nous proposent respectivement JDBC (Java DataBase Connectivity) et ADO.NET (évolution de ActiveX Data Object), deux API très efficaces et très flexibles permettant de se connecter à divers structures de stockage. Nous centrerons cet article sur l'utilisation de ces API dans le cadre de bases de données relationnelles.

Après avoir rappelé l'historique de chacune de ces technologies, nous passerons en revue diverses méthodes d'interaction avec les bases de données, et nous nous poserons des questions récurrentes :

Nous ne traiterons pas ici de mapping objet/relationnel (voir les articles de Sami Jaber sur ObjectSpaces et ses concurrents à ce sujet), mais uniquement des fonctionnalités apportées par les API d'accès aux données.

Il était une fois JDBC

JDBC est apparu en 1996 avec le JDK 1.1. Cette API permit dès lors la connexion à n'importe quelle base de données relationnelle, pour peu qu'il existe un "driver" de connexion pour cette base. En effet, JDBC n'est qu'une interface de programmation destiné à interagir avec une Base de Données. Pour réellement pouvoir l'utiliser, il est indispensable de disposer d'une implémentation spécifique de JDBC appelé Driver.

Les incontournables types de drivers JDBC

Il existe 4 types de drivers JDBC :

Les éditeurs de bases de données fournissent généralement un driver JDBC adapté (de Type 2 ou 4 la plupart du temps). Mais parfois, pour des raisons de performances ou de facilité de déploiement, vous serez amenés à acheter un driver développé par une société tierce, spécialisée dans ce domaine. Vous pouvez jeter un oeil sur le site de Sun pour prendre connaissance de la liste exhaustive des drivers JDBC existants (cf ressources).

Historique des versions de JDBC et principales fonctionnalités

JDBC 1.0 (paru en 1996) : la première version de l'API permet bien sûr d'établir une connexion avec une base de données relationnelle, d'exécuter des requêtes SQL et d'en traiter les résultats. Mais il faut avouer que JDBC 1.0 avait de graves lacunes, qui ont été comblées par les versions ultérieures.

JDBC 2.0 (paru en 1998) : l'API se découpe cette fois en deux parties appelées "core" et "optional package". En plus des fonctionnalités de base proposées par la version précédente, on trouve dans JDBC 2.0 :

JDBC 3.0 (paru en 2002) : cette dernière mouture parvient à masquer certains aspects propriétaires des moteurs de BD relationnelles, et à standardiser certaines optimisations avancées. En particulier, on trouve dans JDBC 3.0 :

 

En conclusion, disons que malheureusement, tous les drivers ne supportent pas toutes les fonctionnalités que nous venons de mentionner. Il faudra un certain temps aux développeurs de drivers JDBC pour implémenter les optimisations de la version 3.0. Mais sans ce bémol, il faut avouer que nous disposons d'un outil pointu et flexible permettant à toute application Java de se connecter à toute base de données relationnelle. Et même si nous n'en parlerons pas ici, sachez que JDBC peut s'adapter à d'autres types de stockage de données tels que les feuilles de calcul, les fichiers structurés...

Il était une fois ADO.NET

Historique des API Microsoft d'accès aux données

L'interface historique de connexion aux bases de données en environnements Microsoft est bien sûr ODBC (Open DataBase Connectivity), qui est apparue avant JDBC. Cette interface générique permet elle aussi à une application (Delphi, Python, VB ou VC++ à l'époque) de se connecter à n'importe quelle base de données du marché, pour peu que lui corresponde un driver ODBC.

ODBC est une interface de relativement bas niveau, et d'un usage peu aisé en particulier en Visual Basic. Ce besoin a mené à l'introduction de RDO (Remote Data Object) qui permit à VB de discuter très simplement avec une base de données relationnelle. RDO est en réalité une surcouche de ODBC, et permet à VB de se connecter sur une base locale ou distante à travers le réseau local.

RDO n'a jamais été rendue disponible au framework VC++ (les Microsoft Foundation Classes), et les développeurs sous cet environnement ont dû attendre DAO (Data Access Object) pour voir des classes d'accès aux données s'intégrer directement aux MFC. Mais DAO ne permit initialement aux applications que de se connecter à une base Microsoft Access (via le moteur Jet). Au vu du succès de DAO, ce fut une sage décision que d'étendre la liste des bases accessibles à travers DAO; la technique était simple : DAO permet de se connecter soit directement au moteur Jet d'Access, soit à un pilote ODBC standard via le pont ODBCDirect.

Mais tout compte fait, cela nous donne beaucoup d'interfaces : ODBC, RDO, DAO... pour le même usage! Microsoft a donc décidé d'uniformiser l'accès aux sources de données avec OLE-DB (Object Linking and Embedding - DataBase). Cette nouvelle API permet à n'importe quel langage (OLE-DB se base sur les composants COM, donc indépendants du langage de programmation) de se connecter à n'importe quel type de source de données (BD relationnelles, fichiers, structurés, tableurs, messagerie). Les performances de OLE-DB sont généralement bien meilleures que celles des API précédentes : à chaque source de données correspond un "provider OLE-DB", "réimplémenté" pour l'occasion, et qui n'hérite pas des faiblesses de ses prédécesseurs. Dans le pire des cas, si aucun provider OLE-DB n'a été implémenté pour une source de données, on pourra toujours utiliser le pont OLE-DB / ODBC.

Malheureusement, OLE-DB est une API COM (Component Object Model), donc d'assez bas niveau. Elle a rapidement été simplifiée par la couche plus abstraite ADO (ActiveX Data Objects), dans laquelle on trouve les interfaces Connection, Command, et Recordset. ADO était accessible à tous les développeurs, quel que soit leur langage de programmation. Sa philosophie était plutôt orientée "connexion", c'est-à-dire que l'on privilégiait le maintien d'une connexion entre application et base de données le temps de traiter les résultats des requêtes SQL.

Avec l'avènement de .NET, l'API standard de connexion à une source de données (relationnelle, XML, tabulaire, etc...) devient ADO.NET, précédemment appelé ADO+. Dans cette API, la philosophie est plutôt orientée vers le mode "déconnecté" dans lequel les applications exécutent leurs requêtes sur la base de données, prennent une copie du résultat de leur requête, et libèrent la connexion avant de traiter les informations. Nous reviendrons sur cette manière de procéder dans les sections suivantes.

Types de providers ADO.NET

Tout comme JDBC, ADO.NET n'est qu'une interface de programmation. Pour se connecter à une source de données, il faut se procurer l'implémentation de cette interface, que l'on appelle un ".NET Data Provider".

La bonne nouvelle est qu'il existe dans le framework .NET deux Data Providers inclus par défaut :

Si votre source de données ne peut être accédée que par ODBC (Excel par exemple), sachez qu'il existe également un ODBC .NET Data Provider disponible en téléchargement sur le site de Microsoft (cf Références).

Un petit regret malgré tout au niveau de la conception de cette API d'accès aux données : le code d'une application qui souhaite utiliser le provider SQLServer n'est pas le même que celle qui utilisera OLE DB. Plus précisément, il existe autant de hiérarchies de classes que de providers de données .NET ! Un petit exemple :

// En utilisant le OLE DB .NET Data Provider OleDbConnection cnx = new OleDbConnection("..."); cnx.Open(); OleDbCommand cmd = new OleDbCommand ("...", cnx); OleDbDataReader reader = cmd.ExecuteReader(); // En utilisant le SQL SERVER .NET Data Provider SqlConnection cnx = new SqlConnection("..."); cnx.Open(); SqlCommand cmd = new SqlCommand ("...", cnx); SqlDataReader reader = cmd.ExecuteReader();

Gageons que Microsoft reverra la conception de cette API dans les versions à venir du framework .NET et utilisera une classe Factory ainsi qu'un ensemble de classe abstraites découplant l'application utilisatrice du type de provider utilisé pour se connecter à la source de données...

Bien. Maintenant que nous savons d'où proviennent nos deux API, ADO.NET et JDBC, nous allons passer en revue leurs fonctionnalités respectives, et mettre en parallèles les exemples de code Java et C# qui accèdent à ces fonctionnalités.

Connexion et requête de sélection en mode connecté

Exemples basiques

L'exécution d'une requête SQL de sélection en JDBC est triviale, jugez plutôt :

import java.sql.*; public class PremiereConnexion { public static void main(String[] argv){ try{ // Chargement du driver de la BD "Hypersonic SQL" Class.forName("org.hsql.jdbcDriver"); // Ouverture de la connexion Connection cnx = DriverManager.getConnection ("jdbc:HypersonicSQL:hsql://localhost") ; // Exécution d'une requête SQL sur cette connexion Statement stmt = cnx.createStatement(); ResultSet rs = stmt.executeQuery("SELECT nom, prenom, age FROM Gurus"); // Traitement des résultats while (rs.next()){ String nom = rs.getString("nom"); // ou rs.getString(1); String prenom = rs.getString("prenom"); int age = rs.getInt("age"); // Faire quelque chose d'utile avec ces infos } } catch(Exception e){ // Gestion des exceptions JDBC éventuelles... } finally { try{ if (cnx != null){ cnx.close(); } }catch(Exception e){ // Gestion de l'impossibilité de fermer la connexion } } } } PremiereConnexion.java

ADO.NET n'est pas en reste :

public class PremiereConnexion { public static void Main(string[] argv) { // Configuration de la connexion OleDbConnection cnx = new OleDbConnection ("Provider= SQLOLEDB;data source= PAR-GIL;initial catalog=DNG;" + "password= héhéhé;persist security info=True;user id=sa;" + "workstation id= PAR-GIL;packet size=4096"); cnx.Open(); OleDbCommand cmd = new OleDbCommand ("SELECT Nom, Prenom, Age FROM Gurus", cnx); OleDbDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { String nom = reader.GetString(0); String prenom = reader.GetString(1); int age = reader.GetInt32(2); // Faire quelque chose d'utile avec ces infos } cnx.Close(); } } PremiereConnexion.cs

Optimisation

JDBC : utilisons un PreparedStatement

Le code Java précédent n'est pas optimal : si dans le même programme vous êtes amenés à exécuter les mêmes requêtes à plusieurs reprises, il est conseillé d'utiliser un PreparedStatement en lieu et place d'un Statement. Celui-ci pourra être doté de paramètres, qui pourront varier d'une invocation à l'autre. L'intérêt d'un PreparedStatement se situe au niveau de l'interprétation de la requête SQL ainsi que du calcul de son plan d'exécution côté BD. Ceux-ci seront effectués qu'une seule fois lors de l'exécution de plusieurs requêtes du même type, ce qui améliore sensiblement les performances d'accès aux données.

// Seule l'exécution de la requête change. // Le reste du code est identique PreparedStatement stmt = cnx.prepareStatement ("SELECT nom, prenom FROM Gurus WHERE age < ?"); stmt.setInt(0, 26); // Ce qui revient à "WHERE age < 26" ResultSet rs = stmt.executeQuery(); UtilisationPreparedStatement.java

Notez que l'emploi d'un PreparedStatement simplifie la conception dynamique des requêts SQL : le remplacement des ? par les valeurs souhaitées se fait proprement par l'invocation de méthodes adaptées comme "setInt(indice)". Cela reste vrai pour les valeurs de type String, ce qui s'avère bien pratique : il n'est plus nécessaire de doubler les simples quotes des chaînes de caractères, ni de gérer les caractères spéciaux manuellement.

ADO.NET : tirons parti de la finesse de l'objet Command

Le code C# précédent passe déjà par un objet intermédiaire, l'objet Command (OleDbCommand ici), qui joue exactement le même rôle que le PreparedStatement JDBC. Donc en ADO.NET également, on peut utiliser dans la requête des paramètres que l'on positionnera à loisir dans la suite du programme avant de relancer la requête précompilée. Le code ADO.NET est d'une simplicité comparable à JDBC :

// Seule l'exécution de la requête change. // Le reste du code est identique OleDbCommand cmd = new OleDbCommand ("SELECT Nom, Prenom FROM Gurus WHERE age < ?", cnx); cmd.Parameters.Add(new OleDbParameter("age", 26)); OleDbDataReader reader = cmd.ExecuteReader(); UtilisationParametresCommand.cs

Procédures Stockées

Bien entendu, JDBC comme ADO.NET permettent d'exécuter des procédures stockées et précompilées en base de données. Nous n'entrerons pas ici dans la lutte sempiternelle qui oppose :

Nous vous laissons prendre position (un compromis semble, comme toujours, être une sage décision...), et allons nous borner à vous donner un exemple de déclenchement de procédure stockée via nos deux interfaces désormais favorites. Mais tout d'abord, voici le code Transact-SQL de la procédure stockée (compilée et stockée dans SQL Server 2000) que nous allons invoquer :

CREATE PROCEDURE [dbo].[GetYoungGurus] @age int AS SELECT Nom, Prenom FROM Gurus WHERE Age < @age GO GetYoungGurusProc.sql

L'invoquer avec JDBC ne pose aucun problème :

CallableStatement stmt = cnx.prepareCall("{call dbo.GetYoungGurus(?)}"); stmt.setInt(1, 26); ResultSet rs = stmt.executeQuery(); InvocProcStock.java

De même en ADO.NET :

OleDbCommand cmd = new OleDbCommand("GetYoungGurus", cnx); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add(new OleDbParameter("age", 26)); OleDbDataReader reader = cmd.ExecuteReader(); InvcProcStock.cs

Batch Updates

Toujours dans l'optique d'optimiser notre code d'accès à la base de données, sachez qu'il est tout à fait possible aux applications Java d'expédier un ensemble de requêtes SQL à la base de données d'un bloc, ce que l'API JDBC appelle le Batch Update. Le principe est très simple, et bien représenté par les quelques lignes suivantes :

Statement stmt = cnx.createStatement(); stmt.addBatch("INSERT INTO Gurus VALUES ('Sami', 'Jaber', 28)"); stmt.addBatch("INSERT INTO Gurus VALUES('Thomas', 'Gil', 25)"); int [] updateCounts = stmt.executeBatch(); UtilisationBatchUpdate.java

ADO.NET ne propose pas directement d'équivalent de cette notion de Batch Update, mais vous verrez dans la section suivante que cela n'est pas pénalisant car nous pourrons simuler un Batch Update sans problème en utilisant l'objet DataSet.

Mode déconnecté

JDBC : Lecture déconnectée en utilisant un RowSet

Les RowSet Java sont des JavaBeans, donc sérialisables, qui peuvent être connectés ou déconnectés (depuis JDBC 2.0). Il peut exister autant de types de RowSets que de vendeurs de BD ou de frameworks d'accès aux données Java, mais Sun en fournit typiquement trois par défaut :

Donc dans cette liste, seul le CachedRowSet correspond à une architecture déconnectée d'accès aux données. Voici un petit exemple de code Java qui manipule cette classe :

Connection cnx = DriverManager.getConnection ("jdbc:HypersonicSQL:hsql://localhost"); Statement stmt = cnx.createStatement(); ResultSet rs = stmt.executeQuery("SELECT nom, prenom, age FROM Gurus"); CachedRowSet crset = new CachedRowSet(); crset.populate(rs); crset.execute(); // Déconnexion de la source de données rs.close(); stmt.close(); cnx.close(); // Traitement déconnecté des données while (crset.next()) { System.out.println(crset.getString("nom")); } UtilisationCachedRowSet.java

Vous l'aurez compris dans ces quelques lignes de code Java : un RowSet représente une collection de tuples en mémoire de notre programme. Il n'est absolument pas connecté à la source de données que l'on a utilisée. Pour le garnir, il suffit de faire appel à la méthode "populate()". Dans cet exemple, nous n'avons fait que lire les informations du RowSet, chose que nous aurions très bien pu faire par le biais d'un ResultSet tout à fait standard; cela aurait d'ailleurs été plus efficace et moins coûteux en occupation mémoire. Mais si les mêmes informations peuvent resservir, un peu plus tard dans le flot d'exécution de notre programme, ou si un traitement complexe de ces informations doit être implémenté en Java, le cache que constitue notre RowSet peut devenir très intéressant puisqu'il évite des allers-retour incessants entre l'applicatif et la base de données.

ADO.NET : l'objet DataSet

Le framework ADO.NET permet lui aussi d'établir un mécanisme d'accès aux données en mode déconnecté; la classe centrale s'appelle ici DataSet, et se comporte globalement comme un RowSet. Ce qui est très intéressant, c'est que tout est prévu dans le reste de la plateforme .NET pour manipuler très aisément un DataSet :

Comme vous le voyez, le framework .NET est vraiment centré autour de cet outil de traitement d'informations déconnecté, le DataSet, bien qu'il soit toujours possible de traiter en flux continu les résultats de requêtes par le biais d'un DataReader.

Au niveau programmation, le code permettant de garnir un DataSet est conceptuellement très proche du code Java / RowSet équivalent, jugez plutôt :

SqlCommand cmd = new SqlCommand(); cmd.CommandText = "SELECT Nom, Prenom, Age FROM Gurus"; cmd.Connection = cnx; // Un DataAdapter permet d'exécuter des requêtes sur la base // et de garnir un DataSet avec le résultat de ces requêtes SqlDataAdapter da = new SqlDataAdapter(); da.SelectCommand = cmd; DataSet ds = new DataSet(); da.Fill(ds); foreach (DataRow row in ds.Tables[0].Rows) { Console.WriteLine("Age du gourou : " + row["age"]); } UtilisationDataSet.cs

Tout est donc fait pour nous simplifier la vie. Mais ce n'est pas tout : VisualStudio.NET nous permet de concevoir des DataSet typés, c'est-à-dire taillés précisément pour un usage particulier. Dans notre exemple, utiliser un DataSet typé permettrait à la fois d'améliorer un soupçon nos performances, mais surtout de rendre notre code C# encore plus clair. Supposons que nous ayons créé avec VS.NET un DataSet appelé "GuruDataSet"; notre exemple se transforme dès lors en :

SqlCommand cmd = new SqlCommand(); cmd.CommandText = "SELECT Nom, Prenom, Age FROM Gurus"; cmd.Connection = cnx; SqlDataAdapter da = new SqlDataAdapter(); da.SelectCommand = cmd; // DataSet typé : toutes les tables et colonnes sont // disponibles par la complétion automatique lors de l'édition ! GuruDataSet gds = new GuruDataSet(); da.Fill(gds); foreach (GuruDataSet.GurusRow row in gds.Gurus.Rows) { Console.WriteLine("Age du gourou : " + row.Age); } UtilisationDataSetType.cs

Le début du code ne change pas. Par contre, dès que notre GuruDataSet est rempli, le code qui le parcourt est bien plus intuitif et plus robuste que dans l'exemple précédent. En réalité, le GuruDataSet ne peut contenir (dans notre conception) que la table "Gurus", dont les colonnes sont respectivement "nom, prénom, age". La conception d'un DataSet typé se fait bien entendu graphiquement dans VS.NET :

 

L'intérêt principal est que le DataSet étant typé, le développeur ne peut plus se tromper de table ni de colonne : il sait dès la compilation qu'il a commis une erreur concernant le schéma du DataSet, ce qui permet de développer des applications robustes très rapidement.

Mise à jour des informations en utilisant un RowSet / DataSet

Jusqu'à présent, nous avons employé les RowSet / DataSet pour traiter les données en lecture seule. C'est le cas le plus simple et peut-être le plus utilisé. Mais il est également possible de modifier les données en mémoire, d'ajouter de nouveaux enregistrements, d'en supprimer d'autres, et bien sûr de répercuter ces modifications vers la source de données originelle. Ces modifications sont triviales à effectuer en Java ...

// Positionnement absolu // (un déplacement relatif est également possible) crset.absolute(1); // Modification en mémoire crset.updateInt("age", 26); // mise à jour de l'âge d'un gourou crset.updateRow(); // Insertion d'un nouvel enregistrement crset.insertRow(); crset.updateString("nom", "Roques"); crset.updateString("prenom", "Pascal"); crset.updateInt("age", 35); // Suppression d'un enregistrement existant crset.deleteRow(); // Répercussions sur la source de données crset.acceptChanges(); ModificationRowSet.java

... comme en C# (utilisant un DataSet typé ici) :

// Modification en mémoire gds.Gurus[2].Age = 26; // mise à jour de l'âge d'un gourou gds.AcceptChanges(); // Insertion d'un nouvel enregistrement GuruDataSet.GurusRow row = gds.Gurus.NewRow(); row.Nom = "Roques"; row.Prenom = "Pascal"; row.Age = 35; gds.Gurus.AddGurusRow(row); // Suppression d'un enregistrement existant gds.Gurus[2].Delete(); // Répercussions sur la source de données crset.acceptChanges(); ModificationRowSet.cs

Mais attention, la répercussion des modifications vers la source de données est une tâche bien plus complexe que ne veulent bien le laisser paraître ces exemples de code. En effet, il faut gérer les cas délicats dans lesquels plusieurs personnes travaillent en parallèle sur différents DataSet / RowSet représentant les mêmes données sources ! Nous n'entrerons pas dans les détails ici, mais sachez qu'il est tout à fait possible de récupérer sous forme d'exceptions les problèmes de fusion de données et de les gérer au cas par cas.

Débat : connecté ou déconnecté

Nous disposons dans JDBC et dans ADO.NET de deux moyens d'accès aux données : l'un en mode connecté (ResultSet ou DataReader), l'autre en mode déconnecté (RowSet ou DataSet). Toute la finesse réside dans les critères de choix : dans quel contexte faut-il préférer un mode déconnecté à un mode connecté ?

Architectures en mode déconnecté

Architecture simple, données en lecture seule

On peut tout à fait envisager d'utiliser un DataSet/RowSet dans le cas d'un système disposant de peu de clients simultanés (clients légers utilisant ASP.NET ou Servlets/JSP côté serveur, ou encore clients lourds utilisant WindowsForms ou JavaSwing), à condition que les accès aux données se fassent en lecture seule. En effet, dans ce cas, il n'existe aucun risque d'incohérence ni aucun problème de fusion entre les copies des données utilisées par chaque client en parallèle.

L'intérêt d'utiliser un DataSet/RowSet dans ce cas est bien entendu la simplicité de mise en oeuvre : bâtir un client lourd ou léger qui affiche le contenu d'un DataSet est incroyablement simplifié par VisualStudio.NET (tout peut être graphique !), et l'on peut même aller jusqu'à gérer la pagination sur un DataSet volumineux.

La contrainte, bien entendu, sera l'occupation mémoire : si chaque client effectue des requêtes de sélection différentes, il faudra maintenir autant de DataSet/RowSet que de clients simultanés :

Architecture complexe, données en lecture-écriture

Nos applications font rarement des accès exclusivement en lecture seule. Dans notre exemple de mode déconnecté, si un client souhaite mettre à jour les informations qu'il visualise, il a deux possibilités :

Datamining simple, traitement désynchronisé des informations

Il est un domaine moins intuitif, peut-être moins fréquemment rencontré, dans lequel nos DataSet/RowSet peuvent également tirer leur épingle du jeu : un client qui visualiserait des informations sous différents angles. On peut imaginer un utilisateur passionné de bourse qui souhaite télécharger le soir (après clôture de la cotation à la bourse de Paris) les cours de toutes les actions qu'il surveille ainsi que toutes les informations nécessaires à ses analyses techniques journalières.

Son logiciel boursier pourrait très bien être implémenté en WindowsForms et télécharger vers 19h00 un DataSet contenant toutes les informations nécessaires (l'historique des actions, OPCVM ou Warrants à surveiller), et calculer côté client tous les indicateurs techniques permettant au boursicoteur de faire son analyse (MACD, Stochastique, RSI, etc...). Sur chaque action, l'utilisateur pourrait demander à son logiciel de zoomer sur une période du passé, faire des comparatifs d'évolution de l'action par rapport à un indice tel que le Cac 40, ou même des estimations automatiques permettant d'avoir des pronostics d'évolution des cours afin de prendre position. Il pourrait également demander à afficher les cours sous la forme de courbes, de chandeliers japonais, etc... tout cela sans avoir à rester connecté au réseau, et sans subir d'aller-retours incessants entre client et serveur : le DataSet permet au boursicoteur de prendre "un cliché d'une vue de la base d'informations boursières".

Côté serveur, on imagine sans peine un WebService qui renvoit au format XML le DataSet contenant les informations utiles au logiciel boursier client; cette application serait très simple à mettre en oeuvre.

 

Anti-pattern de l'usage d'un cache de données

Imaginez un site Web (mettons www.dotnetguru.org par exemple) sur lequel l'affluence est très importante ( ;-) ). Sur ce site, vous pouvez visualiser les statistiques de fréquentation, les horaires pendant lesquels il y a le plus de sollicitations... toutes ces informations étant agrégées dans une page dynamique. Afin d'améliorer les performances du site, le plus simple ne serait pas d'utiliser un cache de données mais plutôt un cache de pages. Dans le cas d'ASP.NET,  l'attribut OutputCache serait positionné à un durée de validité donnée de manière à ce que les informations soient proches de la réalité. De cette manière, non seulement la base de données ne serait pas sollicitée, mais l'applicatif non plus : on pourrait court-circuiter toute la chaîne et renvoyer à l'utilisateur, pendant une heure, la même page HTML.

Architectures en mode connecté

Architecture simple, mais montée en charge importante

Dans le cas d'un nombre de clients simultanés très importants, il faut veiller à l'occupation mémoire de nos systèmes. L'usage d'un cache de données (surtout côté serveur) devient prohibitif. Le deuxième argument qui va à l'encontre de la notion de DataSet / RowSet dans ce contexte est que le nombre d'accès concurrent aux données augmente avec le nombre d'utilisateurs. La gestion des fusions entre les caches et la source de données originelle risque de rendre le système inutilisable : beaucoup de conflits entraîneront rapidement une dégradation des performances, et de nombreuses erreurs remontées jusqu'aux interfaces utilisateur; l'utilisabilité du logiciel en pâtira.

Dans ce cas, il vaut mieux disposer de plus de flexibilité liée  à la gestion des transaction et à l'occupation mémoire. Utiliser un DataReader / ResultSet est donc plus conseillé, associé aux techniques avancées telles que les procédures stockées, les pools de connexion, les transactions explicites que nous nous proposons d'aborder dans les sections suivantes. 

Bien entendu, dans la réalité, une analyse beaucoup plus précise doit être réalisée afin de définir le meilleur compromis possible. 

Gestion des transactions

Bien entendu, JDBC et ADO.NET permettent de piloter (très finement) les transactions lors de l'exécution de code SQL sur notre base de données. Cela suppose bien entendu que le support transactionnel soit intégré à la Base de données cible; on pourra noter certaines divergences entre les niveaux d'isolation transactionnelle selon les bases.

La notion de transaction se gère au niveau de la connexion dans les deux API. Par défaut, dans les deux cas, les connexions se placent en mode auto-commit : chaque déclenchement de requête SQL se fait dans une nouvelle transaction, qui est close dès la fin de l'exécution de notre requête. Mais pour des raisons de performances ou des besoins d'isolation transactionnelle, vous aurez très souvent à débrayer ce mode automatique et passer en commit manuel. Un petit exemple ?

En Java:

// Supposons que la connexion "cnx" soit ouverte // Passage en mode "transactions explicites" // et début d'une transaction cnx.setAutoCommit(false); // Exécuter une PreparedStatement dans le cadre de notre transaction PreparedStatement stmt = cnx.prepareStatement ("INSERT INTO Gurus (Nom, Prenom, Age) VALUES (?, ?, ?)"); try { stmt.setString("Nom", "Jaber"); stmt.setString("Prenom", "Sami"); stmt.setInt("Age", 28); stmt.executeUpdate(); stmt.setString("Nom", "Gil"); stmt.setString("Prenom", "Thomas"); stmt.setInt("Age", 25); stmt.executeUpdate(); stmt.setString("Nom", "Roques"); stmt.setString("Prenom", "Pascal"); stmt.setInt("Age", 35); stmt.executeUpdate(); cnx.Commit(); System.out.println("Les enregistrements ont été écrits en base."); } catch (SQLException ex) { System.err.println("SQLException: " + ex.getMessage()); if (cnx != null) { try { System.err.print("Aucun enregistrement n'a été écrit en base."); cnx.rollback(); } catch (SQLException excep) { System.err.print("SQLException: "); System.err.println(excep.getMessage()); } } } finally { try{ if (cnx != null){ cnx.close(); } }catch(Exception e){ // Gestion de l'impossibilité de fermer la connexion } } TransactionsExplicites.java

Et en C# :

// Supposons que la connexion "cnx" soit paramétrée cnx.Open(); // Début d'une transaction explicite OleDbTransaction trans = cnx.BeginTransaction(); // Exécuter une commande dans le cadre de notre transaction OleDbCommand cmd = new OleDbCommand ("INSERT INTO Gurus (Nom, Prenom, Age) VALUES (?, ?, ?)", cnx); cmd.Transaction = trans; try { cmd.Parameters.Add(new OleDbParameter("Nom", "Jaber")); cmd.Parameters.Add(new OleDbParameter("Prenom", "Sami")); cmd.Parameters.Add(new OleDbParameter("Age", 28)); cmd.ExecuteNonQuery(); cmd.Parameters.Clear(); cmd.Parameters.Add(new OleDbParameter("Nom", "Gil")); cmd.Parameters.Add(new OleDbParameter("Prenom", "Thomas")); cmd.Parameters.Add(new OleDbParameter("Age", 25)); cmd.ExecuteNonQuery(); cmd.Parameters.Clear(); cmd.Parameters.Add(new OleDbParameter("Nom", "Roques")); cmd.Parameters.Add(new OleDbParameter("Prenom", "Pascal")); cmd.Parameters.Add(new OleDbParameter("Age", 35)); cmd.ExecuteNonQuery(); trans.Commit(); Console.WriteLine("Les enregistrements ont été écrits en base."); } catch (Exception e) { trans.Rollback(); Console.WriteLine(e.ToString()); Console.WriteLine("Aucun enregistrement n'a été écrit en base."); } finally { cnx.Close(); } TransactionsExplicites.cs

Nous disions en début de chapitre que JDBC 3.0 permettait de positionner des marqueurs de transactions, les "SavePoints", qui tolèrent l'annulation partielle des opérations entreprises dans la transaction (rollback() partiel); voici comment, en reprenant le code des exemples précédents :

cnx.setAutoCommit(false); PreparedStatement stmt = cnx.prepareStatement ("INSERT INTO Gurus (Nom, Prenom, Age) VALUES (?, ?, ?)"); // Nous passons sous silence la gestion des exception stmt.setString("Nom", "Jaber"); stmt.setString("Prenom", "Sami"); stmt.setInt("Age", 28); stmt.executeUpdate(); // Enregistrement d'un SavePoint Savepoint svpt1 = cnx.setSavepoint("SAVEPOINT_1"); stmt.setString("Nom", "Gil"); stmt.setString("Prenom", "Thomas"); stmt.setInt("Age", 25); stmt.executeUpdate(); // Enregistrement d'un SavePoint Savepoint svpt2 = cnx.setSavepoint("SAVEPOINT_2"); stmt.setString("Nom", "Roques"); stmt.setString("Prenom", "Pascal"); stmt.setInt("Age", 35); stmt.executeUpdate(); // Annulation partielle des modifications cnx.rollback(svpt1); System.out.println("Seul le premier enregistrement a été écrit en base."); // Validation des modifications antérieures au SavePoint cnx.commit(); cnx.setAutoCommit(true); UtilisationSavePoints.java

Et bien entendu, nous trouvons la même notion dans le framework ADO.NET :

OleDbTransaction trans = cnx.BeginTransaction(); OleDbCommand cmd = new OleDbCommand ("INSERT INTO Gurus (Nom, Prenom, Age) VALUES (?, ?, ?)", cnx); cmd.Transaction = trans; // Nous passons sous silence la gestion des exception cmd.Parameters.Add(new OleDbParameter("Nom", "Jaber")); cmd.Parameters.Add(new OleDbParameter("Prenom", "Sami")); cmd.Parameters.Add(new OleDbParameter("Age", 28)); cmd.ExecuteNonQuery(); // Enregistrement d'un SavePoint trans.Save("SAVEPOINT_1"); cmd.Parameters.Clear(); cmd.Parameters.Add(new OleDbParameter("Nom", "Gil")); cmd.Parameters.Add(new OleDbParameter("Prenom", "Thomas")); cmd.Parameters.Add(new OleDbParameter("Age", 25)); cmd.ExecuteNonQuery(); // Enregistrement d'un SavePoint trans.Save("SAVEPOINT_2"); cmd.Parameters.Clear(); cmd.Parameters.Add(new OleDbParameter("Nom", "Roques")); cmd.Parameters.Add(new OleDbParameter("Prenom", "Pascal")); cmd.Parameters.Add(new OleDbParameter("Age", 35)); cmd.ExecuteNonQuery(); // Annulation partielle des modifications trans.Rollback("SAVEPOINT_1"); Console.WriteLine("Seul le premier enregistrement a été écrit en base."); // Validation des modifications antérieures au SavePoint trans.Commit(); UtilisationSavePoints.cs

Pool de connexions

Lorsque l'accès à une base de données est très sollicité, il devient prohibitif d'ouvrir et de refermer systématiquement les connexions entre code applicatif et base. En effet, l'établissement d'une connexion est un processus très coûteux qui risque de devenir un goulot d'étranglement pour votre application.

Comme souvent dans les applications orientées objet, la gestion fine du recyclage des connexions passe par l'utilisation d'un "pool", c'est-à-dire un ensemble de connexions à la base de données que l'on maintient en permanence, et qui sont affectées dynamiquement à la portion de code applicatif qui en fait la demande.

La notion de pool de connexion est implémentée en JDBC comme en ADO.NET. Attention toutefois : tous les drivers JDBC n'implémentent pas cette partie de la spécification, ou du moins pas complètement. En fonction de votre application, assurez-vous bien que le driver que vous souhaitez utiliser dispose bel et bien d'un pool de connexion suffisamment paramétrable pour vos besoins.

Pool de connexions JDBC avec JNDI

Techniquement, on préconise en Java de configurer la connexion à une base de données (et donc le paramétrage du pool de connexion) dans un annuaire logiquement séparé des application clientes. Celles-ci pourront récupérer le paramétrage dans l'annuaire via l'interface JNDI (Java Naming Directory Interface), ce qui évite les incohérences et permet de re-localiser la base de données ou parfaire son paramétrage de connexion sans impact sur les applications utilisatrices.

Le code correspondant à la récupération (par JNDI) d'une référence sur un pool de connexion est très simple :

// Racine de l'annuaire Context ctx = new InitialContext(); // Récupération d'une référence au pool (objet de type DataSource) DataSource ds = (DataSource)ctx.lookup("jdbc/GuruBD"); Connection cnx = ds.getConnection("tom", "héhéhé"); UtilisationPoolConnexions.java

Et comme vous pouvez le constater, on récupère finalement une référence à une connexion tout à fait normale, à partir de laquelle on peut piloter les transaction, préparer nos Statements et exécuter nos requêtes SQL. L'impact sur le reste du code est donc absolument nul, sauf en termes de performances : lorsque vous invoquerez la méthode "cnx.close();", la connexion ne sera pas fermée physiquement, mais simplement rendu au pool. Elle pourra dès lors être réutilisée par un autre objet qui fait à son tour la demande "ds.getConnection(...);".

Bien entendu, en fonction du paramétrage du pool, on pourra tolérer plus ou moins de connexions simultanées ouvertes, paramétrer le pas d'incrémentation du nombre de connexions du pool, ainsi que le délai d'inactivité après lequel le pool pourra se redimensionner en fermant un certain nombre de connexions devenues inutiles.

Pool de connexions ADO.NET

La notion étant exactement la même, nous allons nous focaliser sur les différences techniques entre un pool ADO.NET et la DataSource JDBC.

En réalité, l'utilisation d'un pool de connexions est automatique en ADO.NET puisque les providers OLE-DB et SQL Server inclus dans le framework .NET gèrent cet aspect de manière transparente. Comment ? Tout simplement en associant un pool de connexion à chaque chaîne de connexion (ConnectionString) utilisée par nos applications ! Pour peu que vous ouvriez deux connexions en utilisant la même chaîne, elles seront automatiquent gérées en pool :

string cnxString = "Provider=SQLOLEDB;Data Source=localhost;" + "Initial Catalog=DNG;Integrated Security=SSPI;"; OleDbConnection cnx = new OleDbConnection (cnxString); cnx.Open(); OleDbConnection cnx2 = new OleDbConnection (cnxString); cnx2.Open(); OleDbConnection cnx3 = new OleDbConnection (cnxString); cnx3.Open(); // Le pool contient 0 connexion, l'application en détient 3, ouvertes cnx3.Close(); cnx.Open(); cnx2.Close(); // Le pool contient 3 connexions libres, l'application 0 cnx = new OleDbConnection (cnxString); // Le pool contient 2 connexions libres, l'application 1 // Aucune connexion n'a été ouverte par la dernière instruction UtilisationPoolConnexions.cs

Par défaut, le pool aura un nombre de connexions initial de 0 et maximal de 100. Vous pouvez tout à fait modifier ce paramétrage en passant vos valeurs dans la chaîne de connexion elle-même :

cnx.ConnectionString = "Provider=SQLOLEDB;Data Source=localhost;" + "Initial Catalog=DNG;Integrated Security=SSPI;" + "Max Pool Size=50;Min Pool Size=10"; ParametresPoolConnexions.cs

Conclusion

JDBC et ADO.NET ont des fonctionnalités comparables. Ces deux API ont évolué en parallèle, en introduisant progressivement les mêmes concepts de part et d'autre (ResultSets ou RecordSets scrollables et modifiables sur place, RowSets ou DataSets pour les architectures en mode déconnecté, la notion de pool de connexion, le pilotage fin des transactions...).

Ni JDBC ni ADO.NET n'imposent d'architectures techniques. Elles peuvent toutes deux s'utiliser :

Nous pourrions même imaginer que la sérialisation d'un DataSet ADO.NET puisse être désérialisé dans un RowSet JDBC afin de réduire encore davantage le gap existant entre ces deux frameworks. Des amateurs ?

 

Auteur : Thomas GIL

Copyright : DotNetGuru ©

 

Ressources

Moteur de recherche des drivers JDBC : http://industry.java.sun.com/products/jdbc/drivers

ODBC .NET Data Provider : http://msdn.microsoft.com/downloads