février
2009
Dans cet onzième épisode nous allons voir comment utiliser SQL pour stocker les objets dans une base de données.
Vous aurez sans doute remarqué dans la livraison précédente que le code des actions de création/modification/suppression est fortement lié à la persistance à l’aide d’une StringList. Nous verrons plus tard qu’il nous faudra retravailler le code afin qu’il fonctionne indifféremment avec une StringList ou une base de données.
Nous y reviendrons après avoir construit les visiteurs pour mettre à jour, insérer et effacer un objet dans une base de données.
2.2 – Persistance SQL (Firebird/Interbase)
Pour la sauvegarde des objets dans une base de données, nous allons changer de stratégie. Ce n’est pas la liste People des personnes qui sera écrite en bloc dans la base, mais chaque objet de la classe TPerson au fur et à mesure de sa création, de sa modification ou de sa suppression. Par contre nous continuerons à lire la liste de personnes en une fois au lancement de l’application, à partir de la table People.
Nous lisons toute la table au début, afin de pouvoir afficher la liste des personnes dans la ListBox. Cette liste peut ne pas afficher tous les champs de la table. Il faudra alors adapter la « décorateur » de l’objet TPeople décrit précédemment (voir en 1.4).
Commençons par le plus simple : la lecture des données.
Nous allons écrire un visiteur de l’objet de la classe TPeople pour charger la liste à partir de la base de données. Pour les premiers tests nous aurons besoin d’une base de données placée dans un fichier PeopleDB.fdb (base de données Firebird). Vous pouvez ausssi utiliser Interbase sans modifier grand chose au code.
Avant toute chose, il nous faut créer la table People dans la base PeopleDB, avec les champs suivants :
<strong>LastName</strong> de type VARCHAR(20),
<strong>FirstName</strong> de type VARCHAR(20) et
<strong>BirthDate</strong> de type DATE.
OID sera défini comme clé primaire. Une clé UNIQUE composée de LastName, FirstName et BirhDate peut s’avérer utile pour éviter les doublons.
Notre premier objectif est d’afficher la liste des personnes à partir du fichier PeopleDB.fdb que nous venons de créer. Nous créons à cette fin un visiteur de TPeople appelé TSQLReadPeople.
2.2.1 – Visiteur TSQLReadPeople.
Pour le visiteur de lecture de la table People, je me suis contenté de reproduire la solution élaborée par TechInsite. Je l’aime pour sa simplicité et sa limpidité.
protected
procedure Init; override;
procedure MapRowToObject; override;
procedure SetupParams; override;
public
constructor Create(var aFileName: TFileName); overload; override;
destructor Destroy; override;
procedure VisitBizObj(Visited: TBizObj); override;
end;
Nous avons 3 méthodes protégées et 3 méthodes publiques héritées toutes les six de TSQLRead, ancêtre de tous les visiteurs qui lisent une table SQL. La hiérarchie est la suivante:
La connexion à la base s’effectue dans le constructeur Create et la déconnexion dans le destructeur Destroy. Comme ces opérations sont communes à tous les visiteurs d’accès à la base de données, que ce soit en lecture ou en écriture, la connexion et la déconnexion sont implémentées au niveau de l’ancêtre TSQLFirebird.
Cette façon de procéder a un inconvénient majeur : à chaque création/modification/suppression de chaque objet, il y a connexion puis déconnexion, ce qui ralentit fortement l’opération. Nous modifierons cette façon de faire ultérieurement.
2.2.1.1 – Connexion à la base
Comme les constructeurs des descendants de TSQLFirebird font appel au constructeur hérité (inherited Create
), c’est le code ci-dessous qui est exécuté à la création de tout visiteur d’accès à une base de données, ici Firebird.
begin
inherited Create(aFileName);
// aFileName must have .fdb extension
if ExtractFileExt(aFileName) <> '.fdb' then
ChangeFileExt(aFileName, '.fdb');
FDatabaseName := aFileName;
//connexion à la base de données
FDB := TIBDatabase.Create(nil);
with FDB do
begin
DatabaseName := FDatabaseName;
Params.Clear;
Params.Add('user_name=USER');
Params.Add('password=password');
LoginPrompt := False;
Connected := True;
end;
FTransaction := TIBTransaction.Create(nil);
FTransaction.DefaultDatabase := FDB;
FDB.DefaultTransaction := FTransaction;
FQuery := TIBQuery.Create(nil);
with FQuery do
begin
Database := FDB;
Transaction := FTransaction;
SQL.Clear;
end;
end;
Le code a été obtenu à partir d’une petite application RAD d’accès à la base de données. Le code généré par Delphi a été copié/collé dans le construteur Create ci-dessus. Y ont été ajoutées les 3 premières instructions:
- l’appel du constructeur de l’ancêtre,
- la vérification de l’extension du nom du fichier de base de données, et sa rectification éventuelle,
- le stockage du nom de la base, avec son chemin d’accès, dans le champ FDatabaseName de
Le destructeur ci-dessous se contente de libérer les objets créés dans le constructeur.
begin
FQuery.Free;
FTransaction.Free;
FDB.Free;
inherited Destroy;
end;
Aucune autre méthode n’est implémentée au niveau de FSQLFirebird, mis à part l’appel inherited à la méthode de l’ancêtre.
Revenons maintenant à notre visiteur TSQLReadPeople.
2.2.1.2 – Lecture de la table
Les 3 méthodes publiques ne sont pas implémentées à ce niveau et font donc appel à l’ancêtre.
Les 3 méthodes protégées sont implémentées dans TSQLReadPeople.
begin
FQuery.SQL.Clear;
FQuery.SQL.Add('SELECT * FROM People ORDER BY OID;');
TPeople(FVisited).List.Clear;
end;
La méthode Init se contente d’écrire le code SQL dans FQuery.SQL ; comme il n’y a pas de paramètre dans le code SQL, la méthode SetupParams reste vide. Des paramètres pourraient être utilisés pour remplacer OID dans l’instruction SELECT afin de trier les données selon un autre critère, par exemple le Nom (LastName) et le Prénom (FirstName), voire la date de naissance.
begin
// pas de paramètres à ce stade
end;
La méthode MapRowToObject est chargée d’affecter chaque champ d’un enregistrement de la table récupéré par FQuery au champ correspondant de l’objet de type TPerson, pour finalement ajouter l’objet à la liste des personnes de type TPeople :
var
IData: TPerson;
begin
IData := TPerson.Create;
IData.OID := FQuery.FieldByName('OID').AsInteger;
IData.LastName := FQuery.FieldByName('LastName').AsString;
IData.FirstName := FQuery.FieldByName('FirstName').AsString;
IData.BirthDate := FQuery.FieldByName('BirthDate').AsDateTime;
TPeople(FVisited).Add(IData);
end;
Le code d’itération des enregistrements de la table est implémenté au niveau de TSQLRead, parce qu’il sert à tous les visiteurs destinés à lire les enregistrements d’une table.
2.2.1.3 – Parcours de la table
C’est donc le code ci-dessous qui est exécuté lorsqu’on applique le visiteur TSQLReadPeople à l’objet TPeople.
begin
inherited VisitBizObj(Visited);
Init; // Code SQL ; implémenté dans la classe concrète
SetupParams; // Initialise les paramètres du code SQL. Implementé dans la classe concrète
FQuery.Active := True;
while not FQuery.EOF do begin
MapRowToObject;//"Mappe" une requête avec un objet. Implementé dans la classe concrète
FQuery.Next ;
end ;
Finalize;
end;
En premier lieu il est fait appel à la méthode-ancêtre. En remontant la hiérarchie, on remarquera que cela a pour effet de stocker la valeur de Visited dans le champ FVisited qui sera ainsi disponible pour toutes les méthodes de tous les descendants.
Ensuite on exécute Init et SetupParams de TSQLReadPerson, puis on lance la requête en activant FQuery.
Finalement on parcourt les lignes renvoyées par FQuery et on en extrait les champs avec la méthode MapRowToObject, celle implémentée dans TSQLReadPeople.
N’oublions pas d’ajouter le code nécessaire dans la méthode ReadPeopleFromFile de FormRepert :
var
CsvRead: TCsvTextFileRead;
TxtRead: TFixedTextFileRead;
FileExt: String;
SqlRead: TSQLReadPeople;
begin
FileExt := ExtractFileExt(FN) ;
if FileExt = '.csv' then begin
CsvRead := TCsvTextFileRead.Create(FN);
try
People.AcceptBizObjVisitor(CsvRead);
finally
CsvRead.Free;
end;
end else if FileExt = '.txt' then begin
TxtRead := TFixedTextFileRead.Create(FN);
try
People.AcceptBizObjVisitor(TxtRead);
finally
TxtRead.Free;
end;
end else if FileExt = '.fdb' then begin
SqlRead := TSQLReadPeople.Create(FN);
try
People.AcceptBizObjVisitor(SqlRead);
finally
SqlRead.Free;
end;
end else
ShowMessage('Format de fichier incorrect (extension '+FileExt+')');
end;
Efficace, non ?