Architecture de couche d'accès aux données (DAL) de hautes performances — Partie 1

DTO : Data Transfert Object (Objet de transfert de données)

Cette série de trois articles décrit comment écrire une couche d'accès aux données de hautes performances (DAL).

(Exemple de code en VB.NET)

Partie 1

DTO, DAL, BLL… Tout le monde s'en sort ? Pas si sûr !

Voici le premier chapitre d'un article qui vous aidera à comprendre cette architecture et ses concepts-clés.

Dans cette première partie, nous allons nous intéresser à l'architecture globale de la DAL et l'utilisation des DTO pour transférer des données entre les différentes couches de l'application.

Nous allons aussi voir la mise en pratique de ces concepts à l'aide d'une classe PersonDB qui contiendra l'ensemble de nos méthodes d'accès aux données permettant d'obtenir et de sauvegarder les données d'une entité « personne ».

Commentez cet article : 26 commentaires Donner une note à l'article (5)

Article lu   fois.

Les deux auteur et traducteur

Traducteur : Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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.

Image non disponible
Diagramme de classe PersonDTO et 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.

Définition des valeurs NULLES communes
Sélectionnez
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).

Classe de base
Sélectionnez
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.

Classe de transfert de données
Sélectionnez
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 :

 
Sélectionnez
 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

 
Sélectionnez
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.

Image non disponible
Diagramme des classes de la DAL.

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 :

Code de la méthode GetPersonByPersonGuid()
Sélectionnez
 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 :

Code de la méthode GetPersonByEmail()
Sélectionnez
 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.

Code de la méthode GetAll()
Sélectionnez
 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.

Classe PersonDb
Sélectionnez
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

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2013 Hervé Taraveau. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.