Principes de réalisation d'un DAC indépendant du SGBD en .NET 2.0

Didier Danse

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Principes de réalisation d'un DAC indépendant du SGBD en .NET 2.0

- Auteur : Didier Danse

- Parution : 14 décembre 2005

- Mise à jour : 14 décembre 2005

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2005 Danse Didier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Présentation

- II. Principes (en .NET 1.1)

- II-A. Définition simplifiée d'une interface

- II-B. Définition de la classe fabrique d'objets

- II-C. Utilisation

- III. L'utilisation en .NET 2.0

- III-A. Hiérarchie des classes et des interfaces

- III-B. Nouvelles classes intégrées au Framework 2.0

- III-C. Où sont stockées les informations sur les providers?

- III-D. Énumération des providers installés

- III-E. Utilisation

- III-F. La classe DbConnectionStringBuilder

- IV. Conclusion

I - Présentation

De plus en plus, les applications doivent être utilisables avec toute une série d'éléments extérieurs. Comme principaux éléments, on peut bien entendu citer les bases de données. Le changement de support au niveau de la base de données ne peut en aucun cas nécessiter une réécriture complète du code. C'est pour cette raison qu'il est préférable de passer par une couche intermédiaire pour accéder aux données. Cette fonctionnalité, nous allons la décrire ici, que ce soit pour le framework 1.1 ou pour le 2.0.

Cet article est axé principalement sur le framework 2.0. Cependant, pour améliorer la compréhension du fonctionnement, des explications sont données sur les techniques utilisées en 1.1. Pour plus de détails sur la manière de créer un accès aux données indépendant du provider, je vous renvoie vers https://nx.developpez.com/articles/dac/

Vous pouvez télécharger ce tutoriel au format PDF à l'adresse suivante: https://ditch.developpez.com/dotnet/factories/pdf/factories.pdf

II - Principes (en .NET 1.1)

II-A - Définition simplifiée d'une interface

Une interface est en quelque sorte un contrat que doivent respecter les classes qui l'implémentent. L'objectif est de signaler qu'une classe doit implémenter des méthodes dont le nom est décrit dans le contrat. Ainsi, cette classe doit pouvoir effectuer certaines opérations, quelle que soit la manière de les réaliser. De plus, lors de la compilation, si une classe ne propose pas les fonctionnalités du « contrat » qu'elle est censée respecter, le compilateur renverra une erreur.

Prenons un exemple de la vie de tous les jours : certaines personnes sont des développeurs, d'autres des pilotes de formule1…

Quelles que soient notre occupation et notre formation, nous sommes tous des personnes. C'est pourquoi nous allons imaginer deux classes (Développeur et PiloteF1) qui doivent implémenter l'interface Personne. Par ailleurs, le Pilote de F1 est également un pilote, nous allons donc créer l'interface correspondant à sa fonction.

Les interfaces :

public interface Personne
{
string Nom ;
string Prénom ;
&#8230;

public char[2] getInitiales() ;
}

public interface Pilote
{

public string getEcurie() ;
}

Les classes qui implémentent ces interfaces :

public class Développeur : Personne
{
string Nom ;
string Prénom ;
HashTable LangagesConnus ;
&#8230;
public char[2] getInitiales() ;
}

public class PiloteF1 : Personne, Pilote
{
string Nom ;
string Prénom ;
string Ecurie;
&#8230;
public char[2] getInitiales() ;
public string getEcurie() ;
}

On pourrait ainsi comparer une interface avec une classe abstraite pour laquelle aucune implémentation n'a été réalisée.

II-B - Définition de la classe fabrique d'objets

Pour réaliser notre couche d'accès aux données, il est nécessaire de passer par les interfaces. Effectivement, grâce à celles-ci nous connaissons les méthodes implémentées et nous sommes sûrs qu'elles sont bel et bien implémentées. C'est pourquoi il suffit (note de l'auteur: « si, si, il suffit… ») de créer une classe implémentant des méthodes statiques. Ces méthodes retournent des objets qui implémentent les interfaces. Pour savoir quel type d'objets doivent être renvoyés, nous passerons un paramètre aux différentes méthodes utilisées. Il existe bien entendu toute une série de moyens pour éviter de passer le paramètre à chaque méthode.

Certains se disent alors « Oui mais alors il faut passer de nouveau le code en revue pour y mettre le nom de l'objet désiré ». Évidemment, le problème serait le même, c'est pourquoi il est bon de mettre le nom dans le Web.Config dans le cadre d'asp.NET ou le App.config pour les WinForms.

Premier exemple: SqlConnection, OleDbConnection, OracleConnection et bien d'autres implémentent toute l'interface IDbConnection.

public class DbTemplate
{
public static IDbConnection Connection(string sType)
{
IDbConnection cn=null;
switch(sType)
{
case "SqlServer": cn=new System.Data.SqlClient.SqlConnection();
break;
case "Access": cn=new System.Data.OleDb.OleDbConnection();
break;
//...
}
return cn;
}

public static IDbCommand Command(string sType)
{
IDbCommand cmd=null;
switch(sType)
{
case "SqlServer": cmd=new System.Data.SqlClient.SqlCommand();
break;
case "Access": cmd=new System.Data.OleDb.OleDbCommand();
break;
//...
}
return cmd;
}
&#8230;
}

On pourrait bien entendu ajouter d'autres méthodes renvoyant d'autres types d'objets. Il ne reste plus qu'à l'utiliser de la manière suivante :

IDbConnection Conn=DbTemplate.Connection("SqlServer");
IDbCommand Cmd=DbTemplate.Command("SqlServer");
IDataReader dr = DbTemplate.DataReader("SqlServer");

Cmd.Connection=Conn;
&#8230;
dr = Cmd.ExecuteReader() ;
&#8230;

II-C - Utilisation

Avec l'utilisation d'une clé dans le Web.Config (asp.NET) :

using System.Configuration ;
&#8230;

string DbType = ConfigurationSettings.AppSettings["DbType"] ;*

IDbConnection Conn=DbTemplate.Connection(DbType);
IDbCommand Cmd = DbTemplate.Command(DbType);
IDataReader dr = DbTemplate.DataReader(DbType);

Cmd.Connection=Conn;
&#8230;
dr = Cmd.ExecuteReader() ;
&#8230;

On notera que cette technique nécessite d'ajouter manuellement les différents providers dans la classe explicitée précédemment.

Certes, cette technique permet de mettre à jour uniquement la couche d'accès aux données, ce qui est plus efficace que de devoir mettre à jour tous les accès aux données.

III - L'utilisation en .NET 2.0

III-A - Hiérarchie des classes et des interfaces

Comme dans le cas de l'utilisation en .NET 1.1, les différents providers doivent implémenter des interfaces. C'est grâce à ces interfaces qu'il est possible de réaliser une telle organisation.

III-B - Nouvelles classes intégrées au Framework 2.0

Le framework 2.0 inclut toute une série de nouvelles classes. Parmi celles-ci, deux nous intéressent tout particulièrement:

La première est DbProviderFactory. Il s'agit de la classe permettant de créer des objets Command, Connection ainsi que tous ceux qui s'y rapportent. Nous verrons cela dans le paragraphe consacré à l'utilisation de la classe DbProviderFactory.

Cette première classe est obtenue à l'aide d'une seconde, DbProviderFactories. Concrètement, c'est elle qui renvoie les objets des différents providers selon le paramètre qui lui est passé. C'est pourquoi les méthodes de cette classe sont statiques (comme l'étaient les méthodes de notre classe fabrique en 1.1).

III-C - Où sont stockées les informations sur les providers?

La liste des providers disponibles est accessible dans le machine.config situé dans le répertoire $winapp$\Microsoft.NET\Framework\v2.0.50727\CONFIG.

En voici un extrait :

<system.data>
<DbProviderFactories>
<add name="Odbc Data Provider" invariant="System.Data.Odbc" description=".Net _
Framework Data Provider for Odbc" type="System.Data.Odbc.OdbcFactory, System.Data, _
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<add name="OleDb Data Provider" invariant="System.Data.OleDb" description=".Net _
Framework Data Provider for OleDb" type="System.Data.OleDb.OleDbFactory, System.Data, _
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<add name="OracleClient Data Provider" invariant="System.Data.OracleClient" _
description=".Net Framework Data Provider for Oracle" _
type="System.Data.OracleClient.OracleClientFactory, System.Data.OracleClient, _
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<add name="SqlClient Data Provider" invariant="System.Data.SqlClient" description=_
".Net Framework Data Provider for SqlServer" type="System.Data.SqlClient.SqlClientFactory, _
System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<add name="SQL Server CE Data Provider" invariant="Microsoft.SqlServerCe.Client" description=_
".NET Framework Data Provider for Microsoft SQL Server 2005 Mobile Edition" _
type="Microsoft.SqlServerCe.Client.SqlCeClientFactory, Microsoft.SqlServerCe.Client, _
Version=9.0.242.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" />
</DbProviderFactories>
</system.data>

III-D - Énumération des providers installés

Bien entendu, il n'est pas nécessaire d'utiliser les API pour accéder aux valeurs du machine.config. La classe DbProviderFactories contient une méthode permettant de récupérer la liste des providers installés sur la machine, et ce, dans un DataTable. Vous pouvez ainsi afficher ces données dans un GridView par exemple ou encore effectuer des opérations dessus.

protected sub Page_Load()
{
DataTable dt = DbProviderFactories.GetFactoryClasses();
gridview.DataSource = dt;
gridview.DataBind();
}

Ainsi, on peut manipuler le DataTable afin de vérifier l'existence d'un provider. Cette méthode le permet :

if (DbProviderFactories.GetFactoryClasses.Select("InvariantName='" + invariantName & "'").Length = 0)
Response.Write(invariantName + "n'existe pas");

III-E - Utilisation

Comme on peut le voir dans l'exemple suivant, l'utilisation des classes DbProviderFactory et DbProviderFactories ressemble fortement à l'utilisation de notre objet DbTemplate en .NET 1.1.

IDataReader dbreader = null;

DbProviderFactory dbfactory =
DbProviderFactories.GetFactory(
ConfigurationSettings.AppSettings.Get("Provider"));

IDbConnection dbconn = dbfactory.CreateConnection();
dbconn.ConnectionString = ConfigurationSettings.AppSettings.Get("ConnectionString");
try
{
dbconn.Open();
IDbCommand dbcomm = dbconn.CreateCommand();
dbcomm.Connection = dbconn;
dbcomm.CommandText = "SELECT * FROM Clients";
dbreader = dbcomm.ExecuteReader(CommandBehavior.CloseConnection);
while (dbreader.Read())
{
Console.WriteLine("Client ID:{0}",
dbreader["ClientId"].ToString());
}
}
finally
{
if (dbreader != null) dbreader.Dispose();
}

Pour l'exemple précédent, il est nécessaire d'inclure deux namespaces :

using System.Data.Common;
using System.Configuration;

III-F - La classe DbConnectionStringBuilder

Même si cet objet n'est pas indispensable, il en est pour le moins intéressant. Il permet de diminuer le risque d'erreur lorsque vous écrivez votre ConnectionString.

Son utilisation est simple comme on peut le voir sur l'exemple suivant :

String connectionString = String.Empty;

//Get serverName from the user

SqlConnectionStringBuilder conStrbuilder = new SqlConnectionStringBuilder();
conStrbuilder.DataSource = serverName;
conStrbuilder.UserID = uid;
conStrbuilder.Password = pwd;

SqlConnection c = new SqlConnection (conStrbuilder.ConnectionString);

Toutes les propriétés ont une valeur par défaut. MinPoolSize est à 0 par exemple.

L'utilisation de cet objet permet de vérifier s’il n'y a pas d'erreurs lors de la compilation en lieu et place de les avoir à l'exécution.

IV - Conclusion

Nous venons de voir comment rendre une application indépendante d'un provider et le fonctionnement des objets utilisés. J'espère que cet article vous permettra d'élargir l'utilité de vos applications.

Remarque: l'utilisation d'une classe fabrique n'est pas « la » solution dans tous les cas. Effectivement, les différents systèmes de bases de données n'implémentent pas tous de la même manière le langage SQL. Il s'agit donc tout de même de réécrire les requêtes pour certaines de ces bases de données.

Il faudrait, pour être certain d'avoir des applications nécessitants le moins de modifications possibles lors du portage de la connexion d'une base de données vers une autre, réaliser une classe fabrique pour les requêtes SQL.

On notera également que l'utilisation des procédures stockées permet dans bien des cas de résoudre les problèmes. Malheureusement, tous les SGBD n'implémentent pas le support des procédures stockées.

Merci à David Pédehourcq pour l'aide apportée pour la réalisation de cet article.