Accès aux données générique avec Linq To SQL et C#

25. February 2010 by Pierre-Luc.ROUAYS

Accès aux données générique avec Linq To SQL et C#

Introduction :

Cet article permet de construire une couche d’accès aux données générique en utilisant LINQ To SQL. Ceci permettra par la suite d’effectuer les opérations basiques du CRUD. Nous créerons une classe nommée EntityCrud qui implémentera ces fonctionnalités.

Vous pourrez par la suite créer des Adapters pour chacune de vos classe dérivant ce cette classe.

 

Place à l’action !

Nous allons créer une simple solution contenant 2 projet : le programme et la couche d’accès aux données (DAL).

 

 

Ajouter un élément de type classe LINQ To SQL :

Puis ensuite ajouter les Tables à votre projet par le biais de l’explorateur de solution.

Code Projet MonProgramme.DAL

La classe EntityCrud contient tout le code nécessaire pour communiquer avec la base de donnée par le biais de LinqToSQL.

Voici la classe AdapterFactory :

 

 

using System;

using System.Collections;

using System.Collections.Specialized;

using System.Collections.Generic;

using System.Collections.ObjectModel;

using System.Linq;

using System.Data.Linq;

using System.Data.Linq.Mapping;

using System.Text;

using System.ComponentModel;  

The next section contains the namespace and class declarations. 

namespace MonProgramme.DAL

{

 

    /// <summary>

    /// Generic Interface to Database

    /// using LINQ to SQL

    /// </summary>

          public class EntityCrud

          {

 

 

Première fonction permettant de récuperer un objet par sa clé Primaire.

               /// <summary>

                   /// Select by Primary Key Value

                   /// </summary>

                   /// <typeparam name="T"></typeparam>

                   /// <param name="id">The PK value to search for</param>

                   /// <returns>Single matching PK to id</returns>

                   public static T SelectByPK<T>(String id) where T : class

                   {

                             try

                             {

                                      using (NWindDataContext context = new

                        NWindDataContext())

                                      {

                              // Récupère la table du type passé

                                                var table = context.GetTable<T>();

 

                              // Récupère le mapping de l’objet avec la Base

                                                MetaModel modelMap = table.Context.Mapping;

 

                              // Récupère les members de l’objet

                              ReadOnlyCollection<MetaDataMember> dataMembers

                              = modelMap.GetMetaType(typeof(T)).DataMembers;

 

                              // Cherche le nom du champ de la clé primaire

                              string pk = 

                              (dataMembers.Single<MetaDataMember>(m =>

                              m.IsPrimaryKey)).Name;

 

                              // Retourne l’objet unique qui correspond à cette clé primaire

                              // passée en argument

                                    return table.SingleOrDefault<T>(delegate(T t)

                                    {

                                    String memberId =

      t.GetType().GetProperty(pk).GetValue(t,

                                    null).ToString();

                                    return memberId.ToString() == id.ToString();

                                    }

      );

                                      }

                             }

                             catch (Exception)

                             {

                                      throw;

                             }

                   }

 

 

Seconde méthode permettant de récupérer toutes les objets de la base.

 

        /// <summary>

        /// Select All by Type

        /// </summary>

        /// <typeparam name="T"></typeparam>

        /// <returns>Matching table as typed list</returns>

        public static IList<T> SelectAll<T>() where T : class

        {

            try

            {

                using (NWindDataContext context = new NWindDataContext())

                {

                    // Récupère la table qui correspond au type

                    // et retourne tous les enregistrement sous forme de liste

                    var table = context.GetTable<T>().ToList<T>();

                    return table;

                }

            }

            catch (Exception)

            {

                throw;

            }

        }

 

Méthode d’insertion de donnée dans la base.

                   /// <summary>

                   /// Insert an Object into a Table

                   /// </summary>

                   /// <typeparam name="T"></typeparam>

                   /// <param name="item"></param>

                   public static void Insert<T>(T item) where T : class

                   {

                             try

                             {

                // Récupère la table du type passé en argument

                // Insert l’enregistrement et soumet les changements à la Base

                                      using (NWindDataContext context = new

                        NWindDataContext())

                                      {

                              // Récupère la table correspondante

                                                var table = context.GetTable<T>();

 

                              // Passe l’objet à la method InsertOnSubmit

                              // et Soumet les changements

                                                table.InsertOnSubmit(item);

                              context.SubmitChanges();

                                      }

                             }

                             catch (Exception)

                             {

                                      throw;

                             }

              }

 

Méthode de mise à jour de donnée.

 

       /// <summary>

                   /// Update an Existing Object

                   /// </summary>

                   /// <typeparam name="T"></typeparam>

                   /// <param name="item">The object to update</param>

                   public static void Update<T>(T item) where T : class

                   {

                             try

                             {

                        // Crée une nouvelle instance de l’objet

                                      Object newObj = Activator.CreateInstance(typeof(T),    

                        new object[0]);

 

                                      PropertyDescriptorCollection originalProps =

                        TypeDescriptor.GetProperties(item);

 

                        // Donne la valeur de la donnée passée en argument à l’objet

                                      foreach (PropertyDescriptor currentProp in

                        originalProps)

                                      {

                                                if (currentProp.Attributes[typeof(

                              System.Data.Linq.Mapping.ColumnAttribute)] !=

                              null)

                                                {

                                                          object val = currentProp.GetValue(item);

                                                          currentProp.SetValue(newObj, val);

                                                }

                                      }

 

                        // Lie l’objet à un nouveau context de donnée

                        // et soumet les changement à la Base.

                                      using (NWindDataContext context = new

                        NWindDataContext())

                                      {

                                                var table = context.GetTable<T>();

                                                table.Attach((T)newObj, true);

                                                context.SubmitChanges();

                                      }

                             }

                             catch (Exception)

                             {

                                      throw;

                             }

                   }

 

Code de suppression d’un objet.

               /// <summary>

                   /// Remove an object

                   /// </summary>

                   /// <typeparam name="T"></typeparam>

                   /// <param name="item"></param>

                   public static void Remove<T>(T item) where T : class

                   {

                             try

                             {

                        // Crée une instance de l’objet avec le type passé en argument

                                      Type tType = item.GetType();

                                      Object newObj = Activator.CreateInstance(tType, new 

                        object[0]);

 

                        // Récupère les proprieties de l’objet passé en argument

                                      PropertyDescriptorCollection originalProps =

                        TypeDescriptor.GetProperties(item);

 

                        // Copie le contenue de la donnée passée en argument

                        // au nouveau objet du meme type

                                      foreach (PropertyDescriptor currentProp in

                        originalProps)

                                      {

                                                if (currentProp.Attributes[typeof(

                              System.Data.Linq.Mapping.ColumnAttribute)] !=

                              null)

                                                {

                                                          object val = currentProp.GetValue(item);

                                                          currentProp.SetValue(newObj, val);

                                                }

                                      }

 

                        //lie l’objet à la table, appel de la function deleteOnSubmit

                        // et soumet les changement à la base.

                                      using (NWindDataContext context = new

                        NWindDataContext())

                                      {

                                                var table = context.GetTable<T>();

                              table.Attach((T)newObj, true);

                                                table.DeleteOnSubmit((T)newObj);

                                                context.SubmitChanges();

                                      }

                             }

                             catch (Exception)

                             {

                                      throw;

                             }

                   }

 

Utilisation :

Une fois l’EntityCrud créé vous pouvez l’utiliser comme dans les exemples suivant :

Exemple Insert :

           try

                             {

                             Customer c = new Customer();

                             c.CustomerID = "AAAAA";

                             c.Address = "554 Westwind Avenue";

                             c.City = "Wichita";

                             c.CompanyName = "Holy Toledo Bibles";

                             c.ContactName = "Frederick Flintstone";

                             c.ContactTitle = "Boss";

                             c.Country = "USA";

                             c.Fax = "316-335-5933";

                             c.Phone = "316-225-4934";

                             c.PostalCode = "67214";

                             c.Region = "EA";

 

                             Order_Detail od = new Order_Detail();

         od.Discount = .25f;

                             od.ProductID = 1;

                             od.Quantity = 25;

                             od.UnitPrice = 25.00M;

 

                             Order o = new Order();

                             o.Order_Details.Add(od);

                             o.Freight = 25.50M;

                             o.EmployeeID = 1;

                             o.CustomerID = "AAAAA";

 

                             c.Orders.Add(o);

 

                             EntityCrud.Insert<Customer>(c); //INSERT Customer mais aussi les autres champs qui lui sont associés (order, order_detail).

 

                             }

                             catch (Exception ex)

                             {

                                      MessageBox.Show(ex.Message);

                             }

 

Les données seront toutes insérée en base : Le Customer, L’order et l’order_detail avec seulement cet appel : EntityCrud.Insert<Customer>(c);

Exemple Delete :

                             try

                             {

                             Customer doa = EntityCrud.SelectByPK<Customer>("AAAAA");

                                     EntityCrud.Remove<Customer>(doa); //DELETE

                             }

                             catch (Exception ex)

                             {

                                      MessageBox.Show(ex.Message);

                             }

Exemple Update :

 

         try

                             {

                                      var q = EntityCrud.SelectByPK<Customer>("AAAAA");

 

                                      q.City = "Calgary";

                                      q.Country = "Canada";

                                      q.Address = "100 Longhorn Drive";

                                      q.CompanyName = "Holy Moses Bibles";

                                      q.Region = "EE";

                                      q.ContactTitle = "Gopher";

                                      q.ContactName = "Moxie Peroxide";

 

                                      EntityCrud.Update<Customer>(q);//UPDATE

                             }

                             catch (Exception ex)

                             {

                                      MessageBox.Show(ex.Message);

                             }

 

ADO.NET, ASP.NET AJAX, C#, WPF , , , ,

ASP.NET AJAX 4 : Exemple d’utilisation

8. June 2009 by Davy.Houareau

Nous allons aborder dans cet article un exemple d’utilisation de l’ASP.NET AJAX 4. Cet exemple consiste en l’affichage de produits en fonction de la catégorie sélectionnée. Tout en avançant, nous allons nous attarder sur le fonctionnement de ce nouvel aspect d’ASP.NET AJAX.

Le but de cette nouvelle approche de l’AJAX avec l’ASP.NET est de faire en sorte que le serveur se concentre plus sur l’aspect « donnée » que sur l’aspect « présentation ». En effet, il est possible de faire un site web qui interagit avec une base de données sans aucun contrôle serveur très facilement. C’est sur ce modèle que notre exemple d’utilisation portera (Client « pur » AJAX). On avait l’habitude d’utiliser un script manager couplé à un update panel pour faire de l’AJAX avec nos contrôles serveur et cette technique de rendement partiel nous rendait déjà la tâche assez facile puisqu’on avait de l’AJAX sans patauger dans le JAVASCRIPT. Maintenant, on peut faire de l’AJAX tout aussi simplement sans script manager et sans update panel. Nous verrons ce que sont les nouveaux contrôles client « DataView » et « DataContext »

1. Préparatifs (Exposition de la base de données)

Dans cet exemple nous allons utiliser un ADO.NET Data Service pour interagir avec la base de données, mais il est possible d’utiliser un ASMX ou encore WCF. Les possibilités sont multiples à conditions que le service soit compatible JSON (JavaScript Object Notation -> un format d’échange de données). Commençons par créer un nouveau projet « ASP.NET Web Application » puis ajoutons un ADO.NET Entity Data Model qui pointe sur la base de données que nous intéresse (ici Northwind). Appelons le, ADOEntityDataModel.

Maintenant nous allons pouvoir exposer cet Entity Data Model via un ADO.NET Data Service que nous appellerons MyWebDataService. Puis nous pourrons modifier MyWebDataService.svc pour pouvoir lui associer l’Entity Data Model.

Il faut modifier l’héritage en rajoutant le nom de classe de la source de données. Ici NorthwindEntities. Par défaut c’est le [nom_de_la_base_de_données+Entities]. Vous trouverez ce nom dans le fichier designer de l’Entity Data Model. Notre Entity Data Model est maintenant associé au service mais nous n’avons pas défini ce qui était exposé et de quelle manière. Pour cela nous utiliserons config.SetEntitySetAccessRule("*", EntitySetRights.All);. « * » pour dire que tout est visible et EntitySetRights.All pour autoriser la lecture et l’écriture.

Ce qui donne le code ci-dessous :

public class MyWebDataService : DataService<NorthwindEntities>

    {

        // This method is called only once to initialize service-wide policies.

        public static void InitializeService(IDataServiceConfiguration config)

        {

            config.SetEntitySetAccessRule("*", EntitySetRights.All);

        }

    }

Certes pour notre exemple la sécurité n’est pas optimale mais nous voici avec une base de données exposée assez simplement. Il ne nous reste plus qu’a nous concentrer sur l’ASP.NET AJAX 4 :).

2. Utilisation

Puisque notre exemple porte sur un client « pur » AJAX, supprimons le fichier Default.aspx et ajoutons un simple fichier html que nous nommerons categories.htm. Pour pouvoir utiliser ASP.NET AJAX 4 nous avons besoin de quelques fichiers JavaScript que vous pouvez trouver sur CODEPLEX.

3 Fichiers sont nécessaires:

  • MicrosoftAjax.js (base obligatoire)
  • MicrosoftAjaxAdoNet.js (on utilise de l’ADO.NET)
  • MicrosoftAjaxTemplates.js (pour avoir des templates où nous afficherons les données)

Ajoutons donc un répertoire « Scripts » et nos 3 fichiers JavaScript puis incluons-les dans notre page categories.htm.

Ce n’est pas tout, il nous faut aussi ajouter deux namespaces:

  • xmlns:sys="javascript:Sys"
  • xmlns:dataview="javascript:Sys.UI.DataView" (nous verrons un peu plus tard le Dataview)
  • sys:activate="*" (activer tous les éléments de la page pour le support d’attributs sys, On l’utilisera pour la sélection)

Notre page categories.html ressemble donc à ceci :

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >

<head>

    <title>Categories</title>

    <link type="text/javascript" href="Content/Scripts/MicrosoftAjax.js" />

    <link type="text/javascript" href="Content/Scripts/MicrosoftAjaxAdoNet.js" />

    <link type="text/javascript" href="Content/Scripts/MicrosoftAjaxTemplates.debug.js" />

</head>

<body xmlns:sys="javascript:Sys"

      xmlns:dataview="javascript:Sys.UI.DataView"

      sys:activate="*">

</body>

</html>

Maintenant, nous sommes prêts à aborder le template client.

Grâce à ASP.NET AJAX 4, presque n’importe quelle balise html peut devenir un template dans le lequel on pourra faire du binding. Pour qu’une balise soit reconnue en tant que template, il faut qu’elle possède l’attribut class="sys-template". Ce nom est une convention et doit être utilisé simplement parce que le moteur de rendu cherchera ce nom de classe pour manipuler le template. Il est conseillé de définir cette classe dans le CSS en tant que display:none;. Lors de l’affichage, ce style sera modifié automatiquement afin de voir le résultat.

Pour notre exemple, notre template sera une balise tbody.

    <table cellspacing="0">

        <thead>

            <tr><th colspan="2">Categories</th></tr>

            <tr><th>Name</th><th>Description</th></tr>

        </thead>

        <tbody class="sys-template">

        </tbody>

    </table>

.sys-template

{

    display: none;

}

Une fois la balise reconnue en tant que template, il ne reste plus qu’à faire du binding et il est possible d’en faire n’importe où à l’intérieur du template. Il existe deux écritures possibles : une courte, une longue. L’écriture courte se contente de rendre la valeur de la propriété de l’objet telle quelle et elle se présente de cette façon : {{ MyProperty }}. Ce type de binding se fait dans un seul sens et une seule fois (one-time binding). En ce qui concerne l’écriture longue, l’équivalent de {{ MyProperty }} est : {binding MyProperty, mode=oneWay}. L’écriture longue permet de faire du binding bidirectionnelle et d’utiliser une fonction pour « convertir » le rendu de la Valeur. Pour faire du bidirectionnel, il suffit de { binding MyProperty }. Quand on parle ici de binding bidirectionnelle, c’est au sein du même contexte.

Le template fourni également des pseudo-colonnes prédéfinies qui permettent d’accéder à des valeurs bien utiles comme $index, $dataitem, $id, $element.

  • $index : l’index de l’élément courant. (pratique un style alternatif)
  • $dataitem : l’élément courant
  • $id("ID") : créer ou accède à un identifiant unique pour l’élément en court. La valeur sera le nom spécifié + l’index (ici « ID3 » par exemple)
  • $element : récupère la balise dans laquelle ce mot clé est spécifié (ex :<div>{{$element}}</div>retournera la balise div).
C’est le bon moment pour aborder les deux nouveaux contrôles client que nous apporte ASP.NET AJAX 4 : le DataView et le DataContext.

Le DataContext est un proxy qui va communiquer avec un service sachant utiliser des données sous forme de JSON. C’est lui qui fera la liaison entre le client et le service.

Voici comment le déclarer :

<script type="text/javascript">

        var dataContext = $create(

            Sys.Data.AdoNetDataContext, { serviceUri: "MyWebDataService.svc" });

</script>

Vu que nous utilisons ADO.NET nous sommes obligés de déclarer notre DataContext de cette façon avec Sys.Data.AdoNetDataContext.

Le DataView, quant à lui, doit être associé à un template. Il peut traiter une collection d’éléments en répliquant l’instance de son template pour chaque élément de la collection. Il peut être utilisé pour un affichage sous forme de détail ou de liste. Il possède pas mal de points important pour sa configuration. Nous ne verrons que ceux dont nous aurons besoin.

Pour notre exemple voici à quoi doit ressembler notre balise tbody :

<tbody class="sys-template"

               sys:attach="dataview"

               dataview:sys-key="master"

               dataview:dataprovider="{{dataContext}}"

               dataview:autofetch="true"

               dataview:fetchoperation="Categories"

               dataview:selecteditemclass="rowSelected"

               dataview:fetchparameters="{{ {$expand: 'Products'} }}"

               dataview:initialselectedindex="0">

</tbody>

Quelques explications s’imposent non ?

  • sys:attach="dataview" : permet de déclarer le DataView
  • dataview:sys-key="master" : permet d’identifier le DataView (ici notre DataView s’appelle master)
  • dataprovider="{{dataContext}}" : permet de binder le DataView au DataContext déclaré plus haut.
  • dataview:autofetch="true" : récupérer les données au chargement de la page.
  • dataview:fetchoperation="Categories" : La fonction du service à appeler pour récupérer les données
  • dataview:selecteditemclass="rowSelected" : style pour la ligne sélectionnée.
  • dataview:fetchparameters="{{ {$expand: 'Products'} }}" : fetchparameters pour passer des paramètres à la requête et ici $expand permet de récupérer les éléments de la propriété de navigation « Products » de l’Entity Set « Categories » (Ce qui nous servira pour afficher les produits des catégories plus tard).
  • dataview:initialselectedindex="0" : Présélectionner un élément par défaut.

Il nous manque maintenant le binding. Vu que nous n’avons pas besoin ici de binding bidirectionnel, nous utiliserons l’écriture courte.

Voici à quoi notre binding ressemble :

<tr sys:command="select" class:alternaterow="{{ $index % 2 != 0 }}">

    <td>

        {{ CategoryName }}

    </td>

    <td>

        {{ Description }}

    </td>

</tr>

Notez la présence deux attributs « spéciaux » :

  • sys:command="select" : permet la sélection de l’élément.
  • class:alternaterow="{{ $index % 2 != 0 }}" : petite astuce pour avoir un style alternatif. Le style ici sera alternaterow.

N’oublions pas de rajouter ces classes dans le style :

.rowSelected

{

    color:Red;

}

.alternaterow

{

    background-color:#A3B8FF;

}

Il ne nous reste plus qu’a nous occuper de l’affichage des produits. Là aussi, il nous faut utiliser un DataView que nous lierons au premier.

Voici le DataView pour l’affichage des produits :

<table cellspacing="0">

        <thead>

            <tr>

                <th colspan="2">

                    Products

                </th>

            </tr>

            <tr>

                <th>

                    Name

                </th>

                <th>

                    Price

                </th>

            </tr>

        </thead>

        <tbody id="product-template" class="sys-template" sys:attach="dataview" dataview:data="{binding selectedData.Products, source={{master}} }">

            <tr>

                <td>

                    {binding ProductName}

                </td>

                <td>

                    {binding UnitPrice}

                </td>

            </tr>

        </tbody>

    </table>

Ici, la seule nouveauté c’est : dataview:data="{binding selectedData.Products, source={{master}} }" : Tout simplement pour indiquer que les données qui vont être utilisées par ce DataView proviennent de l’élément sélectionné dans le DataView « master ».

Et nous voici au résultat tant attendu :

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<head>

    <title>Categories</title>

    <link href="Style/style.css" rel="stylesheet" type="text/css" />

    <script src="Scripts/MicrosoftAjax.js" type="text/javascript"></script>

    <script src="Scripts/MicrosoftAjaxTemplates.js" type="text/javascript"></script>

    <script src="Scripts/MicrosoftAjaxAdoNet.js" type="text/javascript"></script>

    <script type="text/javascript">

        var dataContext = $create(Sys.Data.AdoNetDataContext, { serviceUri: "MyWebDataService.svc" });

    </script>

    <style type="text/css">

        .sys-template

        {

            display: none;

        }

        .rowSelected

        {

            color:Red;

        }

        .alternaterow

        {

             background-color:#A3B8FF;

        }

    </style>

</head>

<body xmlns:sys="javascript:Sys"

      xmlns:dataview="javascript:Sys.UI.DataView"

      sys:activate="*">

       

    <table cellspacing="0">

        <thead>

            <tr>

                <th colspan="2">

                    Categories

                </th>

            </tr>

            <tr>

                <th>

                    Name

                </th>

                <th>

                    Description

                </th>

            </tr>

        </thead>

        <tbody class="sys-template"

               sys:attach="dataview"

               dataview:sys-key="master"

               dataview:dataprovider="{{dataContext}}"

               dataview:autofetch="true"

               dataview:fetchoperation="Categories"

               dataview:selecteditemclass="rowSelected"

               dataview:fetchparameters="{{ {$expand: 'Products'} }}"

               dataview:initialselectedindex="0">

            <tr sys:command="select" class:alternaterow="{{ $index % 2 != 0 }}">

                <td>

                    {{ CategoryName }}

                </td>

                <td>

                    {{ Description }}

                </td>

            </tr>

        </tbody>

    </table>

    <table cellspacing="0">

        <thead>

            <tr>

                <th colspan="2">

                    Products

                </th>

            </tr>

            <tr>

                <th>

                    Name

                </th>

                <th>

                    Price

                </th>

            </tr>

        </thead>

        <tbody id="product-template" class="sys-template" sys:attach="dataview" dataview:data="{binding selectedData.Products, source={{master}} }">

            <tr>

                <td>

                    {binding ProductName}

                </td>

                <td>

                    {binding UnitPrice}

                </td>

            </tr>

        </tbody>

    </table>

</body>

</html>

3.Conclusion

Comme vous avez pu le constater, nous avons pu réaliser un client « pure » AJAX en écrivant deux lignes de JAVASCRIPT grâce a ASP.NET AJA 4 . Evidement nous avons abordé un exemple précis mais cette nouvelle façon de faire de l’AJAX offre beaucoup d’autres possibilités d’utilisation.

ADO.NET, ASP.NET AJAX

Abstraction des Accès BDD dans une Factory en ADO .NET

Ecrit par Thomas COVILLE tutoré par Jérémie BERTRAND

    Dans le cadre du développement d’une application reposant sur l’utilisation d’une base de données, il est important de penser à la réutilisabilité de son code. Il n’est pas question de réécrire toute une application à chaque changement de moteur de BDD. Une des solution est donc de rajouter une couche intermédiaire entre la partie business et la base de donnée. Dans le pire des cas, seule cette couche est à réécrire.

    En allant plus loin, il est possible d’intégrer les accès base dans une fabrique abstraite (factory). Microsoft nous a mâché le travail, depuis le framework .Net 2.0 le namespace System.Data.Common contient tout ce qu’il nous faut pour le faire facilement. Voici rapidement les types les plus important contenus dans ce namespace:

DbProviderFactories

Conteneur de Factory. C’est à travers ce type, que l’on va pouvoir binder notre architecture abstraite à un provider spécifique.

DbProviderFactory

Type implémenté pour chaque provider, c’est à travers un objet de ce type que l’on pourra instancier tous les types du provider choisi.

DbConnection

Type gérant la connexion à la base.

DbCommand

Type permettant d’envoyer des commandes (paramétrées ou nom) dans une DbConnection.

DbDataReader

Retour de l’exécution d’une DbCommand (un peu l’équivalent des bons vieux recordsets d’antan…)

Providers

Microsoft a déjà intégré un certain nombre de providers se conformant à cette architecture :
  • System.Data.SqlClient (SQL Server)
  • System.Data.OleDb (MS Access)
  • System.Data.OracleClient (Oracle DataBase)

D’autre providers sont disponibles sur internet, pour connaître ceux dont vous disposez sur votre machine vous pouvez soit jeter un oeil dans le fichier machine.config du framework à la section <DbProviderFactories>. Soit appeler la Méthode static  GetFactoryClasses() du type DbConnectionFactory :

DataTable tab =  DbProviderFactories.GetFactoryClasses();

foreach (DataRow row in tab.Rows)
{
foreach (DataColumn col in tab.Columns)
{
System.Console.WriteLine("{0} = {1} ;", col.ColumnName, row[col]);
}
Console.WriteLine(Environment.NewLine);
}

 

utilisation des DbProviderFactories

Entrons un peu plus dans le détail maintenant que nous avons une vue générale de ce que Microsoft à mis à notre disposition. nous allons donc réaliser une Classe nommée ConnectDb  encapsulant l’utilisation des types vus précédemment :

image_thumb4 

     La classe ConnectDb contient un ensemble de méthodes statiques qui couvrent les opérations de base pour accéder à une base de données.

    Personnellement j’ai choisi de la définir en abstract puisque toutes ses méthodes sont statiques et qu’elle n’a pas vocation à être instanciée. On aurait pu mettre le constructeur par défaut en privé, le résultat serait le même, je pense que c’est surtout une question de goût…

Voyons en détail l’implémentation de cette classe.

 

 

 

Implémentation

Gestion des paramètres de connexion

    Pour ma part j’ai choisi de stocker la chaîne de connexion, ainsi que le provider choisi dans un fichier de configuration (Pour savoir comment créer votre propre fichier de configuration,vous pouvez consulter ce billet).

    Voici à quoi ressemble mon fichier App.config :

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="MyDefaultConnectionString"
connectionString="Data Source=MYSERVER\SQLEXPRESS;Initial Catalog=MyDataBase;Integrated Security=True;Pooling=False"/>
</connectionStrings>
<appSettings>
<add key="MyDefaultProvider" value="System.Data.SqlClient"/>
</appSettings>
</configuration>

     Pour mon exemple j’ai choisi d’utiliser une base de données SQL Server et le provider correspondant : Sytem.Data.SqlClient . Bien entendu, la suite du code fonctionnera avec n’importe quel provider implémentant les Types abstraits du namespace System.Data.Common.

static readonly String _connectionString;
static readonly String _provider;

static ConnectDb()
{
_connectionString = ConfigurationManager.ConnectionStrings["MyDefaultConnectionString"].ConnectionString;
_provider = ConfigurationManager.AppSettings["MyDefaultProvider"].ToString();
}

    J’ai défini un constructeur statique pour récupérer les valeurs de la chaîne de connexion et le provider choisi depuis le fichier de configuration. Un constructeur static est appelé automatiquement à la première référence du type. Il ne nécessite donc pas d’instanciation explicite .

Utilisation de la factory

    La récupération de la factory pour un provider spécifique se fait à l’aide de la classe DbProviderFactories. Pour ma part j’ai choisi de l’encapsuler dans une Propriété “Factory” :

protected static DbProviderFactory Factory
{ get
{
return DbProviderFactories.GetFactory(_provider);
}
}

    A ce stade nous disposons donc d’une instance d’un type implémentant DbProviderFactory, spécifique au provider que nous avons choisi. (dans mon cas il s’agit en interne d’une instance de  System.Data.SqlClient.SqlClientFactory).

    Parmi l’ensemble  des méthodes que nous propose l’objet DbProviderFactory , nous n’en utiliserons que deux dans le cadre de cet article :

//Crée une instance d'un objet de type DbCommand
Factory.CreateCommand();
//Crée une instance d'un objet de type DbConnection
Factory.CreateConnection();

   voyons comment les utiliser :

Connexion à la Base de données

    Rien de bien compliqué ici, on appelle la méthode CreateConnection() de la factory, puis on affecte la Chaîne de connection à l’objet DbConnection que l’on a récupéré.

protected static DbConnection GetConnection()
{
DbConnection conn = Factory.CreateConnection();
conn.ConnectionString = ConnectionString;
return conn;

}

    Attention, l’objet connexion retourné est FERME. la connexion à la base n’est donc pas active. J’ai choisit de ne pas retourné d’objet systématiquement ouvert pour ne pas risquer d’oublier de le refermer. Cela permet donc de garder le contrôle sur le pool de connexion ouvertes.

Manipulation des objets DbCommand

    Création

    Une fois la connexion à la base de donnée gérée, il ne reste plus qu’à envoyer les requêtes. C’est là que l’objet DbCommand entre en jeu. Il y a 2 façons de créer un objet commande: en utilisant la factory, ou en passant par un objet DbConnection :

  • Solution 1
public static DbCommand CreateCommand()
{
DbCommand cmd = Factory.CreateCommand();
return cmd;
}
  • Solution 2
public static DbCommand CreateCommand()
{
DbConnection conn = GetConnection();
DbCommand cmd = conn.CreateCommand();
return cmd;
}

    Pour ma part je préfère la première version. C’est encore une fois assez discutable, la raison est que je préfère garder mes objets indépendants les uns des autres le plus longtemps possible.

Envoi d’une requête

   Une fois notre Objet DbCommand créé nous pouvons lui affecter une requête simple à l’aide de la propriété CommandText (nous verrons par la suite comment faire pour créer des commandes paramétrées") :

DbCommand cmd = ConnectDb.CreateCommand();
cmd.CommandText = "SELECT Champ1, Champ2 FROM MATABLE WHERE Champ3='toto'";

   Attention : La syntaxe des requêtes SQL peut différer d’un SGBD à l’autre, c’est pourquoi, si l’on veut conserver la souplesse de notre factory, l’affectation d’une requête SQL à une DbCommand doit être effectuée EN DEHORS de la classe ConnectDb, par exemple dans une classe de DAO classique.

Nous pouvons regrouper les différents types de requête en 3 catégories :

  • Les requêtes de Sélection qui renvoient un ensemble de données (SELECT .. FROM… JOIN.. WHERE)
  • Les ordres DDL qui renverront la plupart du temps un nombre d’enregistrements affectés (Update, Delete,Insert…)
  • Les requêtes Scalaires qui retournent un champ unique dans un enregistrement unique (ex : “SELECT Count(*) FROM matable”, “SELECT ORACLE_SEQUENCE.NEXTVAL FROM dual”)

    Typiquement nous pouvons donc créer 3 méthodes de requêtage pour gérer les différents cas. Le code de ces méthodes diffère assez peu, dans tous les cas, il nous faut récupérer un objet DbConnection, l’ouvrir et l’affecter à l’objet DbCommand contenant la requête.

Comme un exemple vaut mieux qu’un long discours, voici l’implémentation de ces fonctions :

  • ExecuteQuery (requêtes de Sélection)
public static DbDataReader ExecuteQuery(DbCommand cmd)
{
DbDataReader ret;
DbConnection conn = GetConnection();
conn.Open();
cmd.Connection = conn;
cmd.cr
ret = cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection);

return ret;
}

    L’objet DbDataReader est un Curseur sur un ensemble d’enregistrement en provenance de la base de donnée en mode connecté, un peu à la manière des vieux recordset. c’est donc dans cet objet que sont renvoyés les enregistrements sélectionnés par la commande.

     Le paramètre passé à la méthodes ExecuteReader() de l’objet DbCommand est une énumération qui  permet de s’assurer que la connexion sera fermée en même temps que le DataReader. En effet, pour que ce dernier fonctionne correctement, la connexion sur laquelle il repose doit rester ouverte jusqu’au dernier moment. On ne peut donc pas fermer la connexion immediatement après la création du DataReader.

  • ExecuteNonQuery (ordres DDL)

public static int ExecuteNonQuery(DbCommand cmd)
{
int ret;
using (DbConnection conn = GetConnection())
{
conn.Open();
cmd.Connection = conn;
ret = cmd.ExecuteNonQuery();
conn.Close();
return ret;
}
}

     Ici le code est assez similaire à la méthode précédente, on notera cette fois la présence du bloc using autour de l’objet DbConnection. En effet Microsoft à eu la bonne idée d’implémenter IDisposable sur ces objets. Ainsi, dans le cas de l’objet DbConnection, on est sûr que quoiqu’il arrive, la méthode Dispose() sera appelée, et que la connexion à la base, fermée.

  • ExecuteScalar (requêtes Scalaires)
public static object ExecuteScalar(DbCommand cmd)
{
object ret;
using (DbConnection conn = GetConnection())
{
conn.Open();
cmd.Connection = conn;
ret = cmd.ExecuteScalar();
conn.Close();
return ret;
}
}

    Encore une fois, le code ne diffère pas énormément de la méthode ExecuteQuery(), la seule chose à noter est que le type de retour de la méthode est de type object. il vous faudra donc penser lors de l’utilisation, à effectuer un cast sur la valeur retournée en fonction du type que vous attendez.

    Maintenant que nous avons nos 3 méthodes, l’exécution d’une requête est simplissime :

ex:

//requête simple
DbCommand cmd = ConnectDb.CreateCommand();
cmd.CommandText = "SELECT * FROM Personne where prenom = ‘toto’";

DbDataReader reader = ConnectDb.ExecuteQuery(cmd);
while (reader.Read())
{
System.Console.WriteLine("Nom : {0} ,Prenom : {1} , mail : {2}",reader["nom"],reader["prenom"],reader["mail"]);
}
reader.Close();

     Le parcours d’un objet DbDataReader se fait grâce à la méthode booléenne Read(). A la construction de l’objet le curseur pointe AVANT le tout premier enregistrement. A chaque appel de Read(), le curseur passe à l’enregistrement suivant, si l’on est arrivé à la fin du DataReader ou si la requête n’a aucun résultat , Read() renvoi False.

    Pour accéder aux champs des enregistrement, vous pouvez soit utiliser la méthode “tableaux” à l’aide de crochets : reader[“nomColonne”] ou reader[NumColonne"], sans oublier de caster correctement les valeurs de retour.

    Ou bien encore utiliser toute les méthodes reader.GetXXXX() pour récupérer directement une valeur typée.

     Nous avons vu qu’il était très simple d’envoyer une requête à la base de données, cependant il peut parfois être utile de paramétrer une requête, c’est à dire, dissocier les valeurs des champs. Cela permet un traitement plus automatisé, plus optimisé et plus sûr des requêtes, notamment au niveau des attaques par injection de code SQL. Voyons ensemble comment écrire de telles requêtes.

Création d’une requête paramétrée

     Créer une requête paramétrée n’est pas bien plus difficile que de créer une requête sans paramètre : reprenons un objet DbCommand, et voyons quelles autres méthodes s’offrent à nous :

     Nous avons une Méthode CreateParameter() et une propriété Parameters qui contient la liste de tous les paramètres associés à cette commande.

    Avant d’associer des paramètres à une DbCommand, il faut tout d’abord référencer ces paramètres dans la requête elle même. cela se fait grâce à un mot clé choisi par vous-même, précédé d’un symbole qui indiquera au provider que la chaîne de caractère suivante est un paramètre. Ce symbole varie d’un provider à l’autre, voici un tableau récapitulant ceux que je connais :

Oracle ‘:’ + NomParametre
SqlServer ‘@’+ NomParametre
Access ‘?’+ NomParametre

    Il nous faut ensuite créer les paramètres à proprement parler et leur donner leur valeur. rien de plus simple, on crée un objet DbParameter à l’aide de la méthode CreateParameter de la DbCommand et on affecte les propriétés suivantes :

  • ParameterName : chaîne de caractères correspondant au paramètre dans la requête (sans le symbole)
  • Value : valeur à affecter au paramètre
  • DbType (facultatif) : Type à affecter au paramètre

Puis on ajoute le paramètre créé à la Liste des paramètres de la commande. A partir de là la commande s’exécute exactement comme une commande normale, à l’aide des 3 méthodes que nous avons définies plus haut.

Exemple d’une requête d’insertion: on suppose que l’on dispose d’une table personne dans notre base de données :

image1_thumb

//requête Parametrée
DbCommand cmd2 = ConnectDb.CreateCommand();
cmd2.CommandText = "INSERT INTO Personne (mail,nom,prenom)VALUES (@mail,@nom,@prenom)";
DbParameter mailParam = cmd2.CreateParameter();
mailParam.ParameterName = "mail";
mailParam.Value = "uneadressemail@kkpart.com";
cmd2.Parameters.Add(mailParam);




DbParameter nomParam = cmd2.CreateParameter();
nomParam.ParameterName = "nom";
nomParam.Value = "COVILLE";
cmd2.Parameters.Add(nomParam);



DbParameter prenomParam = cmd2.CreateParameter();
prenomParam.ParameterName = "prenom";
prenomParam.Value = "Thomas";
cmd2.Parameters.Add(prenomParam);


ConnectDb.ExecuteNonQuery(cmd2);

    Et voilà, vous disposez désormais d’une classe complète permettant de gérer tout vos accès base en mode connecté, de façon totalement indépendante du SGBD choisi !

    Nous pouvons cependant aller encore plus loin grâce à la généricité. En effet pourquoi se contenter d’utiliser un DbDataReader, quand on peut facilement disposer d’une Liste d’objets directement prêts à l’usage ? C’est ce que nous allons faire de suite :

La Methode Find<T>(…)

Implémentation

    L’objectif de cette méthode, est d’obtenir, en se basant sur une DbCommand on ne peut plus classique, une liste d’objets de type T  (générique donc).

    Pour ce Faire nous allons utiliser la notion de delegate et plus particulièrement le Type Func<Targ0, TResult> ajouté dans le framework 3.5 . Grosso modo, c’est une référence vers une fonction qui prendrait un paramètre de type Targ0 et renverrait un objet de type TResult.

    L’intérêt pour nous, c’est que nous allons donc pouvoir ajouter en paramètre de notre méthode Find, un delegate qui prendrait en Parametre un DbDataReader et renverrait un objet de Type T  remplit avec les données du DbDataReader. Pour faire simple, le delegate serait un “traducteur” pour passer d’un DbDataReader, vers une List<T>.

Voici donc à quoi ressemble notre Méthode Find :

public static List<T> Find<T>(DbCommand cmd, Func<DbDataReader, T> ParseObject)
{
using (DbDataReader rs = ExecuteQuery(cmd))
{
List<T> retList = new List<T>();

while (rs.Read())
{
T oRow = ParseObject(rs);
retList.Add(oRow);
}
return retList;
}
}

   Tout comme le type DbConnection,  le type DbDataReader implémente IDisposable. On ne se privera donc pas de l’entourer d’une directive using, pour s’assurer que les connections à la base seront correctement fermées. A l’aide de la méthode ExecuteQuery() que nous avons codée précédemment, nous récupérons le DbDataReader correspondant à notre DbCommand.

   Puis, pour chaque Enregistrement du DataReader, nous appelons le delegate passé en paramètre à notre méthode find  (Parseobject()). Il ne nous reste plus qu’à ajouter l’objet de Type T à la List<T> et à retourner cette liste une fois le reader entièrement parcouru.

   Très bien me direz vous, mais comment ça s’utilise ce machin ? démonstration :

Utilisation

   Considérons tout d’abord que dans notre Base de données, nous avons une table Personne contenant les champs suivants :

   Reprenons l’exemple de notre table Personne :

image11

      Nous réalisons une classe Personne dans notre programme pour la manipulation des données :

public class Personne
{
public String NomPrenom
public String Mail;

}

      J’ai fait exprès de Concaténer dans ma Classe les champs Nom et Prenom et d’omettre le champ Age,  pour bien montrer que les structure des classes du programme peuvent être différente de celles des tables de la base.

nous créons notre requête de sélection :

DbCommand cmd = ConnectDb.CreateCommand();
cmd.CommandText = "SELECT * FROM Personne";

    Il ne reste plus qu’à appeler notre fameuse méthode Find() . Avant cela je dois introduire la notion d’Anonymous Delegate. Grâce à ce concept nous allons pouvoir passer directement à notre Func<DbDataReader,T>, un bloc de code qui sera interprété comme une fonction :

List<Personne> personnes = ConnectDb.Find(cmd, delegate(DbDataReader rs)
{
Personne p = new Personne();
p.mail = rs["mail"].ToString();
p.NomPrenom = rs["Nom"].ToString() + rs["prenom"].ToString();
return p;
});

    Le code est donc dérisoirement simple, on passe donc en paramètre au find notre delegate et directement à la ligne, grâce au délégué anonyme nous mettons notre bloc de traitement.

    Remarquez une chose assez intéressante, je n’ai pas spécifié le type générique utilisé pour la méthode Find. Alors que pourtant, le prototype de la fonction est bien Find<T>(…). En fait, le compilateur C# est “intelligent”, et déduit du code, le type générique utilisé et est capable de déterminer si il est correct ou non. Ce mécanisme s’appelle Inférence de Type.

   Rentrons maintenant dans une amélioration introduite par le framework 3.5, et que permet d’utiliser Func<Targ,TResult> : Les lambda Expressions. L’explication détaillée des lambda expressions n’entre pas dans le cadre de cet article, je me permet juste de vous montrer comment aurait pu s’écrire l’appel à la méthode Find en utilisant ce mécanisme, libre à vous d’approfondir vos recherches sur ce sujet :

List<Personne> personnesLambda = ConnectDb.Find(cmd, rs => new Personne()
{
mail = rs["mail"].ToString(),
NomPrenom = rs["Nom"].ToString() + rs["prenom"].ToString()
});

     Les lambda expressions reposent en grande partie sur l’Inférence de type dont je vous parlais plus haut.  Décomposons les grandes lignes de ce code :

    Tout d’abord le rs => new Personne(). J’ai volontairement conservé les nom des variables pour que l’exemple soit plus clair:

     Par inférence de type, le compilateur déduit que la variable rs doit être de type DbDataReader. la flèche (=>) introduit ensuite une ligne de code, devant nécessairement retourner dans notre cas, un objet de Type Personne (toujours par inférence de type). Je n’ai donc qu’à instancier mon objet personne et à initialiser ses membres publics.

      Ceci n’est qu’un exemple extrêmement simpliste de ce qu’il est possible de faire à l’aide des lambda Expressions, si vous souhaitez approfondir cette voie, je vous conseille d’effectuer des recherches sur LINQ introduit également au framework 3.5, au sujet duquel j’écrirais sans doute un article sous peu.

Conclusion

    Nous avons donc désormais entre les mains, une classe permettant d’abstraire tous les accès à une base de données, assez souple pour interagir avec n’importe quel provider .NET implémentant les classes du namespace System.Data.Common.

   Pour conclure, je joint à cet article le code source (librement utilisable, distribuable, commercialisable, etc) complet de cette classe avec les exemples que nous avons vu, et reste à votre entière disposition si vous avez des questions, ou simplement envie de débattre d’une autre méthode d’implémentation.

  Notez bien que tout le code que nous avons écrit ne manipule QUE les objets abstraits du namespace System.Data.Common. EN AUCUN CAS la classe ConnectDb ne doit contenir de référence à un provider spécifique. Nous perdrions d’emblée tout l’intérêt de l’utilisation d’une factory.

Thomas COVILLE

Blog : http://thoums.blogspot.com

ADO.NET, C#