IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Tutoriel pour apprendre comment ajouter à la bibliothèque SQLite un objet de type Table
Par tourlourou

Le , par tourlourou

0PARTAGES

Introduction

Pour manipuler plus commodément les données issues d'une requête (champs), y accéder facilement (selon leur type) et permettre des recherches, un conteneur intermédiaire de type Table ou DataSet s'imposait.
Cet ensemble de données devait pouvoir être affiché facilement dans une grille.
Et idéalement, les modifications de valeurs dans la table ou la grille devaient pouvoir être répercutées :


  • vers la grille ou la table d'une part,
  • vers la base d'autre part.
Tout en restant simple pour l'utilisateur, bien sûr.

Principes

Nous allons envisager les différents aspects de cette évolution de la librairie, des besoins aux solutions.

Le DataSet TlySQLiteTable :

Si l'on voit un DataSet comme tabulaire (c'est une grille de champs côté affichage), l'objet correspond à un tableau à deux dimensions de TlyField (une fois adaptés à ce nouvel usage).
On doit pouvoir accéder à ses champs par index Field[aCol, aRow: integer] ou nom FieldByName[aName: string; aRow: integer], et selon leur type au moyen de ColMetaData[Index: integer].
Il doit permettre à titre de commodité de sélectionner un sous-ensemble de lignes selon la valeur d'un champ Locate(aValue, aFieldName: string) puis d'y accéder individuellement SubsetFieldByName[aName: string; aIndex: integer].
Il doit être capable de s'afficher dans un TStringGrid ToStringGrid(aGrid: TStringGrid; aBiDiUpdate: Boolean; aWithFieldName: Boolean = True; aWithRowNumber: Boolean = True ) avec une option de mise à jour bidirectionnelle en lui fournissant une CallBack.

Pour tout ceci, il sera capable de fournir à l'objet TlySQLiteDB qui encapsule la base les CallBacks qui l'alimenteront, et de la mettre à jour sur option property UpdateIfModified: Boolean en cas de changement de valeur d'un champ.

Mise à jour de la base en cas de modification du DataSet :

C'est une opération qui fait appel à l'élaboration par la Table d'une simple requête SQL de type UPDATE tablename SET fieldname = value WHERE condition. Par sécurité, il faut être sûr que la condition ne se vérifie que pour le seul champ modifié.

:fleche: Le problème se règle simplement en n'autorisant la condition que sur une clef primaire de la même table. C'est une limitation, mais pas pour mon usage.

La Table doit donc être en mesure de vérifier si un de ses champs répond aux exigences. Pour cela, l'interface développée jusqu'ici pour les requêtes n'est pas suffisante, mais l'API SQLite le permet.

Extension du wrapper aux requêtes préparées :

Pour une meilleure efficacité, SQLite permet de pré-compiler des requêtes, puis de les exécuter pas à pas, les relancer, etc. La récupération des champs du résultat d'une requête de type SELECT intègre des fonctions typées et également des fonctions qui donnent accès aux métadonnées des colonnes (origine du champ, type, etc.).

Ces métadonnées permettent à la Table de trouver si un de ses champs est utilisable pour mettre à jour dans la base une donnée qui lui a été modifiée, c'est-à-dire qui constitue une clef primaire de la même table.

Regroupement du code :

Jusqu'ici, le développement de la librairie, modulaire, s'était fait en ajoutant les unités nécessaires au fur et à mesure. Je suis donc parti sur la même base, ajoutant aux cinq unités : lySQLite3PrepIntf pour les requêtes préparées, lySQLite3Table pour l'objet table, et lySQLite3DBTable pour la liaison base-table.

8 unités, pour quelqu'un cherchant à faire simple, ça ne collait pas ! Aussi ai-je commencé par regrouper le code en 3 unités seulement :

  • lySQLiteIntf pour le wrapper SQLite ;
  • lySQLiteFields pour les objets annexes hébergeant des données (TlyField, TlyParamSQL et TlySQLiteTable) ;
  • lySQLiteDB pour l'objet TlySQLiteDB qui encapsule les accès à la base.


    Interface SQLite des requêtes préparées

    Elle se situe dans la droite ligne de la philosophie de l'API en termes de style de déclarations et d'utilisation.

    Principe :

    SQLite peut « compiler, » ou « préparer » des requêtes, c'est-à-dire les analyser et les transformer en une succession d'opcodes exécutables par le moteur interne, véritable machine virtuelle intermédiaire. L'intérêt est de gagner en rapidité pour des requêtes lancées plusieurs fois.

    Ici, l'intérêt fondamental réside pour nous dans l'API spécifique de la récupération des données d'une requête préparée. En effet, son résultat se parcourt une ligne après l'autre, champ par champ (mais sans ordre), et pour plus d'efficacité, selon leur type.

    La librairie SQLite pour Windows 32 bits ayant été compilée avec l'option SQLITE_ENABLE_COLUMN_METADATA, elle offre l'accès à des métadonnées telles que nom de la base, de la table, du champ, type de donnée, etc.

    Cette possibilité est à la base de la synchronisation entre DataSet et BD, qui nécessite de garder trace de l'origine de la donnée pour pouvoir la mettre à jour.

    SQLite propose plusieurs fonctions :
    • tout d'abord pour préparer une requête (sqlite3_prepare_v2) représentée par un objet sqlite3_stmt (Statement) qu'il faut libérer en fin d'utilisation de la ressource (sqlite3_finalize) ;
    • puis pour l'exécuter pas à pas (sqlite3_step) ou la réinitialiser (sqlite3_reset) ;
    • la récupération des métadonnées est accessible dès après préparation de la requête : nombre de colonnes du résultat (sqlite3_column_count) ; noms de la base, de la table, du champ, de son alias, du type déclaré au CREATE pour chaque colonne. Afin d'étoffer les métadonnées recueillies, on peut accéder à d'autres caractéristiques pour chaque colonne (sqlite3_table_column_metadata) ;
    • pour obtenir les valeurs des champs, on dispose de multiples fonctions selon le type de la donnée à récupérer (obtenu dans les métadonnées), SQLite essayant de transtyper si nécessaire.

    :alerte: SQLite utilise un typage dynamique qui permet de stocker n'importe quel type dans n'importe quelle colonne, ce qui n'est pas une raison pour faire n'importe quoi !


    :fleche: Nous ne nous intéresserons ici qu'au système Windows 32 Bits, en excluant Windows CE, dont les appels système sont un peu différents.

    Code :

    Il a été rassemblé dans une seule unité (lySQLiteIntf.pas) qui est le wrapper pour la librairie, puis enrichie des nouvelles API.
    Le code débute par les constantes qui définissent les exigences de versions (d'où l'intérêt de les rappeler) :

    unit lySQLiteIntf;

    interface

    uses
    SysUtils;

    const
    // liées à ce wrapper et la version minimale de la dll
    DllName = 'sqlite3.dll' ; // Library Name
    sMinVersion = '3.7.17';
    MinVersion = 3007017;
    IO_Version = 3;
    VfsVersion = 3;
    Win32VFSName = 'win32';
    WinFileStructLength = 72;

    Suivent les autres constantes, dont quelques nouvelles :

    // types de champs
    SQLITE_INTEGER = 1;
    SQLITE_FLOAT = 2;
    SQLITE_TEXT = 3;
    SQLITE3_TEXT = 3;
    SQLITE_BLOB = 4;
    SQLITE_NULL = 5;

    Puis les définitions des types et fonctions déjà vus (non rappelés) auxquels on ajoute le nouveau type PStatement, pointant sur une requête préparée, ainsi que les fonctions permettant sa gestion :

    type
    PStatement = Pointer; // sur requête préparée (précompilée)

    // préparation d'une requête
    // aSQLLength (#0 inclus) : SQL lu jusqu'au 1° #0 ou cette longueur ; -1 => jusqu'au premier #0
    // aStatement : vaut nil si erreur ; doit être finalisé (méthode utilisable sans erreur sur nil)
    // aTail pointe sur la suite non lue du SQL dans aSQL
    function sqlite3_prepare_v2(aDB: PSQLiteDB; aSQL: PChar; aSQLLength: integer; var aStatement: PStatement; var aTail: PChar): integer; cdecl; external DllName;

    // libération des ressources en fin d'utilisation
    // ne jamais utiliser un Statement finalisé sous peine de risquer des fuites mémoires ou corruptions
    function sqlite3_finalize(aStatement: PStatement): integer; cdecl; external DllName;

    // exécution pas à pas (ligne par ligne)
    // retourne SQLITE_ROW à chaque ligne de résultat disponible
    // et SQLITE_DONE en fin d'exécution ; sinon, code d'erreur.
    // ne pas réutiliser un statement après SQLITE_DONE sans faire de sqlite3_reset
    function sqlite3_step(aStatement: PStatement): integer; cdecl; external DllName;

    // RAZ statement pour une nouvelle exécution (ne sera pas exploité pour ToTable)
    // attention : ne RAZ pas les paramètres (utiliser sqlite3_clear_bindings, non wrappé)
    function sqlite3_reset(aStatement: PStatement): integer; cdecl; external DllName;

    // nb de colonnes du résultat (0 pour un UPDATE, par ex.)
    function sqlite3_column_count(aStatement: PStatement): integer; cdecl; external DllName;

    // même chose, mais après chaque SQLITE_ROW ; intérêt ?
    function sqlite3_data_count(aStatement: PStatement): integer; cdecl; external DllName;

    // récupération des métadonnées
    // la dll a été compilée avec l'option SQLITE_ENABLE_COLUMN_METADATA
    // qui permet notamment de retrouver des données de colonnes lors d'un SELECT

    // aDBName pê NULL => recherche parmi toutes les bases attachées
    // aPrimaryKey True if column part of PK
    function sqlite3_table_column_metadata(aDB: PSQLiteDB; aDBName, aTableName, aColumnName: PChar;
    var aDataType, aCollSeq: PChar; var aNotNull, aPrimaryKey, aAutoInc: LongBool): integer; cdecl; external DllName;

    // noms retournés dé-aliasés et valides jusqu'à Finalize, Step ou demandés en WideChar (interface non faite ici)
    function sqlite3_column_database_name(aStatement: PStatement; aCol: integer): PChar; cdecl; external DllName;
    function sqlite3_column_table_name(aStatement: PStatement; aCol: integer): PChar; cdecl; external DllName;
    // origin_name = field name
    function sqlite3_column_origin_name(aStatement: PStatement; aCol: integer): PChar; cdecl; external DllName;
    // nom de l'alias quand il y a un AS dans un SELECT (indéfini sinon)
    function sqlite3_column_name(aStatement: PStatement; aCol: integer): PChar; cdecl; external DllName;
    // nom du type lors du CREATE
    function sqlite3_column_decltype(aStatement: PStatement; aCol: integer): PChar; cdecl; external DllName;
    // avant cast éventuel
    function sqlite3_column_type(aStatement: PStatement; aCol: integer): integer; cdecl; external DllName;

    // récupération des valeurs elles-mêmes
    // nb d'octets du champ ; 0 si NULL ; n si BLOB ou UTF-8 (#0 terminal exclu) ; n après conversion numérique -> UTF-8
    function sqlite3_column_bytes(aStatement: PStatement; aCol: integer): integer; cdecl; external DllName;
    function sqlite3_column_int(aStatement: PStatement; aCol: integer): integer; cdecl; external DllName;
    function sqlite3_column_int64(aStatement: PStatement; aCol: integer): Int64; cdecl; external DllName;
    function sqlite3_column_double(aStatement: PStatement; aCol: integer): Double; cdecl; external DllName;
    function sqlite3_column_text(aStatement: PStatement; aCol: integer): PChar; cdecl; external DllName;
    function sqlite3_column_blob(aStatement: PStatement; aCol: integer): TBytes; cdecl; external DllName;

    end;

    Rien d'extraordinaire ! Je n'ai pas traduit l'interface SQLite gérant les paramètres dans les requêtes préparées, puisque j'avais développé un objet (cf. TlyParamSQL dans le second billet) qui – lui - est également utilisable en dehors du contexte de ces requêtes préparées.

    Et pour finir, rappelons le code qui restreint à une version minimale de la dll :
    implementation

    initialization
    if sqlite3_libversion_number < MinVersion
    then raise Exception.Create('Cryptage prévu seulement pour SQLite '+sMinVersion+' ou supérieur ; ici : ' + sqlite3_libversion);

    finalization
    // DoNone

    end.

    Adaptation des champs TlyField

    Les contraintes apportées par le DataSet ont conduit à des modifications mineures ou enrichissements des champs destinés maintenant aussi à former le tableau de données.

    Parmi les points les plus significatifs, un champ doit désormais :

    • conserver une référence à la table à laquelle il appartient, sa position dedans (ligne et colonne), et pouvoir lui signaler toute modification de sa valeur ;
    • gérer le type Blob, implémenté au travers d'un accès par un flux (TStream).


    Quelques champs et méthodes ont donc été ajoutés à l'objet, obligeant à une pré-déclaration :

    // pré-declaration
    TlySQLiteTable = class;

    TlyField = class
    private
    FName: string;
    FText: string;
    FNull: Boolean;
    bText: Boolean;
    FType: TlyFieldType;
    // nouvelles déclarations
    FStream: TMemoryStream;
    FTable: TlySQLiteTable;
    FRow, FCol: integer;
    FOnValueChange: TValidateEntryEvent;
    protected
    procedure SetNull(aValue: Boolean);
    procedure SetText(aValue: string);
    procedure SetSQL(aValue: string);
    function GetSQL: string;
    procedure SetInt(aValue: int64);
    function GetInt: int64;
    procedure SetFloat(aValue: Double);
    function GetFloat: Double;
    procedure SetBool(aValue: Boolean);
    function GetBool: Boolean;
    procedure SetTime(aValue: TDateTime);
    function GetTime: TDateTime;
    // nouvelles déclarations
    function GetCanUpdateDB: Boolean;
    function GetIsBlob: Boolean;
    public
    constructor Create; overload;
    constructor Create(aName: string); overload;
    constructor Create(aName: string; aType: TlyFieldType); overload;
    destructor Destroy; override;
    procedure Clear;
    // nouvelles déclarations
    function BlobToStream(var aStream: TStream): Boolean;
    procedure StreamToBlob(aStream: TStream);
    property Name: string read FName; // affectable seulement par le constructeur
    property Row: integer read FRow;
    property Col: integer read FCol;
    property Table: TlySQLiteTable read FTable;
    property FieldType: TlyFieldType read FType; // affectable seulement par le constructeur ou le type de la valeur affectée
    property IsText: Boolean read bText;
    property IsBlob: Boolean read getIsBlob;
    property IsNull: Boolean read FNull write SetNull;
    property CanUpdateDB: Boolean read getCanUpdateDB;
    property AsSQL: string read GetSQL write SetSQL; // par défaut UTF-8 pour SQLite 3.7.13 ; quoté si nécessaire (texte)
    property AsText: string read FText write SetText; // valeur brute de la chaîne, sans quotes éventuelles
    property AsInteger: int64 read GetInt write SetInt; // stockage interne dans SQLite sous forme d'entier de 1 à 8 octets
    property AsFloat: Double read GetFloat write SetFloat; // c'est le format de stockage interne des réels dans SQLite
    property AsBoolean: Boolean read GetBool write SetBool; // stockage interne en tant qu'entier : False=0 et True=1
    property AsDateTime: TDateTime read GetTime write SetTime; // pas de stockage par défaut dans SQLite : traité comme réel

    La gestion du cycle de vie d'un champ reste simple :

    // ne peut servir qu'à rendre NULL : l'inverse se fait en affectant qqch
    procedure TlyField.SetNull(aValue: Boolean);
    begin
    if aValue
    then begin
    SetText('NULL'); // appellera la CallBack de mise à jour
    bText:=False; // car mot réservé
    FNull:=True;
    end;
    end;

    procedure TlyField.Clear;
    begin
    SetNull(True);
    FType:=ftUnknown;
    FStream.Clear;
    // on préserve la synchro FCol et FRow d'un champ d'une table
    if not Assigned(FTable)
    then begin
    FCol:=-1;
    FRow:=-1;
    end;
    end;

    constructor TlyField.Create;
    begin
    inherited;
    FStream:=TMemoryStream.Create;
    FName:='';
    Clear;
    end;

    constructor TlyField.Create(aName: string);
    begin
    Create;
    FName:=aName;
    end;

    constructor TlyField.Create(aName: string; aType: TlyFieldType);
    begin
    Create(aName);
    FType:=aType;
    end;

    destructor TlyField.Destroy;
    begin
    FreeAndNil(FStream);
    inherited;
    end;

    On note que la méthode Clear a été adaptée pour garder la référence ligne/colonne d'un champ appartenant à une table.

    :alerte: Cette méthode entraîne la mise à jour de la base et de la grille éventuellement associées, en appelant SetText qui déclenche une CallBack (voir plus loin), mais en cas d'échec de la mise à jour, il y a perte de cohérence. Appeler cette méthode sur un champ NOT NULL suffirait par exemple à rompre le lien entre données de la table et de la base.

    Le type BLOB est ajouté aux champs, avec les méthodes pour permettre un accès par l'intermédiaire d'un flux TStream.

    Objet décrivant une colonne

    Ce n'est qu'un conteneur (un record aurait convenu) des propriétés à connaître d'une colonne du DataSet pour accéder aux champs et la mise à jour de la BD. Il est renseigné grâce à l'interface des requêtes préparées. On lui adjoint une propriété Visible que l'utilisateur peut définir pour l'affichage dans la grille.

    // Métadonnées d'une colonne
    TColMetaData = class
    Number: integer; // zero based
    DBName,
    TableName,
    OriginName,
    FullName,
    AliasName: string;
    IsView,
    IsRowid,
    Visible: Boolean;
    Affinity: TlyFieldAffinity;
    Sorting: TlyCollateSequence;
    NotNull,
    PrimaryKey,
    AutoInc: Boolean;
    RowidCol: integer;
    FieldType: TlyFieldType;
    end;

    Objet de type Table

    Ce n'est qu'un tableau de ces nouveaux champs, agrémenté de fonctions de recherche, d'affichage dans une grille et de mise à jour bidirectionnelle grille et BD, grâce aux métadonnées des colonnes lues dans la base.

    Définition :

    Elle inclut celle du type de la CallBack que l'objet de gestion de la BD lui fournira pour l'actualiser :

    // Callback d'une méthode de lySQLiteDB exécutant une requête d'Update d'une valeur modifiée dans la table
    TUpdateFromTable = function(aTable: TlySQLiteTable; aSQL: string): Boolean of object;

    // table remplissable par une requête préparée
    TlySQLiteTable = class
    private
    FUpdateIfModified, FBiDiUpdate: Boolean;
    FRowCount, FColCount, FCurrentField, FRowShift: integer;
    FColData, FFields: TObjectList;
    FDataBase: TObject;
    FConnexion: Pointer;
    FUpdateFromTable: TUpdateFromTable;
    Subset, GridToCol, ColToGrid: array of integer;
    FGrid: TStringGrid;
    protected
    procedure setUpdateIfModified(aValue: Boolean);
    function getSubsetCount: integer;
    procedure setColCount(aValue: integer);
    function getColMetaData(aIndex: integer): TColMetaData;
    function getColByName(aName: string): integer;
    function getFieldByIndex(aCol, aRow: integer): TlyField;
    function getFieldByName(aName: string; aRow: integer): TlyField;
    function getSubsetFieldByIndex(aCol, aIndex: integer): TlyField;
    function getSubsetFieldByName(aName: string; aIndex: integer): TlyField;
    function SameLevelColName(aFieldName: string; aColIndex: integer): string;
    function UpdateDB(aCol, aRow: Integer; aValue: string): Boolean;
    procedure OnCellChange(sender: TObject; aCol, aRow: Integer; const OldValue: string; var NewValue: string);
    procedure OnFieldChange(sender: TObject; aCol, aRow: Integer; const OldValue: string; var NewValue: string);
    public
    constructor Create;
    destructor Destroy; override;
    procedure Clear;
    // méthodes de peuplement de l'ensemble de données
    procedure AddCol(var aColMetaData: TColMetaData);
    procedure AddField(var aField: TlyField);
    procedure EndRow;
    procedure EndFilling(aDataBase: TObject = nil; aConnexion: Pointer = nil; aUpdateFromTable: TUpdateFromTable = nil; aUpdateDBOnChange: Boolean = False);
    // méthodes d'exploitation des données
    procedure ToStringGrid(aGrid: TStringGrid; aBiDiUpdate: Boolean; aWithFieldName: Boolean = True; aWithRowNumber: Boolean = True );
    function SaveToCSV(aName: TFileName; EmptyNull: Boolean = False; WithoutBlobs: Boolean = True): Boolean;
    property ColCount: integer read FColCount write setColCount;
    property RowCount: integer read FRowCount;
    property SubsetCount: integer read getSubsetCount;
    property DataBase: TObject read FDataBase;
    property Connexion: Pointer read FConnexion;
    property UpdateIfModified: Boolean read FUpdateIfModified write setUpdateIfModified;
    property ColByName[aName: string]: integer read getColByName;
    // les objets renvoyés sont pour consultation seulement : il ne faut pas les libérer !
    property ColMetaData[Index: integer]: TColMetaData read getColMetaData;
    property Field[aCol, aRow: integer]: TlyField read getFieldByIndex;
    property FieldByName[aName: string; aRow: integer]: TlyField read getFieldByName;
    // sélection/filtrage des données selon une valeur
    function Locate(aValue: string; aCol: integer): Boolean; overload;
    function Locate(aValue, aFieldName: string): Boolean; overload;
    // parcours de la sélection après un Locate
    property SubsetFieldByName[aName: string; aIndex: integer]: TlyField read getSubsetFieldByName;
    end;


    Cycle de vie :

    Rien de bien particulier :

    procedure TlySQLiteTable.Clear;
    begin
    FreeAndNil(FFields);
    FreeAndNil(FColData);
    SetLength(Subset, 0);
    SetLength(GridToCol, 0);
    SetLength(ColToGrid, 0);
    FRowCount:=0;
    FColCount:=0;
    FCurrentField:=0;
    FDataBase:=nil;
    FConnexion:=nil;
    // paramétrages par défaut
    FUpdateIfModified:=False;
    // préparation des listes
    // propriétaires des objets
    FFields:=TObjectList.Create(True);
    FColData:=TObjectList.Create(True);
    end;

    constructor TlySQLiteTable.Create;
    begin
    inherited;
    Clear;
    end;

    destructor TlySQLiteTable.Destroy;
    begin
    // libèration des champs et métadonnées
    FFields.Free;
    FColData.Free;
    SetLength(Subset, 0);
    SetLength(GridToCol, 0);
    SetLength(ColToGrid, 0);
    inherited;
    end;

    Remplissage :

    La méthode de peuplement du DataSet se base sur la logique d'accès aux résultats d'une requête préparée. Elle fait appel successivement à plusieurs étapes :

    • fixation du nombre de colonnes avec la propriété ColCount ;
    • ajout des métadonnées sur les colonnes avec la procédure AddCol ;
    • passage des valeurs (dans l'ordre des colonnes) avec la procédure AddField ;
    • traitement de fin de ligne avec la procédure EndRow ;
    • fin du peuplement et traitements ultimes avec la procédure EndFilling.


    Ceci permet de la même façon le remplissage à partir d'une requête ou par code.

    Toutes les procédures font des vérifications afin de s'assurer qu'elles sont appelées au bon moment, dans une séquence correcte :

    procedure TlySQLiteTable.AddCol(var aColMetaData: TColMetaData);
    begin
    // métadonnées acceptées seulement à la 1° ligne
    if FRowCount>0
    then Exception.Create('Table.AddCol impossible after EndRow');
    // envoi des colonnes dans l'ordre ?
    if aColMetaData.Number<>FColData.Count
    then Exception.Create('Table.AddCol column index mismatch');
    // vérification qu'on n'envoie pas trop de colonnes
    if FColData.Count then begin
    FColData.Add(aColMetaData); // ajout des données
    aColMetaData:=nil; // la table devient propriétaire des données
    end
    else Exception.Create('Table.AddCol exceeds ColCount');
    end;

    procedure TlySQLiteTable.AddField(var aField: TlyField);
    begin
    if FCurrentField then begin
    if aField.Name=ColMetaData.FullName // setter Name à FullName si nil ?
    then begin
    aField.FTable:=self;
    aField.FCol:=FCurrentField;
    aField.FRow:=FRowCount;
    FFields.Add(aField);
    aField:=nil; // la table gardera la propriété du champ
    Inc(FCurrentField);
    end
    else Exception.Create('Col/Field names mismatch');
    end
    else Exception.Create('Table.AddField exceeds ColCount');
    end;

    procedure TlySQLiteTable.EndRow;
    begin
    // vérification qu'il est bien appelé au bon moment
    if (FCurrentField=ColCount)
    and (FColData.Count=ColCount)
    then begin
    Inc(FRowCount);
    FCurrentField:=0;
    end
    else Exception.Create('Table Col/Field Number <> ColCount');
    end;

    La procédure finale s'assure des champs modifiables dans la base par l'objet lySQLiteDB.

    :fleche: Seuls le seront ceux pour lesquels une autre colonne de la même table contient la clef primaire entière (rowid). Cette limitation a été introduite par souci de simplicité.

    procedure TlySQLiteTable.EndFilling(aDataBase: TObject = nil; aConnexion: Pointer = nil; aUpdateFromTable: TUpdateFromTable = nil; aUpdateDBOnChange: Boolean = False);
    var
    i, j: integer;
    ColData_1, ColData_2: TColMetaData;
    TableName_1, TableName_2: string;
    begin
    if FCurrentField=0 // cad EndRow avant => RowCount et DataCol corrects
    then begin
    FDataBase:=aDataBase;
    FConnexion:=aConnexion;
    FUpdateFromTable:=aUpdateFromTable;
    UpdateIfModified:=aUpdateDBOnChange;
    // on prépare toujours pour Update : peut être modifié ensuite
    // mais ne sera effectif que pour chaque nouvelle modification
    for i:=0 to FColCount-1
    do begin
    ColData_1:=ColMetaData;
    if ColData_1.IsRowid
    then begin
    TableName_1:=ColData_1.DBName+'.'+ColData_1.TableName;
    for j:=0 to FColCount-1
    do begin
    ColData_2:=ColMetaData;
    TableName_2:=ColData_2.DBName+'.'+ColData_2.TableName;
    if (TableName_2=TableName_1)
    and not ColData_2.IsRowid // restera à -1 car non modifiable (même si possible dans SQLite)
    then ColData_2.RowidCol:=i;
    end;
    end;
    end;
    end
    else Exception.Create('Table not correctly filled (missing EndRow)');
    end;

    Accesseurs :

    Rien de particulier concernant les getters
    et setters. Ils servent par exemple à interdire de modifier le nombre de colonnes d'une table, obligeant préalablement à un Clear, faute de quoi on perdrait la cohérence des données. C'est ainsi également que les champs de la table se voient au besoin affecter l'événement qui sera déclenché en cas de modification de leur valeur :

    procedure TlySQLiteTable.setUpdateIfModified(aValue: Boolean);
    var
    i: integer;
    begin
    if aValue and not FUpdateIfModified
    // and (FColData.Count>0) and Assigned(ColMetaData.DB) // ces 2 conditions ne sont pas forcément nécessaires
    then begin
    for i:=0 to FFields.Count-1
    do TlyField(FFields).FOnValueChange:=@OnFieldChange;
    end;
    FUpdateIfModified:=aValue;
    end;

    function TlySQLiteTable.getSubsetCount: integer;
    begin
    Result:=Length(Subset);
    end;

    procedure TlySQLiteTable.setColCount(aValue: integer);
    begin
    // pour éviter des fuites mémoire ou des erreurs d'indexation
    // si l'utilisateur voulait modifier le nombre de colonnes
    // d'une table. S'il en a besoin, faire un Clear d'abord
    if FColCount=0
    then FColCount:=aValue
    else Exception.Create('Table.ColCount already set');
    end;

    function TlySQLiteTable.getColMetaData(aIndex: integer): TColMetaData;
    begin
    try
    Result:=TColMetaData(FColData);
    except
    Result:=nil;
    end;
    end;

    function TlySQLiteTable.SameLevelColName(aFieldName: string; aColIndex: integer): string;
    var
    Qualifiers: integer;
    tsl: TStringList;
    ColData: TColMetaData;
    begin
    Result:=EmptyStr;
    tsl:=TStringList.Create;
    tsl.Delimiter:='.';
    tsl.DelimitedText:=aFieldName;
    Qualifiers:=tsl.Count;
    tsl.Free;
    if not Qualifiers in [1..3] then Exit;
    ColData:=ColMetaData;
    case Qualifiers of
    1: Result:=ColData.AliasName;
    2: Result:=ColData.TableName+'.'+ColData.AliasName;
    3: Result:=ColData.DBName+'.'+ColData.TableName+'.'+ColData.AliasName;
    end;
    end;

    function TlySQLiteTable.getColByName(aName: string): integer;
    var
    i: integer;
    FieldName: string;
    begin
    Result:=-1;
    for i:=0 to FColCount-1
    do begin
    FieldName:=SameLevelColName(aName, i);
    if FieldName=aName
    then begin
    Result:=i;
    Break; // renvoie donc le premier nom qui correspond
    end;
    end;
    end;

    function TlySQLiteTable.getFieldByIndex(aCol, aRow: integer): TlyField;
    var
    i: integer;
    begin
    if (aCol<0) or (aCol>FColCount-1) or (aRow<0) or (aRow>FRowCount-1)
    then Result:=nil
    else begin
    i:=FColCount*aRow+aCol;
    Result:=TlyField(FFields);
    end;
    end;

    function TlySQLiteTable.getFieldByName(aName: string; aRow: integer): TlyField;
    var
    i: integer;
    begin
    i:=getColByName(aName);
    Result:=getFieldByIndex(i, aRow);
    end;

    function TlySQLiteTable.getSubsetFieldByIndex(aCol, aIndex: integer): TlyField;
    begin
    if (aCol<0) or (aCol>FColCount-1) or (aIndex<0) or (aIndex>SubsetCount-1)
    then Result:=nil
    else Result:=TlyField(FFields[Subset]);
    end;

    function TlySQLiteTable.getSubsetFieldByName(aName: string; aIndex: integer): TlyField;
    var
    i: integer;
    begin
    i:=getColByName(aName);
    Result:=getSubsetFieldByIndex(i, aIndex);
    end;

    On voit que le getColByName
    utilise la fonction SameLevelColName pour permettre une recherche selon l'alias seul ou préfixé du nom de la table, voire de la base, permettant aussi bien de chercher les champs 'main.employees.name' ou 'zipcode', par exemple.

    Utilisation :

    Basiquement, une table accueille le résultat d'une requête de l'objet gérant la BD.
    Elle peut se parcourir avec Field ou FielByName (dans les limites de RowCount et ColCount).
    On peut aussi filtrer la table sur une valeur avec la fonction Locate, qui renvoie True et permet le parcours du sous-ensemble correspondant avec SubsetFieldByName, dans la limite de SubsetCount.

    :fleche: Le filtrage ne se fait que sur une valeur exacte. Pour une inégalité ou un filtrage plus complexe, il faut l'inclure dans la requête qui peuple la table (WHERE salaire > 1500 ou WHERE id = 5 AND soe = 0).

    On peut aussi l'afficher simplement dans une grille ou l'exporter vers un fichier .csv récupérable dans un tableur.

    function TlySQLiteDB.ToTable(aTable: TlySQLiteTable; aSQL: string; aUpdateDBIfTableChange: Boolean): Boolean;

    procedure TlySQLiteTable.ToStringGrid(aGrid: TStringGrid; aBiDiUpdate: Boolean; aWithFieldName: Boolean = True; aWithRowNumber: Boolean = True );

    function TlySQLiteTable.Locate(aValue, aFieldName: string): Boolean;

    //..

    if DB.ToTable( MaTable, 'SELECT * FROM employes', False) then begin
    MaTable.ToStringGrid( StringGrid1, False);
    if MaTable.Locate( 'tourlourou', 'nom') and ( MaTable.SubsetCount = 1 ) then
    ShowMessage( 'tourlourou gagne ' + MaTable.SubsetFieldByName[ 'salaire', 0].AsText );
    end;

    Adaptation de l'objet gérant la BD

    Il suffit de lui ajouter la méthode ToTable qui remplit la table passée en argument du résultat de la requête.

    function TlySQLiteDB.ToTable(aTable: TlySQLiteTable; aSQL: string; aUpdateDBIfTableChange: Boolean): Boolean;
    var
    Requete, DB_Name, TableName, OriginName, FullName, AliasName, RowidAlias, Value, PrevTable, AliasQuest, S: string;
    ErrCode, Row, ColCount, Col, ColType, iSize: integer;
    View, NotNull, PrimaryKey, AutoInc: LongBool;
    Error, DataType, CollSeq, Tail2: PChar;
    Sorting: TlyCollateSequence;
    Affinity: TlyFieldAffinity;
    AliasStatement: PStatement;
    ColMetaData: TColMetaData;
    Tail: PChar = nil;
    Blob: TBytes;
    tms: TMemoryStream;
    begin
    if aTable is TlySQLiteTable
    then begin
    // initialisations diverses
    Result:=False;
    aTable.Clear; // efface le paramétrage de UpdateIfModified
    Error:=nil;
    FreeAndNil(FField);
    PrevTable:=EmptyStr;

    if Assigned(FStatement)
    then begin
    if sqlite3_finalize(FStatement)=SQLITE_OK // libère mémoire requête précompilée
    then FStatement:=nil;
    end;

    // préparation de la requête
    if aSQL>UseParamSQL
    then Requete:=aSQL
    else Requete:=FParamSQL.Request;

    // précompilation
    ErrCode:=sqlite3_prepare_v2(DB, PChar(Requete), Length(Requete)+1, FStatement, Tail);

    if ErrCode=SQLITE_OK
    then Row:=0
    else begin // fin si erreur (dans ce cas, FStatement = nil => pas besoin de Finalize)
    FLastErrorCode:=ErrCode;
    Error:=sqlite3_errmsg;
    FLastErrorMsg:=StrPas(Error);
    DoLog('Error : '+FLastErrorMsg+#10#13+' while preparing ToTable statement : '+Requete);
    Exit;
    end;

    //----------------------------------------
    // exécution de la requête ligne par ligne
    //----------------------------------------
    ErrCode:=sqlite3_step(FStatement);
    while ErrCode=SQLITE_ROW do // nouvelle ligne de résultat prête
    begin
    //--------------------------------------------------------
    // on initialise le nombre de colonnes à la première ligne
    //--------------------------------------------------------
    if Row=0 then
    begin
    // nombre de colonnes du DataSet résultat
    ColCount:=sqlite3_column_count(FStatement);
    // fixer la largeur de la table
    aTable.ColCount:=ColCount;
    end;

    //--------------------------------------------------
    // déclenchement événement (CallBack) nouvelle ligne
    //--------------------------------------------------
    if Assigned(OnNewRow) then OnNewRow(self, ColCount);

    //--------------------------------------------
    // on récupère les données colonne par colonne
    //--------------------------------------------
    for Col:=0 to ColCount-1 do
    begin

    //-------------------------------------------
    // récupération des métadonnées de la colonne
    //-------------------------------------------
    if Row=0 then
    begin
    // par interrogation de la BDD à la première ligne
    DB_Name := StrPas( sqlite3_column_database_name( FStatement, Col) ); // nom symbolique de la base
    TableName := StrPas( sqlite3_column_table_name( FStatement, Col) ); // nom de la table
    OriginName := StrPas( sqlite3_column_origin_name( FStatement, Col) ); // nom d'origine du champ
    FullName := DB_Name + '.' + TableName + '.' + OriginName ;
    AliasName := StrPas( sqlite3_column_name( FStatement, Col) ); // nom du champ dans la requête (alias éventuel)
    // une erreur est retournée si la table concernée est une vue
    ErrCode := sqlite3_table_column_metadata( DB, PChar(DB_Name), PChar(TableName), PChar(OriginName),
    DataType, CollSeq, NotNull, PrimaryKey, AutoInc);
    // on va traiter chaque erreur comme si elle était renvoyée par une vue,
    // mais ce n'est pê pas le cas... cependant, prudence => ReadOnly !
    View := ( ErrCode <> SQLITE_OK );
    if View
    then begin
    Affinity := faUnknown;
    Sorting := csUnknown;
    end
    else begin
    // traduction ColumnAffinity
    Affinity := faUnknown;
    // If the declared type for a column contains the string "BLOB"
    // or if no type is specified then the column has affinity NONE.
    if (DataType = 'BLOB') // devrait être NONE, mais BLOB observé...
    or (DataType = 'NONE') then Affinity := faBlob;
    if DataType = 'TEXT' then Affinity := faText;
    if DataType = 'INTEGER' then Affinity := faInteger;
    if DataType = 'REAL' then Affinity := faReal;
    if DataType = 'NUMERIC' then Affinity := faNumeric;
    // traduction CollateSequence
    Sorting := csUserDefined;
    if CollSeq = 'BINARY' then Sorting := csBinary;
    if CollSeq = 'NOCASE' then Sorting := csNoCase;
    if CollSeq = 'RTRIM' then Sorting := csRTrim;
    end;

    // recherche de l'Alias du Rowid
    S := DB_Name + '.' + TableName;
    if S <> PrevTable // économie si monotable !
    then begin
    AliasQuest:='SELECT rowid FROM '+S;
    iSize:=Length(AliasQuest)+1;
    if sqlite3_prepare_v2(DB, PChar(AliasQuest), iSize, AliasStatement, Tail2) = SQLITE_OK
    then begin
    RowidAlias := StrPas( sqlite3_column_name( AliasStatement, Col) ) ;
    sqlite3_finalize(AliasStatement);
    end
    else RowidAlias := EmptyStr;
    PrevTable:=S;
    end;

    // ajout des métadonnées
    ColMetaData:=TColMetaData.Create;
    ColMetaData.Affinity := Affinity;
    ColMetaData.AliasName := AliasName;
    ColMetaData.AutoInc := AutoInc;
    // ColMetaData.DB := DB;
    ColMetaData.DBName := DB_Name;
    ColMetaData.FieldType := ftUnknown;
    ColMetaData.FullName := FullName;
    ColMetaData.IsView := View;
    ColMetaData.IsRowid := ( AliasName = RowidAlias ) ;
    ColMetaData.NotNull := NotNull;
    ColMetaData.Number := Col;
    ColMetaData.OriginName := OriginName;
    ColMetaData.PrimaryKey := PrimaryKey;
    ColMetaData.RowidCol := -1;
    ColMetaData.Sorting := Sorting;
    ColMetaData.TableName := TableName;
    ColMetaData.Visible := True;
    try
    //----------------------
    // on renseigne la table
    //----------------------
    aTable.AddCol(ColMetaData);
    except
    on E: Exception
    do begin
    DoLog('Error : '+E.Message+#10#13
    +' with ToTable statement : '+Requete);
    FreeAndNil(ColMetaData); // car aTable pas devenue propriétaire de l'objet
    if sqlite3_finalize(FStatement)=SQLITE_OK
    then FStatement:=nil; // sinon, sera repassé dans Finalze au Destroy
    Exit;
    end;
    end; // try
    end
    else begin
    // auprès de la table pour les lignes suivantes
    FullName := aTable.ColMetaData.FullName; // ou OriginName pour le Field.Create ??????????
    AliasName := aTable.ColMetaData.AliasName;
    end; // if Row=0

    //-----------------------------------
    // récupération de la valeur du champ
    //-----------------------------------

    // type originel avant conversion éventuelle dans la requête
    // sinon, non défini (d'après manuel 3.8.7, vu le 20/11/2014)
    ColType := sqlite3_column_type( FStatement, Col); // à récupérer à chaque fois car typage dynamique
    FField:=TlyField.Create(FullName, TlyFieldType(ColType)); // ou OriginName ??????????

    case ColType of
    SQLITE_INTEGER : FField.AsInteger := sqlite3_column_int64( FStatement, Col) ;
    SQLITE_FLOAT : FField.AsFloat := sqlite3_column_double( FStatement, Col) ;
    SQLITE_TEXT : FField.AsText := StrPas( sqlite3_column_text( FStatement, Col) );
    SQLITE_BLOB : begin
    Blob := sqlite3_column_blob( FStatement, Col) ;
    if Assigned(Blob)
    then begin
    iSize := sqlite3_column_bytes( FStatement, Col) ;
    tms:=TMemoryStream.Create;
    tms.SetSize(iSize);
    Move(Blob, tms.Memory^, iSize);
    FField.StreamToBlob(tms);
    tms.Free;
    end
    else FField.Clear;
    end;
    SQLITE_NULL : FField.Clear;
    else FField.Clear;
    end;

    // à récupérer avant le AddField qui renverra nil pour FField
    // en devenant propriétaire du champ qu'il intègre à sa table
    Value := FField.AsText; // quel est le AsText d'un Blob ? chaîne qui dit 'BLOB de x octets'

    try
    //----------------------
    // on renseigne la table
    //----------------------
    aTable.AddField(FField);
    except
    on E: Exception
    do begin
    DoLog('Error : '+E.Message+#10#13
    +' with ToTable statement : '+Requete);
    FreeAndNil(FField); // car aTable pas devenue propriétaire de l'objet
    if sqlite3_finalize(FStatement)=SQLITE_OK
    then FStatement:=nil; // sinon, sera repassé dans Finalize au Destroy
    Exit;
    end;
    end;

    //----------------------------------------------------
    // déclenchement événement (CallBack) nouvelle colonne
    //----------------------------------------------------
    if Assigned(OnNewCol) then OnNewCol(self, AliasName, Value);

    end; // for col

    //---------------------------
    // traitement de fin de ligne
    //---------------------------

    Inc(Row);
    try
    aTable.EndRow;
    except
    on E: Exception
    do begin
    DoLog('Error : '+E.Message+#10#13
    +' with ToTable statement : '+Requete);
    if sqlite3_finalize(FStatement)=SQLITE_OK
    then FStatement:=nil; // sinon, sera repassé dans Finalize au Destroy
    Exit;
    end;
    end;

    //------------------------------------------------
    // déclenchement événement (CallBack) fin de ligne
    //------------------------------------------------
    if Assigned(OnEndRow) then OnEndRow(self);

    // y a-t-il une ligne suivante ?
    ErrCode:=sqlite3_step(FStatement);

    end; // while ErrCode=SQLITE_ROW

    //-------------------------------------------------------------------------------------------------------------------
    // qu'il y ait eu ou non lignes de résultat traités dans le while (un UPDATE donne directement SQLITE_DONE ou erreur)
    //-------------------------------------------------------------------------------------------------------------------
    Result := ( ErrCode = SQLITE_DONE );

    if Result then
    begin
    try
    aTable.EndFilling(self, DB, @UpdateFromTable, aUpdateDBIfTableChange);
    ErrCode:=SQLITE_OK;
    if LogRequests
    then DoLog('Request : '+Requete);
    except
    on E: Exception
    do begin
    DoLog('Error : '+E.Message+#10#13
    +' with ToTable statement : '+Requete);
    if sqlite3_finalize(FStatement)=SQLITE_OK
    then FStatement:=nil; // sinon, sera repassé dans Finalize au Destroy
    Exit;
    end;
    end;
    end
    else begin
    FLastErrorCode:=ErrCode;
    aTable.Clear;
    Error:=sqlite3_errmsg;
    FLastErrorMsg:=StrPas(Error);
    DoLog('Error : '+FLastErrorMsg+' for request : '+Requete);
    end;

    if sqlite3_finalize(FStatement)=SQLITE_OK
    then FStatement:=nil; // sinon, sera repassé dans Finalize au Destroy

    end // if aTable is TlySQLiteTable
    else ErrCode:=LYSQLITEDB_NOTASSIGNED;
    Result := (setLastError(ErrCode) = SQLITE_OK);
    end;

    Elle se base sur l'interface des requêtes pré-compilées. Si la requête aboutit, le résultat en est recueilli ligne par ligne, colonne par colonne.
    Les valeurs de ColCount et les métadonnées des colonnes sont récupérées lors du parcours de la première ligne du résultat.

    L'intérêt essentiel de ces métadonnées est de permettre d'identifier les champs modifiables automatiquement car appartenant à une table dont la requête fournit aussi une clef primaire unique (PK). Ceci est une limitation de cet outil, qui se veut simple avant tout. Toute erreur ou absence de PK sera traitée comme si la table était une vue, en lecture seule. Une requête annexe permet de récupérer l'alias de la colonne de la PK pour la table concernée, donc de savoir quel champ de la requête contient cette PK pour une table donnée.

    Il suffit ensuite de récupérer la valeur de chaque champ, selon son type.

    Gestion des liaisons avec l'objet Table

    Optionnelles, elles sont rendues possibles entre :

    • Table et BD : une modification d'un champ dans l'objet Table entraînera si possible sa mise à jour dans la BD ;
    • grille et Table : une modification d'une cellule de la grille entraînera la mise à jour du champ correspondant dans la Table ;


    C'est cet objet Table qui pilote l'ensemble, grâce à des fonctions de rappel.

    Liaison avec la BD :

    L'objet gérant la BD fournit une CallBack privée à l'objet Table pour qu'il exécute au besoin une requête de mise à jour de champ dans la BD :

    // callback de l'objet gérant la BD exécutant une requête
    function TlySQLiteDB.UpdateFromTable(aTable: TlySQLiteTable; aSQL: string): Boolean;
    begin
    Result:=False;
    if (aTable.DataBase<>self) or (aTable.Connexion<>DB) then Exit;
    Result:=Execute(aSQL);
    end;

    La mise à jour par l'objet Table repose sur la simple élaboration du SQL ad hoc :

    // pour mettre à jour la BD quand un champ de la Table est modifié
    function TlySQLiteTable.UpdateDB(aCol, aRow: Integer; aValue: string): Boolean;
    var
    RowidCol: integer;
    Affinity: TlyFieldAffinity;
    TableName, FieldName, RowidName, Value, SQL: string;
    begin
    Result:=False;
    if (DataBase=nil) or (Connexion=nil) or (FUpdateFromTable=nil) or ColMetaData.IsView or (ColMetaData.FieldType=ftBlob) then Exit;
    RowidCol:=ColMetaData.RowidCol;
    if RowidCol<0 then Exit; // vaut -1 si IsRowid ou pas de colonne Rowid => non modifiable
    TableName:=ColMetaData.DBName+'.'+ColMetaData.TableName;
    FieldName:=ColMetaData.OriginName;
    RowidName:=ColMetaData.OriginName; // ou AliasName ?????????????????
    Affinity:=ColMetaData.Affinity;
    if Affinity in [faInteger, faReal, faNumeric]
    then Value:=aValue
    else Value:=AnsiQuotedStr(aValue, QuoteChar); // tant pis si typage dynamique !
    SQL:='UPDATE '+TableName+' SET '+FieldName+' = '+Value+' WHERE '+RowidName+' = '+Field[RowidCol, aRow].AsSQL;
    // seul lySQLiteDB interagira avec la base
    // et pourra garder les requêtes d'Update en log
    Result:=FUpdateFromTable(self, SQL);
    end;

    :alerte: En l'état actuel, la mise à jour n'est pas possible pour les BLOBs.

    Gestion d'une modification d'un champ de l'objet Table :

    Elle est détectée par l'accesseur, au SetText du champ :

    procedure TlyField.SetText(aValue: string); // valeur textuelle qui devra être quotée
    var
    NewValue: string;
    begin
    if Pos(#0, aValue) > 0 then
    raise Exception.Create('Présence du caractère nul, interdit dans une chaîne');
    NewValue:=aValue; // car aValue ne doit pas être modifiée
    if Assigned(FOnValueChange) then
    FOnValueChange(self, FCol, FRow, FText, NewValue);
    FText:=NewValue; // peut avoir été restauré à FText par FOnValueChange (échec synchro BD par ex.)
    FType:=ftText;
    bText:=True;
    FNull:=False;
    end;

    Ce dernier déclenche sur option l'événement OnFieldChange de la table, chargé de mettre à jour la valeur dans la cellule correspondante de la grille et d'appeler la fonction de mise à jour du champ de la BD :

    procedure TlySQLiteTable.OnFieldChange(sender: TObject; aCol, aRow: Integer; const OldValue: string; var NewValue: string);
    var
    TheCol, TheRow: integer;
    begin
    if NewValue<>OldValue then // sinon, pas la peine de se fatiguer !
    try
    if (Sender as TlyField).Table<>self then
    raise Exception.Create('no Table link');
    TheCol:=-1;
    if FBiDiUpdate // synchronisation avec la grille
    then begin
    if not Assigned(FGrid) then
    raise Exception.Create('no Grid link');
    TheRow:=aRow+FRowShift;
    TheCol:=ColToGrid;
    if TheCol>-1 then FGrid.Cells[TheCol, TheRow]:=NewValue;
    end;
    if FUpdateIfModified // synchronisation avec la BD
    then begin
    if not UpdateDB(aCol, aRow, NewValue)
    then begin
    if TheCol>-1 then FGrid.Cells[TheCol, TheRow]:=OldValue; // marche arrière !
    raise Exception.Create('DB did not sync');
    end;
    end;
    except
    NewValue:=OldValue;
    end;
    end;

    :fleche: En cas d'échec de la mise à jour dans la BD, les modifications dans la table et la grille sont annulées pour conserver la cohérence.

    Gestion d'une modification d'une cellule de la grille :

    Sur le même schéma, elle est détectée en fin d'édition de la cellule, par la grille, dont l'événement OnValidateEntry pointe sur option sur la procedure de la table qui va gérer les différentes priorités de mise à jour : d'abord BD, puis champ :

    TlySQLiteTable.OnCellChange(Sender: TObject; aCol, aRow: Integer; const OldValue: string; var NewValue: String);
    const
    AffToType: array of TlyFieldType = (ftInteger, ftText, ftBLOB, ftFloat, ftFloat, ftUnknown);
    var
    TheField: TlyField;
    TheCol, TheRow: integer;
    Affinity: TlyFieldAffinity;
    begin
    if FBiDiUpdate and (NewValue<>OldValue) then // sinon, pas la peine de se fatiguer !
    try
    if (Sender<>FGrid) then raise Exception.Create('no link');
    TheCol:=GridToCol; // si jamais des colonnes avaient été ajoutées à la grille...
    Affinity:=ColMetaData.Affinity;
    TheRow:=aRow-FRowShift; // ou son nombre de lignes modifié...
    TheField:=Field[TheCol, TheRow]; // d'abord, en cas d'index hors limite...
    if UpdateIfModified // synchronisation avec la BD
    then begin
    if not UpdateDB(TheCol, TheRow, NewValue)
    then raise Exception.Create('DB did not sync');
    end;
    // MAJ table avec affectation directe des champs pour ne pas déclencher OnFieldChange
    TheField.FText:=NewValue;
    TheField.FNull:=False;
    if TheField.FieldType in [ftUnknown, ftNull]
    then TheField.FType:=AffToType; // ne peut donc plus rester ftNull alors qu'on a attribué une valeur
    if Affinity=faText
    then TheField.bText:=True
    else TheField.bText:=False;
    except
    NewValue:=OldValue;
    end;
    end;

    Conclusion

    Ici s'achève le développement actuel de la librairie, avec l'ajout de ce dernier objet destiné à faciliter son utilisation.

    :alerte: À l'instar du reste du projet, ce DataSet reste une approche destinée à rester simple, facile d'utilisation, destinée à ne satisfaire que des besoins basiques. Malgré tout, la complexité atteinte et celle des interactions ne me permet pas d'en garantir par des tests systématiques et exhaustifs le fonctionnement parfait en toutes circonstances... Je vous incite donc à la prudence !

    Vous trouverez les unités ici : Billet_numero_5.zip

    Relire le code pour le commenter a fait naître certaines réflexions et pistes d'amélioration (je suis d'ailleurs à l'écoute des suggestions) ; qui sait ce que réserve l'avenir ?
  • Vous avez lu gratuitement 52 articles depuis plus d'un an.
    Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.

    Une erreur dans cette actualité ? Signalez-nous-la !