I. Introduction▲
C'est le dernier article d'une série qui décrit une conception que j'utilise pour un accès de haute performance aux données. Dans la partie 1, nous avons couvert l'architecture et la conception globale de PersonDb. Dans la partie 2, nous avons couvert la mise en œuvre de l'implémentation de DALBase.
II. Les classes de DTOParser▲
Nous mettons en œuvre l'architecture ci-dessous.
Donc, au début, nous avons décidé d'utiliser les Data Transfert Objects (DTO) pour déplacer des données entre notre BLL et notre DAL. Nous avons aussi défini que notre DAL n'aurait que deux types de retour de base ; un seul DTO, ou une liste générique de DTO. À ce stade, nous avons écrit notre classe PersonDb qui encapsule nos méthodes d'accès aux données et nous avons écrit notre classe DALBase qui encapsule nos méthodes GetSingleDTO() et GetDTOList() ainsi que des méthodes générales pour d'autres opérations comme obtenir une chaîne de connexion ou créer des paramètres de procédure stockée. Voici le code que nous avons écrit pour GetSingleDTO().
' GetSingleDTO
Protected
Shared
Function
GetSingleDTO
(
Of
T As
DTOBase)(
ByRef
command As
SqlCommand) As
T
Dim
dto As
T =
Nothing
Try
command.Connection.Open
(
)
Dim
reader As
SqlDataReader =
command.ExecuteReader
(
)
If
reader.HasRows
Then
reader.Read
(
)
Dim
parser As
DTOParser =
DTOParserFactory.GetParser
(
GetType
(
T))
parser.PopulateOrdinals
(
reader)
dto =
DirectCast
(
parser.PopulateDTO
(
reader), T)
reader.Close
(
)
Else
' S'il n'y a pas de données, nous renvoyons null.
dto =
Nothing
End
If
Catch
e As
Exception
Throw
New
Exception
(
"Error populating data"
, e)
Finally
command.Connection.Close
(
)
command.Connection.Dispose
(
)
End
Try
' Renvoie le DTO, rempli soit avec des données soit avec null.
Return
dto
End
Function
Ainsi, cette méthode encapsule toute notre logique reproductible pour obtenir un seul DTO d'un reader (lecteur), et nous utilisons les génériques .NET pour créer une méthode générale qui peut renvoyer n'importe quel type qui hérite de DTOBase. Toutefois, notez que nous obtenons les détails pour chaque champ de données à partir du reader. Le remplissage des propriétés via les champs correspondants du reader sera confié à un autre objet, le DTOParser.
III. Que sont les ordinaux et pourquoi les utiliser ? ▲
Avant d'entrer dans le DTOParser, prenons une minute pour parler des ordinaux. Un ordinal est juste un indice qui vous indique où un champ de données est situé dans le flux auquel vous accédez via votre SqlDataReader. Disons que vous avez un champ de données nommé « date_created » et que vous ayez besoin d'obtenir la donnée de ce champ via un reader. La plupart des développeurs utiliseraient un code qui ressemble à ceci.
Dim
field As
Object
=
reader
(
"date_created"
)
Dim
dateCreated As
DateTime =
If
((
DBNull.Value.Equals
(
field)), DateTime.MinValue
, DirectCast
(
field, DateTime))
La donnée est obtenue à partir du reader par le nom du champ de données. Elle est stockée dans un objet. Nous effectuons une vérification sur DBNull et si la valeur de notre donnée est correcte, nous la castons en DateTime. Il s'agit d'un code assez solide et j'aime le fait que nous faisons toujours une vérification sur NULL mais il y a encore quelques problèmes dans une perspective de performance.
Tout d'abord, nous obtenons notre donnée à partir du reader par le nom « date_created ». Le reader ne sait pas quel est le type spécifique de « date_created ». Il doit aller trouver l'index associé à ce nom et il peut alors utiliser cet index pour accéder à la donnée. Cette valeur d'indice est l'ordinal. Le SqlDataReader peut travailler d'une manière beaucoup plus efficace si nous lui donnons cet ordinal à la place du nom du champ de données pour travailler.
Deuxièmement, nous obtenons une valeur DateTime, mais nous faisons un premier cast sur l'objet. Je préfère ne pas faire de cast puisque je sais que je suis à la recherche de données datetime mais la syntaxe du reader ["field_name"] retourne un objet. De plus j'ai besoin de faire un une vérification sur NULL. Quel autre choix ai-je ? Si j'utilise les ordinaux, la réponse est que le SqlDataReader a une méthode GetDateTime() fortement typée qui a été faite pour ce but précis. SqlDataReader a des méthodes GetXxx() fortement typées pour chaque type de données, ce qui nous permettra d'éviter ce casting pour l'objet. SqlDataReader a également une méthode IsDBNull() que nous pouvons utiliser pour faire notre vérification de DBNull. Le hic, c'est que ces méthodes n'accepteront pas les noms de champs de données, ils vous obligent à utiliser les ordinaux.
Donc, nous allons écrire le même code en supposant que nous savons que l'ordinal pour « date_created » est 4. Le résultat devrait ressembler à ceci.
Dim
dateCreated As
DateTime =
If
(
reader.IsDBNull
(
4
), DateTime.MinValue
, reader.GetDateTime
(
4
))
Ce code utilise la méthode la plus efficace possible pour obtenir des données à partir de notre SqlDataReader, et nous échappons à un cast vers l'objet et un cast depuis l'objet (boxing et unboxing). Si nous voulons vraiment optimiser le rendement, c'est le code que nous devons utiliser.
IV. Comment le DTOParser est utilisé ▲
Nous avons abordé ceci dans le précédent article, mais juste pour nous rafraîchir la mémoire, nous allons jeter un coup d'œil à la façon dont notre objet DTOParser est utilisé dans la classe DALBase. Le code ci-dessous est tiré de notre méthode générique DALBase.GetSingeDTO <T>(SqlCommand command). Nous créons un objet de retour de type T, et nous utilisons l'objet command qui a été passé pour obtenir un reader. Si le reader a des lignes, nous appelons Read(). Ensuite, nous utilisons DTOParserFactory pour obtenir un objet parser. La méthode DTOParserFactory.GetParser() prend le type de DTO souhaité comme paramètre et retourne une instance de la classe concrète DTOParser appropriée. À ce moment-là, ce que nous avons à faire est de passer notre reader à notre parser, caster le DTO retourné au type T, et faire un peu de nettoyage.
Dim
dto As
T =
Nothing
command.Connection.Open
(
)
Dim
reader As
SqlDataReader =
command.ExecuteReader
(
)
If
reader.HasRows
Then
reader.Read
(
)
Dim
parser As
DTOParser =
DTOParserFactory.GetParser
(
GetType
(
T))
parser.PopulateOrdinals
(
reader)
dto =
DirectCast
(
parser.PopulateDTO
(
reader), T)
reader.Close
(
)
Else
' S'il n'y a pas de données, nous renvoyons null.
dto =
Nothing
End
If
V. La classe de base DTOParser ▲
Maintenant, nous pouvons enfin revenir à l'écriture de nos classes DTOParser. Nous allons avoir une classe concrète de DTOParser distincte pour chaque type DTO dans notre application. Il y a deux choses que nous devons faire pour chaque DTOParser concret. Tout d'abord, notre parser a besoin d'une méthode qui prend un SqlDataReader et ensuite il obtient et sauvegarde les ordinaux pour tous nos champs de données. Deuxièmement, le parser a besoin d'une méthode qui prend un reader et renvoie un seul DTO alimenté avec les données du reader de l'enregistrement courant. Nous allons définir l'interface de ces deux méthodes en utilisant une classe de base abstraite de DTOParser. Toutes nos classes de DTOParser concrètes seront désormais héritées de DTOParser et mettront en œuvre ces deux méthodes. Notez que le type de retour pour PopulateDTO est DTOBase, qui est le type de base pour l'ensemble de nos DTO.
MustInherit
Class
DTOParser
Public
MustOverride
Sub
PopulateOrdinals
(
ByVal
reader As
SqlDataReader)
Public
MustOverride
Function
PopulateDTO
(
ByVal
reader As
SqlDataReader) As
DTOBase
End
Class
VI. La classe concrète DTOParser_Person ▲
Maintenant, nous pouvons enfin entrer dans notre classe concrète parser pour PersonDTO. DTOParser_Person va encapsuler l'ensemble de notre logique pour obtenir les valeurs des données des champs/colonnes de nos données. La classe doit faire trois choses :
1. Fournir des propriétés pour stocker un ordinal pour chaque champ de données/colonne ;
2. Implémenter la méthode PopulateOrdinals() ;
3. Implémenter la méthode PopulateDTO.
Pour vous rafraîchir la mémoire, voici à quoi ressemble notre classe PersonDTO :
Nous allons donc commencer par créer les propriétés du Ord_DataMemberName pour contenir la valeur ordinale de chacun de nos membres de données PersonDTO. Les propriétés de Ord_DataMemberName sont de type entier. Vous pouvez vous demander pourquoi nous soucier de créer des propriétés pour chaque ordinal, pourquoi ne pas les obtenir à la volée dans notre méthode PopulateDTO() ? La réponse est que nous n'avons pas vraiment besoin de ces propriétés lorsque nous accédons à un seul DTO. Cependant, lorsque nous obtenons une liste de DTO nous voulons être en mesure d'obtenir une instance de notre parser, appeler PopulateOrdinals() une fois, puis appeler PopulateDTO() pour chaque élément de notre liste. Dans cette situation, nous ne remplissons les ordinaux qu'une fois et parce que nous les avons affectés sur des propriétés locales, nous pouvons les utiliser pour chaque appel suivant à PopulateDTO(). La classe DTOParser_Person résultante ressemblera à ceci :
Maintenant, nous devons mettre en œuvre la méthode PopulateOrdinals(). Cette logique est assez simple. Nous prenons une référence à SqlDataReader comme seul paramètre. Le reader dispose d'une méthode GetOrdinal que nous pouvons utiliser pour obtenir la valeur de chaque ordinal par nom de champ/colonne. Nous avons juste besoin de faire cette recherche pour chaque champ/colonne, puis de stocker le résultat dans la propriété Ord_XXX correspondante.
Public
Overrides
Sub
PopulateOrdinals
(
ByVal
reader As
SqlDataReader)
Ord_PersonGuid =
reader.GetOrdinal
(
"person_guid"
)
Ord_PersonId =
reader.GetOrdinal
(
"person_id"
)
Ord_UtcCreated =
reader.GetOrdinal
(
"utc_created"
)
Ord_UtcModified =
reader.GetOrdinal
(
"utc_modified"
)
Ord_Password =
reader.GetOrdinal
(
"password"
)
Ord_Name =
reader.GetOrdinal
(
"name"
)
Ord_Nickname =
reader.GetOrdinal
(
"nickname"
)
Ord_PhoneMobile =
reader.GetOrdinal
(
"phone_mobile"
)
Ord_PhoneHome =
reader.GetOrdinal
(
"phone_home"
)
Ord_Email =
reader.GetOrdinal
(
"email"
)
Ord_ImAddress =
reader.GetOrdinal
(
"im_address"
)
Ord_ImType =
reader.GetOrdinal
(
"im_type"
)
Ord_TimeZoneId =
reader.GetOrdinal
(
"time_zone_id"
)
Ord_LanguageId =
reader.GetOrdinal
(
"language_id"
)
Ord_City =
reader.GetOrdinal
(
"city"
)
Ord_State =
reader.GetOrdinal
(
"state_code"
)
Ord_ZipCode =
reader.GetOrdinal
(
"zip_code"
)
End
Sub
La seule autre chose que nous devons faire est de mettre en œuvre la méthode PopulateDTO(). La logique est aussi simple. La première chose que nous faisons est de créer un nouveau PersonDTO. Rappelez-vous que PersonDTO ainsi que tous les autres types de DTO héritent de DTOBase afin que nous puissions les utiliser comme valeur de retour. Aussi, n'oubliez pas que le constructeur de PersonDTO initialise tous les membres de données à leur valeur nulle de leur type (les valeurs nulles sont définies dans la classe CommonBase). Ainsi, chaque membre de données commence par une valeur nulle, ce qui à notre demande signifie « pas affecté ». Cela veut dire que si un champ ne passe pas le contrôle DBNull, nous n'avons rien à faire pour ce membre de données PersonDTO correspondant, car il est déjà initialisé à sa valeur null.
Ainsi, après avoir créé notre objet PersonDTO, nous avons juste besoin de faire une simple boucle pour chaque membre de données (For Each data member). Premièrement, nous allons utiliser l'ordinal correspondant pour nous assurer que la valeur retournée par le reader n'est pas null. Deuxièmement, si cette valeur n'est pas null, nous allons utiliser la méthode getxxx typée du reader pour obtenir la valeur. Une fois que tous les membres de données du PersonDTO ont été renseignés, nous le renverrons. Le code résultant ressemble à ceci :
Public
Overrides
Function
PopulateDTO
(
ByVal
reader As
SqlDataReader) As
DTOBase
' On suppose que le reader possède des données et est déjà sur la ligne qui contient les données dont nous avons besoin.
' Nous n'avons pas besoin d'appeler read.
' En règle générale, on suppose que chaque champ doit être vérifié null.
' Si un champ est null alors la valeur Nullvalue pour ce champ a déjà été fixée par le constructeur DTO, nous n'avons pas besoin de la changer.
Dim
person As
New
PersonDTO
(
)
' PersonGuid
If
Not
reader.IsDBNull
(
Ord_PersonGuid) Then
person.PersonGuid
=
reader.GetGuid
(
Ord_PersonGuid)
' PersonId
If
Not
reader.IsDBNull
(
Ord_PersonId) Then
person.PersonId
=
reader.GetInt32
(
Ord_PersonId)
' UtcCreated
If
Not
reader.IsDBNull
(
Ord_UtcCreated) Then
person.UtcCreated
=
reader.GetDateTime
(
Ord_UtcCreated)
' UtcModified
If
Not
reader.IsDBNull
(
Ord_UtcModified) Then
person.UtcModified
=
reader.GetDateTime
(
Ord_UtcModified)
' Password
If
Not
reader.IsDBNull
(
Ord_Password) Then
person.Password
=
reader.GetString
(
Ord_Password)
' Name
If
Not
reader.IsDBNull
(
Ord_Name) Then
person.Name
=
reader.GetString
(
Ord_Name)
' Nickname
If
Not
reader.IsDBNull
(
Ord_Nickname) Then
person.Nickname
=
reader.GetString
(
Ord_Nickname)
' PhoneMobile
If
Not
reader.IsDBNull
(
Ord_PhoneMobile) Then
person.PhoneMobile
=
reader.GetString
(
Ord_PhoneMobile)
' PhoneHome
If
Not
reader.IsDBNull
(
Ord_PhoneHome) Then
person.PhoneHome
=
reader.GetString
(
Ord_PhoneHome)
' Email
If
Not
reader.IsDBNull
(
Ord_Email) Then
person.Email
=
reader.GetString
(
Ord_Email)
' ImAddress
If
Not
reader.IsDBNull
(
Ord_ImAddress) Then
person.ImAddress
=
reader.GetString
(
Ord_ImAddress)
' ImType
If
Not
reader.IsDBNull
(
Ord_ImType) Then
person.ImType
=
reader.GetInt32
(
Ord_ImType)
' TimeZoneId
If
Not
reader.IsDBNull
(
Ord_TimeZoneId) Then
person.TimeZoneId
=
reader.GetInt32
(
Ord_TimeZoneId)
' LanguageId
If
Not
reader.IsDBNull
(
Ord_LanguageId) Then
person.LanguageId
=
reader.GetInt32
(
Ord_LanguageId)
' City
If
Not
reader.IsDBNull
(
Ord_City) Then
person.City
=
reader.GetString
(
Ord_City)
' State
If
Not
reader.IsDBNull
(
Ord_State) Then
person.State
=
reader.GetString
(
Ord_State)
' ZipCode
If
Not
reader.IsDBNull
(
Ord_ZipCode) Then
person.ZipCode
=
reader.GetInt32
(
Ord_ZipCode)
' IsNew
person.IsNew
=
False
Return
person
End
Function
VII. Résumé ▲
C'est tout ! Nous avons un DTO ! Nous avons maintenant un framework pour créer, parser et retourner un DTO fortement typé. En raison des optimisations que nous avons prises comme choix de conteneur de données léger, en utilisant SqlDataReader avec les ordinaux et en minimisant les casts, notre DAL sera performante.
En regardant en arrière sur ce code, je me rends compte qu'il y a vraiment pas mal de morceaux. Toutefois, j'ai aussi remarqué que la plupart des morceaux sont très petits et faciles à comprendre. J'essaye d'employer des principes solides, en particulier le principe de la responsabilité unique. En observant une conception DAL comme cela, je pense qu'elle est vraiment rentable en termes de maintenabilité et de lisibilité du code. Quand vous regardez des méthodes comme PopulateOrdinals() ou PopulateDTO(), elles ne font qu'une seule chose. Les noms des méthodes et le code lui-même rendent très évident ce pour quoi ces méthodes sont conçues. Il est facile de voir quel code mettre en œuvre pour différents types de DTO. Je pense que la clarté et l'intelligibilité créées en concevant le code de cette manière vaut bien l'effort supplémentaire qu'il exige.
Voilà pour l'ensemble. La seule chose que je n'ai pas expliquée c'est la classe DTOParserFactory. C'est juste une simple classe de fabrique que j'ai incluse dans le code correspondant ci-dessous ainsi que le code complet de la classe DTOParser_Person.
Friend
NotInheritable
Class
DTOParserFactory
Private
Sub
New
(
)
End
Sub
' GetParser
Friend
Shared
Function
GetParser
(
ByVal
DTOType As
System.Type
) As
DTOParser
Select
Case
DTOType.Name
Case
"PersonDTO"
Return
New
DTOParser_Person
(
)
Case
"PostDTO"
'Return New DTOParser_Post()
Case
"SiteProfileDTO"
'Return New DTOParser_SiteProfile()
End
Select
' Si nous arrivons ici alors c'est que nous n'avons pas réussi à trouver le type correspondant. Nous levons donc une exception.
Throw
New
Exception
(
"Unknown Type"
)
End
Function
End
Class
Class
DTOParser_Person
Inherits
DTOParser
Private
Ord_PersonGuid As
Integer
Private
Ord_PersonId As
Integer
Private
Ord_UtcCreated As
Integer
Private
Ord_UtcModified As
Integer
Private
Ord_Password As
Integer
Private
Ord_Name As
Integer
Private
Ord_Nickname As
Integer
Private
Ord_PhoneMobile As
Integer
Private
Ord_PhoneHome As
Integer
Private
Ord_Email As
Integer
Private
Ord_ImAddress As
Integer
Private
Ord_ImType As
Integer
Private
Ord_TimeZoneId As
Integer
Private
Ord_LanguageId As
Integer
Private
Ord_City As
Integer
Private
Ord_State As
Integer
Private
Ord_ZipCode As
Integer
Public
Overrides
Sub
PopulateOrdinals
(
ByVal
reader As
SqlDataReader)
Ord_PersonGuid =
reader.GetOrdinal
(
"person_guid"
)
Ord_PersonId =
reader.GetOrdinal
(
"person_id"
)
Ord_UtcCreated =
reader.GetOrdinal
(
"utc_created"
)
Ord_UtcModified =
reader.GetOrdinal
(
"utc_modified"
)
Ord_Password =
reader.GetOrdinal
(
"password"
)
Ord_Name =
reader.GetOrdinal
(
"name"
)
Ord_Nickname =
reader.GetOrdinal
(
"nickname"
)
Ord_PhoneMobile =
reader.GetOrdinal
(
"phone_mobile"
)
Ord_PhoneHome =
reader.GetOrdinal
(
"phone_home"
)
Ord_Email =
reader.GetOrdinal
(
"email"
)
Ord_ImAddress =
reader.GetOrdinal
(
"im_address"
)
Ord_ImType =
reader.GetOrdinal
(
"im_type"
)
Ord_TimeZoneId =
reader.GetOrdinal
(
"time_zone_id"
)
Ord_LanguageId =
reader.GetOrdinal
(
"language_id"
)
Ord_City =
reader.GetOrdinal
(
"city"
)
Ord_State =
reader.GetOrdinal
(
"state_code"
)
Ord_ZipCode =
reader.GetOrdinal
(
"zip_code"
)
End
Sub
Public
Overrides
Function
PopulateDTO
(
ByVal
reader As
SqlDataReader) As
DTOBase
' On suppose que le reader possède des données et est déjà sur la ligne qui contient les données dont nous avons besoin.
' Nous n'avons pas besoin d'appeler read.
' En règle générale, on suppose que chaque champ doit être vérifié null.
' Si un champ est null alors la valeur Nullvalue pour ce champ a déjà été fixée par le constructeur DTO, nous n'avons pas besoin de la changer.
Dim
person As
New
PersonDTO
(
)
' PersonGuid
If
Not
reader.IsDBNull
(
Ord_PersonGuid) Then
person.PersonGuid
=
reader.GetGuid
(
Ord_PersonGuid)
' PersonId
If
Not
reader.IsDBNull
(
Ord_PersonId) Then
person.PersonId
=
reader.GetInt32
(
Ord_PersonId)
' UtcCreated
If
Not
reader.IsDBNull
(
Ord_UtcCreated) Then
person.UtcCreated
=
reader.GetDateTime
(
Ord_UtcCreated)
' UtcModified
If
Not
reader.IsDBNull
(
Ord_UtcModified) Then
person.UtcModified
=
reader.GetDateTime
(
Ord_UtcModified)
' Password
If
Not
reader.IsDBNull
(
Ord_Password) Then
person.Password
=
reader.GetString
(
Ord_Password)
' Name
If
Not
reader.IsDBNull
(
Ord_Name) Then
person.Name
=
reader.GetString
(
Ord_Name)
' Nickname
If
Not
reader.IsDBNull
(
Ord_Nickname) Then
person.Nickname
=
reader.GetString
(
Ord_Nickname)
' PhoneMobile
If
Not
reader.IsDBNull
(
Ord_PhoneMobile) Then
person.PhoneMobile
=
reader.GetString
(
Ord_PhoneMobile)
' PhoneHome
If
Not
reader.IsDBNull
(
Ord_PhoneHome) Then
person.PhoneHome
=
reader.GetString
(
Ord_PhoneHome)
' Email
If
Not
reader.IsDBNull
(
Ord_Email) Then
person.Email
=
reader.GetString
(
Ord_Email)
' ImAddress
If
Not
reader.IsDBNull
(
Ord_ImAddress) Then
person.ImAddress
=
reader.GetString
(
Ord_ImAddress)
' ImType
If
Not
reader.IsDBNull
(
Ord_ImType) Then
person.ImType
=
reader.GetInt32
(
Ord_ImType)
' TimeZoneId
If
Not
reader.IsDBNull
(
Ord_TimeZoneId) Then
person.TimeZoneId
=
reader.GetInt32
(
Ord_TimeZoneId)
' LanguageId
If
Not
reader.IsDBNull
(
Ord_LanguageId) Then
person.LanguageId
=
reader.GetInt32
(
Ord_LanguageId)
' City
If
Not
reader.IsDBNull
(
Ord_City) Then
person.City
=
reader.GetString
(
Ord_City)
' State
If
Not
reader.IsDBNull
(
Ord_State) Then
person.State
=
reader.GetString
(
Ord_State)
' ZipCode
If
Not
reader.IsDBNull
(
Ord_ZipCode) Then
person.ZipCode
=
reader.GetInt32
(
Ord_ZipCode)
' IsNew
person.IsNew
=
False
Return
person
End
Function
End
Class
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▲
Traduction de l'article de M. Rudy Lacovara — High Performance Data Access Layer Architecture Part 3