I. Pourquoi écrire un code d'accès aux données quand nous avons des ORM ?▲
Vous rappelez-vous des modèles (pattern) d'accès aux données ? Il semble qu'il y ait une énorme attention ces jours-ci autour de LINQ, ADO.Net Entity Framework et d'autres outils ORM-like qui visent à rendre les programmeurs plus efficaces en leur libérant tout le temps qu'ils utilisaient pour programmer l'accès aux données. Il y a certainement des améliorations de la productivité lors de l'utilisation de ces outils, mais l'application résultante est-elle suffisamment performante ? Dans la plupart des cas où nous avons une ligne de demande standard de l'entreprise dont les principales préoccupations sont la fonctionnalité et le workflow, la réponse est « assez bonne » (on parle de la performance). Les outils de ce genre s'adaptent parfaitement pour ces applications. Toutefois, si vous disposez d'une application à volume élevé où la performance est la préoccupation principale, ces outils peuvent ne pas être le bon choix. Le code LINQ et ADO.Net généré est nettement plus lent que du code ADO.Net bien écrit. Selon un billet sur le blog de l'équipe ADO.net intitulé ADO.NET Entity Framework Performance Comparison, ADO.Net Entity Framework peut être de 50 % à 300 % plus lent que ADO.Net utilisant les ordinaux et SqlDataReaders.
Donc, mon avis est que les ORM sont parfaits pour la plupart des applications, mais lorsque la performance est un facteur important il est préférable de réaliser votre propre DAL. Ce document fera la démonstration de quelques-uns des modèles que j'utilise pour la couche d'accès aux données qui permettent un développement rapide et des performances « rapides comme l'éclair».
II. Utilisez les DTO, pas un ensemble de données ou un DataTable▲
Tout d'abord, quel conteneur allons-nous utiliser pour transmettre des données à partir de notre DAL aux autres couches de notre application ? Les réponses habituelles que je reçois sont, soit des DataTables/des DataSets, soit des objets métier complets. Je n'aime pas non plus ces derniers. Les DataSets et DataTables viennent avec des zones mémoire importantes et ils ne contiennent pas de données fortement typées. Les objets métier contiennent des données fortement typées, mais ils contiennent généralement beaucoup de logique métier supplémentaire dont je n'ai pas besoin, et ils peuvent même contenir une logique de persistance. Je ne veux vraiment pas de tout cela. Je veux le conteneur le plus léger et le plus simple possible qui me donnera des données fortement typées, et ce conteneur est un objet de transfert de données (DTO). Les DTO sont des classes simples qui ne contiennent que des propriétés. Ils n'ont pas de méthode réelle, seulement des mutateurs et des accesseurs pour leurs données. Voici un diagramme de classe d'un PersonDTO ainsi que les classes de DTOBase et CommonBase qui sont dans sa chaîne d'héritage.
PersonDTO contient toutes les données nécessaires à une entité de personnes dans mon application.
Voici comment je construis généralement un DTO. Tout d'abord, les DTO sont conçus pour se déplacer entre des couches de l'application. Donc, ils n'ont pas leur place dans la DAL. Je les mets dans un projet/Assembly distinct nommé Common. Puis je crée une référence à Common dans ma DAL, BLL, interface utilisateur Web, et dans tout autre projet dans mon application.
Maintenant, nous pouvons commencer à créer des classes dans Common. La première classe que nous devons créer est CommonBase. Le seul but de CommonBase est de contenir les propriétés statiques qui définissent les valeurs null. Nos DTO vont contenir à la fois des données de type valeur et de type référence. Et surtout les types valeur ont toujours une valeur et ne sont jamais null. Dans le cas contraire, la vérification d'une valeur null peut représenter un défi dans les couches supérieures de l'application. Pour compliquer encore les choses, certains développeurs vont utiliser String.Empty ou "" pour représenter une valeur null pour une chaîne. D'autres vont utiliser null (string est de type référence après tout). Pour éviter toute cette confusion, je tiens à définir les valeurs null réelles pour chaque type dans mon Assembly Common. De cette façon, nous avons une valeur prédéfinie que nous pouvons utiliser pour le contrôle de null et le réglage de null pour toute l'application. Voici le code pour la classe CommonBase.
Public
Class
CommonBase
' Les valeurs nulles standards de configuration
Public
Shared
DateTime_NullValue As
DateTime =
DateTime.MinValue
Public
Shared
Guid_NullValue As
Guid =
Guid.Empty
Public
Shared
Int_NullValue As
Integer
=
Integer
.MinValue
Public
Shared
Float_NullValue As
Single
=
Single
.MinValue
Public
Shared
Decimal_NullValue As
Decimal
=
Decimal
.MinValue
Public
Shared
String_NullValue As
String
=
Nothing
End
Class
La classe suivante est DTOBase. Cette classe de base encapsule une fonctionnalité commune pour mes DTO. À l'heure actuelle, la seule chose que j'ai placée dans DTOBase est un indicateur IsNew qui peut être utilisé pour indiquer si un DTO contient des données nouvellement créées (par opposition à des données qui ont été tirées de la base de données).
Public
MustInherit
Class
DTOBase
Inherits
CommonBase
Private
m_IsNew As
Boolean
Public
Property
IsNew
(
) As
Boolean
Get
Return
m_IsNew
End
Get
Set
m_IsNew =
Value
End
Set
End
Property
End
Class
Maintenant, je peux créer ma classe PersonDTO. PersonDTO contient juste un ensemble de propriétés qui représentent les données d'un dossier d'une personne, et un constructeur qui initialise chaque propriété à la valeur null de son type.
Public
Class
PersonDTO
Inherits
DTOBase
Private
m_PersonGuid As
Guid
Public
Property
PersonGuid
(
) As
Guid
Get
Return
m_PersonGuid
End
Get
Set
m_PersonGuid =
Value
End
Set
End
Property
Private
m_PersonId As
Integer
Public
Property
PersonId
(
) As
Integer
Get
Return
m_PersonId
End
Get
Set
m_PersonId =
Value
End
Set
End
Property
Private
m_UtcCreated As
DateTime
Public
Property
UtcCreated
(
) As
DateTime
Get
Return
m_UtcCreated
End
Get
Set
m_UtcCreated =
Value
End
Set
End
Property
Private
m_UtcModified As
DateTime
Public
Property
UtcModified
(
) As
DateTime
Get
Return
m_UtcModified
End
Get
Set
m_UtcModified =
Value
End
Set
End
Property
Private
m_Password As
String
Public
Property
Password
(
) As
String
Get
Return
m_Password
End
Get
Set
m_Password =
Value
End
Set
End
Property
Private
m_Name As
String
Public
Property
Name
(
) As
String
Get
Return
m_Name
End
Get
Set
m_Name =
Value
End
Set
End
Property
Public
Property
Nickname
(
) As
String
Get
Return
m_Nickname
End
Get
Set
m_Nickname =
Value
End
Set
End
Property
Private
m_Nickname As
String
Public
Property
PhoneMobile
(
) As
String
Get
Return
m_PhoneMobile
End
Get
Set
m_PhoneMobile =
Value
End
Set
End
Property
Private
m_PhoneMobile As
String
Public
Property
PhoneHome
(
) As
String
Get
Return
m_PhoneHome
End
Get
Set
m_PhoneHome =
Value
End
Set
End
Property
Private
m_PhoneHome As
String
Public
Property
Email
(
) As
String
Get
Return
m_Email
End
Get
Set
m_Email =
Value
End
Set
End
Property
Private
m_Email As
String
Public
Property
ImAddress
(
) As
String
Get
Return
m_ImAddress
End
Get
Set
m_ImAddress =
Value
End
Set
End
Property
Private
m_ImAddress As
String
Public
Property
ImType
(
) As
Integer
Get
Return
m_ImType
End
Get
Set
m_ImType =
Value
End
Set
End
Property
Private
m_ImType As
Integer
Private
m_TimeZoneId As
Integer
Public
Property
TimeZoneId
(
) As
Integer
Get
Return
m_TimeZoneId
End
Get
Set
m_TimeZoneId =
Value
End
Set
End
Property
Private
m_LanguageId As
Integer
Public
Property
LanguageId
(
) As
Integer
Get
Return
m_LanguageId
End
Get
Set
m_LanguageId =
Value
End
Set
End
Property
Private
m_City As
String
Public
Property
City
(
) As
String
Get
Return
m_City
End
Get
Set
m_City =
Value
End
Set
End
Property
Private
m_State As
String
Public
Property
State
(
) As
String
Get
Return
m_State
End
Get
Set
m_State =
Value
End
Set
End
Property
Private
m_ZipCode As
Integer
Public
Property
ZipCode
(
) As
Integer
Get
Return
m_ZipCode
End
Get
Set
m_ZipCode =
Value
End
Set
End
Property
' Constructeur
' Pas de paramètre, et toutes les propriétés sont initialisées à leur valeur nulle tel que défini dans CommonBase.
Public
Sub
New
(
)
PersonGuid =
Guid_NullValue
PersonId =
Int_NullValue
UtcCreated =
DateTime_NullValue
UtcModified =
DateTime_NullValue
Name =
String_NullValue
Nickname =
String_NullValue
PhoneMobile =
String_NullValue
PhoneHome =
String_NullValue
Email =
String_NullValue
ImAddress =
String_NullValue
ImType =
Int_NullValue
TimeZoneId =
Int_NullValue
LanguageId =
Int_NullValue
City =
String_NullValue
State =
String_NullValue
ZipCode =
Int_NullValue
IsNew =
True
End
Sub
End
Class
III. Comment la DAL devrait envoyer des données à d'autres couches ? ▲
Lorsque vous construisez le code de votre Framework, c'est une bonne idée d'arrêter périodiquement et de bien réfléchir à la façon dont le code que vous écrivez va être utilisé. La technique la plus utile que j'utilise est d'arrêter, et de visualiser ce à quoi le code doit ressembler. Nous avons déjà décidé que nous utilisons les DTO pour contenir des données. Prenons un moment pour réfléchir à ce que nous voulons que notre code BLL ressemble et quelles fonctionnalités il faudra pour notre DAL. Dans ma BLL, je vais probablement avoir un PersonRepository. Dans ce référentiel, je vais avoir des méthodes qui voudront obtenir un objet PersonDTO individuel et des listes génériques de PersonDTO provenant de la DAL. J'aurai probablement un seul objet DAL qui fournira des méthodes pour obtenir ces DTO. Je tiens donc à créer une classe PersonDb dans ma DAL qui va me permettre d'écrire du code BLL qui ressemble à ceci :
Dim
db As
PersonDb =
New
PersonDb
Dim
dto As
PersonDTO =
db.GetPersonByPersonGuid
(
personGuid)
Dim
dto As
PersonDTO =
db.GetPersonByEmail
(
email)
Dim
people As
List
(
Of
PersonDTO) =
db.GetPersonList
(
)
Avec cet objectif à l'esprit, je vais créer une classe PersonDb dans ma DAL. PersonDb va fournir des méthodes qui vont, soit renvoyer un seul PersonDTO, soit une liste de PersonDTO (List<PersonDTO>).
IV. L'architecture DAL▲
Nous allons avoir une classe DALBase qui encapsule toute notre logique fonctionnelle comme la création de connexions, des commandes TSQL, des procédures stockées, et des paramètres. La classe DALBase contiendra également des méthodes pour obtenir nos deux principaux types de retour, DTO et la liste de DTO, à partir d'un SqlDataReader. Pour agir comme un guichet unique pour l'ensemble de nos méthodes d'accès aux données, pour obtenir et définir les données de personnes, nous allons créer une classe PersonDB. PersonDB héritera de DALBase et contiendra l'ensemble de nos méthodes qui retournent ou enregistrent des données de personnes comme GetPersonByEmail(), GetPersonById() et SavePerson().
Nous aurons également besoin de trouver un endroit où mettre la logique pour la lecture de nos données de personnes avec un SqlDataReader ouvert et les mettre dans un PersonDTO. Il s'agit de trouver le nombre ordinal d'un champ de données, de vérifier pour voir s'il est null, et si ce n'est pas le cas, de stocker la valeur de la donnée dans le DTO. C'est vraiment une logique de parsing et nous allons donc la mettre dans une classe DTOParser_Person séparée. Actuellement, nous regardons seulement les classes pour PersonDTO, mais nous aurons besoin d'avoir un parser (analyseur/répartiteur) différent pour chaque type DTO que la DAL pourra retourner (PersonDTO, CompanyDTO, UserDTO, etc.). Nous allons utiliser une classe DTOParser abstraite pour définir l'interface pour tous les DTOParsers et encapsuler les fonctionnalités répétées. Enfin, nous allons créer une classe DTOParserFactory statique qui retourne une instance de DTOParser appropriée pour tout type de DTO que nous lui passerons. Donc, si nous avons besoin de parser un PersonDTO sur un reader, nous l'appelons tout simplement
Dim
parser As
DTOParser =
DTOParserFactory.GetParser
(
GetType
(
PersonDTO))
et nous aurons une instance de la classe DTOParser_Person. Voici à quoi nos classes de la DAL vont ressembler.
V. PersonDb▲
Employant encore une fois le principe de penser d'abord à propos de la façon dont nous voulons utiliser notre code, puis d'écrire le code pour atteindre cet objectif, nous allons d'abord écrire notre classe PersonDb, puis nous écrirons notre classe DALBase. La classe PersonDb devra utiliser des méthodes de DALBase pour des opérations comme la création d'objets SqlCommand ou l'obtention d'une liste de PersonDTOs. Une fois que nous voyons comment nous voulons utiliser ces fonctionnalités dans PersonDb, nous aurons une meilleure idée de la façon dont nous voulons travailler avec DALBase.
D'abord, nous allons écrire la méthode GetPersonByPersonGuid(). Extraire des données à partir d'une base de données, puis remplir un DTO avec celles-ci et enfin renvoyer ce DTO ne prend que peu de code. Mais si on y réfléchit bien, la plus grande partie de ce code est dupliquée pour chaque méthode d'accès aux données que nous écrivons. Si nous extrayons les seules choses qui changent pour chaque méthode, nous obtenons la liste suivante :
-nous allons utiliser des procédures stockées du côté de SQL Server, la première chose que nous devons faire est d'obtenir un objet SqlCommand pour la procédure stockée nommée ;
-ensuite, nous aurons besoin d'ajouter des paramètres et de définir leur valeur ;
-la dernière chose que nous devons faire est de lancer la commande et récupérer le type d'ensemble de données souhaité (soit un DTO ou une Liste <DTO>) rempli avec les données.
Ce sont les seuls éléments qui changent vraiment. Quelle procédure stockée sproc nous appelons, quels sont les paramètres que nous devons ajouter, et quel est le type de retour. Nous allons donc écrire des méthodes prédéfinies dans la classe DALBase qui nous permettront d'effectuer chacune de ces tâches avec une seule ligne de code. Le code de GetPersonByPersonGuid()résultant ressemblera à ceci :
Dim
command As
SqlCommand =
GetDbSprocCommand
(
"Person_GetByPersonGuid"
)
command.Parameters.Add
(
CreateParameter
(
"@PersonGuid"
, PersonGuid))
Return
GetSingleDTO
(
command)
Si nous avons besoin d'une méthode GetPersonByEmail(), nous pouvons utiliser le code ci-dessus avec des modifications mineures. Les éléments qui changent sont juste le nom de la procédure stockée sproc et le paramètre. Le code modifié est :
Dim
command As
SqlCommand =
GetDbSprocCommand
(
"Person_GetByEmail"
)
command.Parameters.Add
(
CreateParameter
(
"@Email"
, Email))
Return
GetSingleDTO
(
command)
Ensuite, si nous avons besoin d'une méthode qui retourne tous les enregistrements de personnes GetAll(), nous pouvons aussi le faire facilement. Cette fois, le nom de la procédure stockée sproc, les paramètres (cette fois, il n'y en a pas besoin), et le type de retour changent tous.
Dim
command As
SqlCommand =
GetDbSprocCommand
(
"Person_GetAll"
)
Return
GetDTOList
(
command)
Donc, avec quelques méthodes prédéfinies, nous pouvons mettre en place une classe PersonDb simple et facile à maintenir.
Si vous regardiez attentivement, vous avez remarqué quelles sont les exigences pour la classe DALBase qui ont émergé lors de l'écriture du code de la classe PersonDb. Tout d'abord, nous voulons utiliser les méthodes GetSingleDTO() et GetDTOList(), mais nous devons être en mesure de leur dire de retourner des types spécifiques de DTO, comme PersonDTO. C'est pourquoi celles-ci doivent être des méthodes génériques qui prennent le DTO comme type de paramètre, comme GetSingleDTO<PersonDTO>().
Deuxièmement, nous avons utilisé la même méthode CreateParameter() pour créer un paramètre de chaîne et un paramètre Guid. Nous aurons donc à faire un peu de polymorphisme et écrire des méthodes surchargées pour CreateParameter() pour chaque type de paramètre que nous voulons créer.
Nous allons entrer dans les détails la prochaine fois lorsque nous finirons notre DAL en codant la classe DALBase, les classes de DTOParser, et la classe DTOParserFactory. C'est là que nous verrons le vrai gain de performance. Pour l'accès aux données, nous allons utiliser les ordinaux (données atomiques) pour extraire des données à partir du reader de la manière la plus efficace possible et ensuite utiliser le SqlDataReader et des méthodes (Get) fortement typées pour faire les vérifications null et écrire les valeurs de données dans notre DTO, le tout sans caster la valeur dans l'objet. Pour l'instant, voici la classe PersonDb complète avec une méthode SavePerson() qui prend comme seul paramètre un PersonDTO.
Public
Class
PersonDb
Inherits
DALBase
' GetPersonByPersonGuid
Public
Shared
Function
GetPersonByPersonGuid
(
ByVal
PersonGuid As
Guid) As
PersonDTO
Dim
command As
SqlCommand =
GetDbSprocCommand
(
"Person_GetByPersonGuid"
)
command.Parameters.Add
(
CreateParameter
(
"@PersonGuid"
, PersonGuid))
Return
GetSingleDTO
(
Of
PersonDTO)(
command)
End
Function
' GetPersonByEmail
Public
Shared
Function
GetPersonByEmail
(
ByVal
email As
String
) As
PersonDTO
Dim
command As
SqlCommand =
GetDbSprocCommand
(
"Person_GetByEmail"
)
command.Parameters.Add
(
CreateParameter
(
"@Email"
, email, 100
))
Return
GetSingleDTO
(
Of
PersonDTO)(
command)
End
Function
' GetAll
Public
Shared
Function
GetAll
(
) As
List
(
Of
PersonDTO)
Dim
command As
SqlCommand =
GetDbSprocCommand
(
"Person_GetAll"
)
Return
GetDTOList
(
Of
PersonDTO)(
command)
End
Function
' SavePerson
Public
Shared
Sub
SavePerson
(
ByRef
person As
PersonDTO)
' La sproc gérera l'insertion et la mise à jour.
' Nous avons juste besoin de retourner le GUID approprié de la personne.
' S'il s'agit d'une nouvelle personne, alors nous retournons le NewPersonGuid.
' S'il s'agit d'une mise à jour, nous renvoyons juste le PersonGuid.
Dim
isNewRecord As
Boolean
=
False
If
person.PersonGuid.Equals
(
DTOBase.Guid_NullValue
) Then
isNewRecord =
True
End
If
' Crée la commande et les paramètres.
' Lors de la création des paramètres nous n'avons pas besoin de vérifier les valeurs null.
' La méthode CreateParameter va gérer cela pour nous et créer les paramètres null pour tous les membres DTO qui correspondent à DTOBase.NullValue dans le type de données du membre.
Dim
command As
SqlCommand =
GetDbSprocCommand
(
"Person_Save"
)
command.Parameters.Add
(
CreateParameter
(
"@PersonGuid"
, person.PersonGuid
))
command.Parameters.Add
(
CreateParameter
(
"@Password"
, person.Password
, 20
))
command.Parameters.Add
(
CreateParameter
(
"@Name"
, person.Name
, 100
))
command.Parameters.Add
(
CreateParameter
(
"@Nickname"
, person.Nickname
, 50
))
command.Parameters.Add
(
CreateParameter
(
"@PhoneMobile"
, person.PhoneMobile
, 25
))
command.Parameters.Add
(
CreateParameter
(
"@PhoneHome"
, person.PhoneHome
, 25
))
command.Parameters.Add
(
CreateParameter
(
"@Email"
, person.Email
, 100
))
command.Parameters.Add
(
CreateParameter
(
"@ImAddress"
, person.ImAddress
, 50
))
command.Parameters.Add
(
CreateParameter
(
"@ImType"
, person.ImType
))
command.Parameters.Add
(
CreateParameter
(
"@TimeZoneId"
, person.TimeZoneId
))
command.Parameters.Add
(
CreateParameter
(
"@LanguageId"
, person.LanguageId
))
Dim
paramIsDuplicateEmail As
SqlParameter =
CreateOutputParameter
(
"@IsDuplicateEmail"
, SqlDbType.Bit
)
command.Parameters.Add
(
paramIsDuplicateEmail)
Dim
paramNewPersonGuid As
SqlParameter =
CreateOutputParameter
(
"@NewPersonGuid"
, SqlDbType.UniqueIdentifier
)
command.Parameters.Add
(
paramNewPersonGuid)
' Exécute la commande.
command.Connection.Open
(
)
command.ExecuteNonQuery
(
)
command.Connection.Close
(
)
' Vérifie si c'est un email dupliqué.
If
CBool
(
paramIsDuplicateEmail.Value
) Then
Throw
New
Common.Exceptions.DuplicateEmailException
(
) ' non documenté ici
End
If
' S'il s'agit d'un nouvel enregistrement, attribut un nouveau Guid à l'objet.
If
isNewRecord Then
person.PersonGuid
=
DirectCast
(
paramNewPersonGuid.Value
, Guid)
End
If
End
Sub
End
Class
VI. 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.
VII. Source▲
Traduction de l'article de M. Rudy Lacovara — High Performance Data Access Layer Architecture Part 1.