I. Introduction

Voici un schéma de la conception DAL que nous utilisons.

Image non disponible

Dans la partie 1, nous avons parlé de l'architecture globale de la DAL et de l'utilisation de DTO pour déplacer des données dans et hors de la DAL. Nous avons également traité la classe PersonDb. Cette fois, nous allons regarder la classe DALBase et comment écrire le code pour construire les classes PersonDb et d'autres classes EntityDb.

II. La classe DALBase

Notre classe PersonDb contient l'ensemble de nos méthodes d'accès aux données pour obtenir et définir les données pour une entité personne. La dernière fois, nous écrivions des méthodes PersonDb qui ressemblaient à :

GetPersonByPersonGuid
Sélectionnez
// GetPersonByPersonGuid
public static PersonDTO GetPersonByPersonGuid(Guid PersonGuid)
{
 SqlCommand command = GetDbSprocCommand("Person_GetByPersonGuid");
 command.Parameters.Add(CreateParameter("@PersonGuid", PersonGuid));
 return GetSingleDTO<PersonDTO>(ref command);
}

Maintenant, nous devons écrire les méthodes de la classe DALBase nécessaires pour faire ce travail. Nous allons avoir besoin d'une méthode GetDbSprocCommand(). Certains CreateParameter() surchargés pour chaque type de paramètre que nous devons créer. Et, le plus important, nous aurons besoin de méthodes génériques GetSingleDTO()et GetDTOList() qui recevront un objet Command et qui transformeront (parse) ensuite les données en un seul DTO ou en une liste générique de DTO. Je vais passer en revue chaque partie de la classe DALBase, puis à la fin de ce document, je reprendrai le code complet de la classe.

III. Chaîne de connexion

Premièrement, nous devons gérer la chaîne de connexion. Nous allons créer une propriété pour encapsuler la chaîne de connexion du ConfigurationManager.

ConnectionString
Sélectionnez
// ConnectionString
protected static string ConnectionString
{
 get { return ConfigurationManager.ConnectionStrings["MyConnection"].ConnectionString; }
}

IV. SqlConnection et des objets de commande

Maintenant, nous allons créer des méthodes pour obtenir une instance de SQLConnection, une instance de SqlCommand pour le TSQL, et puisque nous allons utiliser des procédures stockées, une instance de SqlCommand pour une procédure stockée. Notez que GetDbSprocCommand() est la méthode que nous utilisons dans notre code PersonDb. Maintenant que nous l'écrivons, nous savons exactement les paramètres dont elle a besoin ainsi que son type de retour.

GetDbConnection
Sélectionnez
// GetDbConnection
protected static SqlConnection GetDbConnection()
{
 return new SqlConnection(ConnectionString);
}// GetDbSqlCommand
protected static SqlCommand GetDbSQLCommand(string sqlQuery)
{
 SqlCommand command = new SqlCommand();
 command.Connection = GetDbConnection();
 command.CommandType = CommandType.Text;
 command.CommandText = sqlQuery;
 return command;
}

// GetDbSprocCommand
protected static SqlCommand GetDbSprocCommand(string sprocName)
{
 SqlCommand command = new SqlCommand(sprocName);
 command.Connection = GetDbConnection();
 command.CommandType = CommandType.StoredProcedure;
 return command;
}

V. Création des SqlParameters

Maintenant que nous avons une méthode pour créer une instance de SQLCommand pour procédure stockée, nous devons être en mesure de créer des paramètres. Lorsque nous avons rédigé notre code de la classe PersonDb, nous avons fixé un objectif pour la façon dont nous voulons que le CreateParameter() fonctionne. Nous voulons une méthode CreateParameter() unique qui peut être utilisée pour ajouter des paramètres integer, guid, string, datetime, et nous voulons qu'elle ait une syntaxe simple qui fonctionne sur une seule ligne, comme indiqué ci-dessous.

 
Sélectionnez
command.Parameters.Add (CreateParameter ("@Email", email, 100));

Cela devrait être possible. Notre méthode CreateParameter() prendra au moins deux paramètres. Le premier sera le nom du SqlParameter qui sera toujours une chaîne. Le deuxième sera la valeur du SqlParameter et le type de ce paramètre nous dit quel type de SqlParameter nous devrons créer. Donc, si nous avons une signature de méthode qui ressemble à CreateParameter(string name, guid value), nous savons que nous devons retourner un SqlParameter de Type SqlDbType.UniqueIdentifier. Si nous avons une signature de méthode qui ressemble à CreateParameter(string name, int value), nous savons que nous devons retourner un paramètre de type SqlDbType.Int. Voici à quoi nos méthodes CreateParameter() surchargées vont ressembler. Remarquez que nous avons ajouté un paramètre « size » supplémentaire à la signature de la surcharge qui crée un SqlParameter NvarChar.

CreateParameter
Sélectionnez
// CreateParameter - uniqueidentifier
protected static SqlParameter CreateParameter(string name, Guid value)
{
 if (value.Equals(Common.DTOBase.Guid_NullValue))
 {
  return CreateNullParameter(name, SqlDbType.UniqueIdentifier);
 }
 else
 {
  SqlParameter parameter = new SqlParameter();
  parameter.SqlDbType = SqlDbType.UniqueIdentifier;
  parameter.ParameterName = name;
  parameter.Value = value;
  parameter.Direction = ParameterDirection.Input;
  return parameter;
 }
}

// CreateParameter - nvarchar
protected static SqlParameter CreateParameter(string name, string value, int size)
{
 if (value == Common.DTOBase.String_NullValue)
 {
  return CreateNullParameter(name, SqlDbType.NVarChar);
 }
 else
 {
  SqlParameter parameter = new SqlParameter();
  parameter.SqlDbType = SqlDbType.NVarChar;
  parameter.Size = size;
  parameter.ParameterName = name;
  parameter.Value = value;
  parameter.Direction = ParameterDirection.Input;
  return parameter;
 }
}

Donc, nous faisons une méthode comme celles ci-dessus pour chaque type de SqlParameter que nous devons créer (datetime, int, etc.). Nous avons également besoin d'avoir un moyen de créer des SqlParameters avec une valeur null, et des paramètres de sortie. La meilleure façon que j'aie trouvée pour le faire est d'utiliser les techniques ci-dessus pour réaliser une méthode CreateOutputParameter() supplémentaire et une méthode CreateNullParameter(). Le code correspondant peut être trouvé dans le listing complet de la classe à la fin de ce document.

VI. GetSingleDTO()

Nous arrivons à quelque chose d'un peu plus intéressant. Nous avons codé les méthodes prédéfinies nécessaires pour créer un objet de commande et ajouter les paramètres associés.

Maintenant, nous devons prendre cet objet de commande et obtenir de lui un ensemble utilisable de données, notre PersonDTO. C'est le but de GetSingleDTO() qui prend un objet de commande, ouvre sa connexion, appelle ExecuteReader() sur cette dernière, tente d'obtenir un seul DTO du reader (lecteur), puis le renvoie. Puisque nous ne voulons pas de réécriture de code pour chaque type de DTO, nous avons fait une méthode générique GetSingleDTO qui accepte n'importe quel type qui hérite de DTOBase. Vous remarquerez que GetSingleDTO() encapsule la logique qui serait autrement répétée dans chaque méthode d'accès aux données que nous écrivons. Des opérations comme l'ouverture de la connexion, l'obtention d'un reader ainsi que la vérification de la présence de lignes dans le reader sont des fonctionnalités que nous faisons de la même façon à chaque fois que nous avons besoin d'obtenir des données. Alors, pourquoi écrire encore et encore ? Nous pouvons simplement tout encapsuler ici dans notre méthode GetSingleDTO(). En fait, nous n'avons pas vraiment le choix. Nous avons tout encapsulé ici parce que nous avons déjà écrit notre code d'accès aux données PersonDb et il exige que GetSingleDTO() prenne un objet de commande comme paramètre et renvoie un DTO du type demandé. Nous avons codé notre méthode pour cette façon de procéder.

GetSingleDto
Sélectionnez
// GetSingleDTO
protected static T GetSingleDTO<T>(ref SqlCommand command) where T : DTOBase
{
 T dto = null;
 try
 {
  command.Connection.Open();
  SqlDataReader reader = command.ExecuteReader();
  if (reader.HasRows)
  {
   reader.Read();
   DTOParser parser = DTOParserFactory.GetParser(typeof(T));
   parser.PopulateOrdinals(reader);
   dto = (T)parser.PopulateDTO(reader);
   reader.Close();
  }
  else
  {
   // S'il n'y a pas de données, nous renvoyons null.
   dto = null;
  }
 }
 catch (Exception e)
 {
  throw new Exception("Error populating data", e); 
 }
 finally
 {
  command.Connection.Close();
  command.Connection.Dispose();
 }
 // Renvoie le DTO, rempli soit avec des données soit avec null.
 return dto;
}

Cette méthodologie prend place après que nous confirmions que le reader a vraiment des lignes (HasRow). À ce moment-là, nous appelons Read() et créons un DTOParser pour le type que nous voulons renvoyer. Les deux prochaines étapes sont la clé. D'abord, nous appelons parser.PopulateOrdinals(). Cette méthode reçoit et affecte les ordinaux pour chaque champ de données que nous devons extraire du Reader. Un ordinal est essentiellement un indice qui vous indique où trouver une valeur de champ de données spécifique dans le reader. Obtenir des valeurs par ordinal (index) est beaucoup plus efficace que d'obtenir des valeurs par le nom de champ (colonne) de données. Après avoir renseigné les ordinaux, nous sommes alors en mesure d'utiliser le Parser (analyseur/répartiteur) pour remplir l'objet DTO que notre méthode renverra.

Vous remarquerez que le code ci-dessus ne contient aucune logique pour parser le DTO en cours depuis le reader. C'est parce que les changements logiques dépendent du type de DTO que nous recevons. Donc, nous encapsulons ce qui change dans un objet DTOParser. Nous allons alors simplement utiliser le DTOParser pour obtenir le type de DTO que nous recherchons et nous laissons le DTOParser se soucier de la façon dont le parsing (analyse/répartition) est réellement fait.

La méthode GetDTOList() utilise le même design que ci-dessus, elle retourne simplement une liste générique de DTO lieu d'un seul DTO. La partie de code de GetDTOList() est incluse dans la classe complète située à la fin de ce document.

VII. Résumé

Maintenant, nous avons couvert la classe DALBase. Dans le prochain article, nous allons entrer dans la vraie magie de la performance où nous prendrons ce reader, lequel sera passé au DTOParser, et utilisera les données ordinales ainsi que les méthodes (Get) fortement typées pour extraire des données de la manière la plus efficace possible sans caster l'objet.

Voici le listing complet de DALBase.

La classe DALBase
Sélectionnez
public abstract class DALBase 
{
 // ConnectionString
 protected static string ConnectionString
 {
  get { return ConfigurationManager.ConnectionStrings["MyConn"].ConnectionString; }
 }

 // GetDbSqlCommand
 protected static SqlCommand GetDbSQLCommand(string sqlQuery)
 {
  SqlCommand command = new SqlCommand();
  command.Connection = GetDbConnection();
  command.CommandType = CommandType.Text;
  command.CommandText = sqlQuery;
  return command;
 }

 // GetDbConnection
 protected static SqlConnection GetDbConnection()
 {
  return new SqlConnection(ConnectionString);
 }

 // GetDbSprocCommand
 protected static SqlCommand GetDbSprocCommand(string sprocName)
 {
  SqlCommand command = new SqlCommand(sprocName);
  command.Connection = GetDbConnection();
  command.CommandType = CommandType.StoredProcedure;
  return command;
 }// CreateNullParameter
 protected static SqlParameter CreateNullParameter(string name, SqlDbType paramType)
 {
  SqlParameter parameter = new SqlParameter();
  parameter.SqlDbType = paramType;
  parameter.ParameterName = name;
  parameter.Value = null;
  parameter.Direction = ParameterDirection.Input;
  return parameter;
 }

 // CreateNullParameter - avec la taille pour nvarchars
 protected static SqlParameter CreateNullParameter(string name, SqlDbType paramType, int size)
 {
  SqlParameter parameter = new SqlParameter();
  parameter.SqlDbType = paramType;
  parameter.ParameterName = name;
  parameter.Size = size;
  parameter.Value = null;
  parameter.Direction = ParameterDirection.Input;
  return parameter;
 }

 // CreateOutputParameter
 protected static SqlParameter CreateOutputParameter(string name, SqlDbType paramType)
 {
  SqlParameter parameter = new SqlParameter();
  parameter.SqlDbType = paramType;
  parameter.ParameterName = name;
  parameter.Direction = ParameterDirection.Output;
  return parameter;
 }// CreateOuputParameter - avec la taille pour nvarchars
 protected static SqlParameter CreateOutputParameter(string name, SqlDbType paramType, int size)
 {
  SqlParameter parameter = new SqlParameter();
  parameter.SqlDbType = paramType;
  parameter.Size = size;
  parameter.ParameterName = name;
  parameter.Direction = ParameterDirection.Output;
  return parameter;
 }

 // CreateParameter - uniqueidentifier
 protected static SqlParameter CreateParameter(string name, Guid value)
 {
  if (value.Equals(Common.DTOBase.Guid_NullValue))
  {
   // Si la valeur est null alors crée un paramètre null.
   return CreateNullParameter(name, SqlDbType.UniqueIdentifier);
  }
  else
  {
   SqlParameter parameter = new SqlParameter();
   parameter.SqlDbType = SqlDbType.UniqueIdentifier;
   parameter.ParameterName = name;
   parameter.Value = value;
   parameter.Direction = ParameterDirection.Input;
   return parameter;
  }
 }// CreateParameter - int
 protected static SqlParameter CreateParameter(string name, int value)
 {
  if (value == Common.DTOBase.Int_NullValue)
  {
   // Si la valeur est null alors crée un paramètre null.
   return CreateNullParameter(name, SqlDbType.Int);
  }
  else
  {
   SqlParameter parameter = new SqlParameter();
   parameter.SqlDbType = SqlDbType.Int;
   parameter.ParameterName = name;
   parameter.Value = value;
   parameter.Direction = ParameterDirection.Input;
   return parameter;
  }
 }

 // CreateParameter - datetime
 protected static SqlParameter CreateParameter(string name, DateTime value)
 {
  if (value == Common.DTOBase.DateTime_NullValue)
  {
   // Si la valeur est null alors crée un paramètre null.
   return CreateNullParameter(name, SqlDbType.DateTime);
  }
  else
  {
   SqlParameter parameter = new SqlParameter();
   parameter.SqlDbType = SqlDbType.DateTime;
   parameter.ParameterName = name;
   parameter.Value = value;
   parameter.Direction = ParameterDirection.Input;
   return parameter;
  }
 }

 // CreateParameter - nvarchar
 protected static SqlParameter CreateParameter(string name, string value, int size)
 {
  if (value == Common.DTOBase.String_NullValue)
  {
   // Si la valeur est null alors crée un paramètre null.
   return CreateNullParameter(name, SqlDbType.NVarChar);
  }
  else
  {
   SqlParameter parameter = new SqlParameter();
   parameter.SqlDbType = SqlDbType.NVarChar;
   parameter.Size = size;
   parameter.ParameterName = name;
   parameter.Value = value;
   parameter.Direction = ParameterDirection.Input;
   return parameter;
  }
 }// GetSingleDTO
 protected static T GetSingleDTO<T>(ref SqlCommand command) where T : DTOBase
 {
  T dto = null;
  try
  {
   command.Connection.Open();
   SqlDataReader reader = command.ExecuteReader();
   if (reader.HasRows)
   {
    reader.Read();
    DTOParser parser = DTOParserFactory.GetParser(typeof(T));
    parser.PopulateOrdinals(reader);
    dto = (T)parser.PopulateDTO(reader);
    reader.Close();
   }
   else
   {
    // S'il n'y a pas de données, nous renvoyons null.
    dto = null;
   }
  }
  catch (Exception e)
  {
   throw new Exception("Error populating data", e);
  }
  finally
  {
   command.Connection.Close();
   command.Connection.Dispose();
  }
  // Renvoie le DTO, rempli soit avec des données soit avec null.
  return dto;
 }// GetDTOList
 protected static List<T> GetDTOList<T>(ref SqlCommand command) where T : DTOBase
 {
  List<T> dtoList = new List<T>();
  try
  {
   command.Connection.Open();
   SqlDataReader reader = command.ExecuteReader();
   if (reader.HasRows)
   {
    // Obtenir un analyseur (parser) pour ce type de DTO et remplir les ordinaux.
    DTOParser parser = DTOParserFactory.GetParser(typeof(T));
    parser.PopulateOrdinals(reader);
    // Utilise l'analyseur (parser) pour construire notre liste de DTO.
    while (reader.Read())
    {
     T dto = null;
     dto = (T)parser.PopulateDTO(reader);
     dtoList.Add(dto);
    }
    reader.Close();
   }
   else
   {
   // S'il n'y a pas de données, nous renvoyons null.
   dtoList = null;
   }
  }
  catch (Exception e)
  {
   throw new Exception("Error populating data", e);
  }
  finally
  {
   command.Connection.Close();
   command.Connection.Dispose();
  }
  return dtoList;
 }
}

VIII. Remerciements

Je remercie M. Lacovara de m'avoir permis de traduire sa série d'articles « High Performance Data Access Layer Architecture ».

Gaëtan Wauthy et Kropernic pour la relecture et la validation technique, ainsi qu'une première relecture orthographique.

Claude Leloup pour la relecture orthographique.

IX. Source