Blog ENI : Toute la veille numérique !
🐠 -25€ dès 75€ 
+ 7 jours d'accès à la Bibliothèque Numérique ENI. Cliquez ici
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. Delphi 10.3
  3. Attributs, annotations et RTTI
Extrait - Delphi 10.3 Programmation orientée objet en environnement Windows
Extraits du livre
Delphi 10.3 Programmation orientée objet en environnement Windows
9 avis
Revenir à la page d'achat du livre

Attributs, annotations et RTTI

Introduction

Dans ce chapitre vont être utilisées conjointement deux fonctionnalités de Delphi : les attributs et le mécanisme de réflexion RTTI. Le cas d’application de l’utilisation de ces notions est d’améliorer l’accès aux données de l’application d’achats d’objets du chapitre précédent en mettant au point un moteur d’accés aux données plus générique. Par plus générique on entend minimiser la construction de requêtes SQL spécifiques ne s’appliquant qu’à tel ou tel modèle de données. Le but est d’obtenir une approche "plus objet" de la manipulation de la base de données comme peuvent le proposer certains ORM très connus comme Hibernate pour Java ou Entity Framework en .NET. ORM signifie Object Relationel Mapping et son principe est de construire une correspondance (Mapping) entre un schéma de base de données et un modèle de données. L’ORM fournit son propre moteur de génération de requête SQL qui permet (en théorie) au développeur de ne presque plus écrire de requête SQL lui-même. Ceci induit un gain de temps de développement non négligeable mais il faut surveiller où le curseur est positionné entre le "tout automatique"...

Principe du mapping propriété/colonne

Que ce soit en lecture (select) ou écriture (update/insert), on remarque dans les contrôleurs ce genre de ligne :

function TUserController.UpdateUser:TErrorCode; 
.... 
     aQuery.ParamByName('afirstname').AsString := 
FCurrentUser.FirstName; 
     aQuery.ParamByName('alastname').AsString := 
FCurrentUser.LastName; 

ou

function TUserController.LogUser(login, password: string;out 
anError:TErrorCode): TUser; 
.... 
     aUser := TUser.Create; 
     aUser.ID := aquery.FindField('id').AsInteger; 
     aUser.Login := aquery.FindField('login').AsString; 
     aUser.Password := aquery.FindField('password').AsString; 

On manipule par exemple sur la classe TUser la propriété FirstName dont la valeur va dans la colonne first_name de la table users. Idem pour LastName et les autres propriétés.

type TEntity = class(TObject) 
 private 
   FDBField1 : string; 
 public 
   property DBField1 : string read FDBField1  write FDBField1; 
end; 

Dans ce code, on crée une classe dans laquelle on veut que la propriété DBField1 soit en relation avec une colonne d’une table dont le nom est également DBField1.

Ainsi, en se forçant à...

Attributs Delphi

1. Généralités

Dans Delphi, les attributs se définissent comme une classe et possèdent une implémentation. Ils héritent de la classe de base TCustomAttribute.

Ils s’utilisent en écrivant une annotation correspondant à cet attribut sur une classe ou n’importe quel membre d’une classe.

Il existe une règle de nommage qui suggère de ne pas terminer son nom de classe d’attribut par Attribute car Delphi supprime implicitement le suffixe Attribute en interne et cela peut poser des problèmes d’accessibilité. Par exemple, on déclare deux descendants de TCustomAttribute TMyTestAttribute et TMyTest. Dans ce cas, l’implémentation de la classe TMyTest ne sera jamais accessible.

Pour un plein fonctionnement, il est conseillé de positionner toutes les fonctions RTTI en ajoutant la directive de compilation {$RTTI}.

2. Modélisation

La définition d’une classe attribut TAttributeExemple est :

 type 
   TAttributeExemple = class(TCustomAttribute) 
 end; 

Pour exploiter cet attribut, on écrit une annotation [TAttributeExemple] pour enrichir une classe ou un champ.

type 
  [TAttributeExemple] 
  TMyClass1 = class(TObject) 
 
 
  TMyClass2  = class 
       [TAttributeExemple] 
       procedure DoSomething; 
  end; 

Étant donné que les attributs se définissent comme une classe, ils peuvent avoir plusieurs constructeurs et peuvent accepter des champs dans leur définition et implémentation.

Ce qu’on appelle "entité" ici représente un modèle de données. Ce modèle de données est stocké dans une table et chaque propriété est reliée à une colonne de la table.

Deux classes attributs seront définies : une pour configurer le nom de table (TTableName) qui sera associé à la classe de l’entité et l’autre pour configurer le nom de la colonne (TDBColumn) et éventuellement le type d’accès quand il s’agit d’une colonne de type Blob qui stocke du binaire.

Donc, de manière générique, en s’affranchissant de la contrainte d’avoir...

Un moteur d’accès aux données générique

1. Implémentation

La responsabilité de TDBEntityBase est d’initialiser l’entité en créant en mémoire la liste des propriétés, un dictionnaire reliant les noms de propriétés et les noms de colonnes.

type TDBEntityBase=class(TPersistent) 
  private 
  protected      
    FPropAccessorsList:TDictionary<TRttiProperty,TAccessors>; 
    FRttiCtx:TRttiContext; 
    FPropList :TList<TRttiProperty>; 
    FPropNameColumnNameList :TDictionary<string,string>; 
    FTableName:string; 
    procedure InitPropList; 
  public 
    constructor Create;reintroduce; 
    destructor Destroy;override; 
end; 

Voici le code concernant l’implémentation :

{ TDBEntityBase } 
 
 
constructor TDBEntityBase.Create(); 
begin 
 inherited Create; 
FRttiCtx:=TRttiContext.Create; 
 FPropList := TList<TRttiProperty>.Create; 
 FPropNameColumnNameList :=TDictionary<string,string>.Create; 
 FPropAccessorsList:=TDictionary<TRttiProperty,TAccessors>.Create; 
 InitPropList; 
end; 
 
destructor TDBEntityBase.Destroy; 
begin 
 FRttiCtx.Free; 
 FPropAccessorsList.Free; 
 FPropNameColumnNameList.Free; 
 inherited; 
end; 

Le constructeur initialise les dictionnaires et le contexte Rtti et appelle la méthode d’initialisation InitPropList. Le destructeur détruit les instances précitées.

L’implementation de la méthode TDBEntityBase.InitPropList est écrite ci-dessous. Sa responsabilité est de récupérer le nom de la table associée à la classe en lisant l’attribut TTableName et d’initialiser le dictionnaire gérant la correspondance nom de propriété <=> nom de colonne en lisant les attributs TDBColumn.

procedure TDBEntityBase.InitPropList; 
var 
   aProp : TRttiProperty; 
   aRttiType :TRttiType; 
   RTTIAttr: TCustomAttribute; 
   aValue:TValue; 
   columnName,anAccessRead,anAccessWrite:string; ...

Conclusion

L’implémentation de la couche générique n’est pas forcément très simple mais elle apporte certains avantages concernant les points suivants :

  • La facilité d’évolution de codes : si on change de nom d’un champ, on ne reprend pas toutes les requêtes, le changement dans l’annotation doit suffire. 

  • Rajouter des tables et des entités devient relativement aisé : une simple configuration de la classe permet d’interfacer la nouvelle table et la ou les nouvelles relations.

  • On minimise et centralise les endroits où du code SQL est créé et exécuté, ce qui facilite la maintenance.

La généricité s’obtient toujours au détriment de la performance. Il faut rester raisonnable et trouver le bon curseur entre ces deux concepts importants de l’informatique, qui se trouvent souvent à deux opposés par rapport aux besoins réels de l’application.