Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUMS DELPHI F.A.Q DELPHI TUTORIELS DELPHI LIVRES COMPOSANTS SOURCES DEFI TELECHARGEZ DELPHI TV

Développer un DataSet en mémoire

Optimisation des accès Base de données, IIère Partie

Par Franck SORIANO (Pages perso)
 

Cet article présente en détail le fonctionnement de la classe TDataSet. Il explique notamment comment dériver la classe TDataSet pour réaliser un dataset en mémoire : la classe TMemoryDataSet.
Les articles suivants utiliseront ce dataset pour s'interfacer avec OLEDB et OCI.

I. Introduction
Télécharger les sources de l'article
II. Organisation générale du TDataSet
II-A. Gestion des buffers
II-B. Ouverture/Fermeture du curseur
II-C. Navigation
II-D. Modification des données
III. La classe TMemoryDataSet
III-A. Organisation des données en mémoire
III-B. Gestion des buffers
III-B-1. Définition des Champs du TMemoryDataSet
III-B-2. Gestion des champs LOB
III-B-3. Organisation d'un buffer
III-B-4. Implémentation des buffers
III-B-4-a. GetRecordSize
III-B-4-b. AllocRecordBuffer
III-B-4-c. FreeRecordBuffer
III-B-4-d. InternalInitRecord
III-B-4-e. Accès direct aux champs
III-C. Ouverture/Fermeture
III-C-1. InternalOpen
III-C-2. InternalClose
III-C-3. IsCursorOpen
III-C-4. InternalInitFieldDefs
III-D. Navigation
III-D-1. GetRecord
III-D-2. GetBookmarkFlag/SetBookmarkFlag
III-D-3. GetBookmarkData/SetBookmarkData
III-D-4. InternalGotoBookmark
III-D-5. BookmarkValid
III-D-6. InternalSetToRecord
III-D-7. InternalFirst
III-D-8. InternalLast
III-D-9. GetRecNo/SetRecNo
III-E. Lecture/Ecriture des champs
III-E-1. GetFieldData
III-E-2. SetFieldData
III-F. Gestion des LOB
III-F-1. TMemoryBlobStream
III-F-2. CreateBlobStream
III-G. Modification des données
III-G-1. GetCanModify
III-G-2. InternalEdit
III-G-3. InternalCancel
III-G-4. InternalPost
III-G-5. InternalDelete
Conclusion
IV. Quelques cas d'utilisations
IV-A. Scénario 1 : Ouverture/Fermeture du DataSet
IV-B. Scenario 2 : Affichage et Navigation avec une DBGrid
IV-B-1. First
IV-B-2. Last
IV-B-3. Prior
IV-B-4. Next
IV-B-5. SetRecNo
V. Références


I. Introduction

Un DataSet en mémoire. Encore un ! Ce type de composant fait légion sur le net. Même Delphi intègre le TClientDataSet en standard qui peut faire office de DataSet en mémoire.

Alors pourquoi vouloir en développer un de plus ? Tout simplement pour trois raisons majeures :

  • Malgré tout les composants existants, je n'en ai jamais trouvé un seul qui ait le niveau de performances que j'en attends. Comme je l'ai expliqué dans le précédent article, le TClientDataSet est très lourd, avec une architecture complexe. On ne dispose d'aucun moyen permettant de le charger rapidement.
  • Le TDataSet est au cœur de toute application base de données en Delphi. Si on veut écrire des applications performantes, il est important de comprendre les mécanismes mis en œuvre à l'intérieur du TDataSet. De cette manière, lorsqu'on écrit une ligne de code, on appréhende mieu ses conséquences. Ecrire un DataSet en mémoire est un très bon prétexte pour étudier le fonctionnement de ce composant.
  • Enfin, on aura besoin de surcharger un TDataSet dans les prochains articles afin de s'interfacer avec OLEDB.
Nous allons voir les principes de bases permettant d'implémenter un TDataSet complet. Il sera cependant bridé aux fonctionnalités essentielles :

  • Navigation et Curseur Bidirectionnelle.
  • Accès direct à une ligne
  • Les champs LOB (Blob, Memo,…) seront gérés.
  • Les champs calculés ne seront pas gérés.
  • Les indexes et toutes les méthodes associées ne seront pas gérées.
  • Les filtres seront partiellement gérés. On ne pourra filtrer le dataset qu'avec l'événement OnFilterRecord.
Cependant, si les indexes et les filtres ne seront pas totalement gérés, je donnerai quelques pistes pour ceux qui voudraient le faire eux même.


Télécharger les sources de l'article


Pour compiler les sources, vous aurez besoin de ETW :
srcs ftp://ftp-developpez.com/fsoriano/archives/etw/delphi/fichiers/etw-sources.zip

Vous pouvez télécharger l'article complet au format docx ici.

L'ensemble des codes sources utilisés pour cet article compile avec Turbo Delphi Explorer.


II. Organisation générale du TDataSet

Tous les composants d'accès aux données permettant de lire un jeu de données dérivent d'un ancêtre commun : La classe TDataSet.

Cette dernière définit un certain nombre d'opérations et de mécanismes communs à tous les composants d'accès aux données. Cependant elle ne s'occupe pas de la gestion de la source de données elle-même. Elle ne peut pas être instanciée directement car bon nombre de méthodes essentielles sont abstraites et doivent être implémentées dans les descendants.

L'écriture d'un Dataset personnalisé consiste en fait à implémenter ces méthodes abstraites.

Elles peuvent être classées en plusieurs catégories :


II-A. Gestion des buffers

La classe TDataSet maintient un cache permettant de conserver en mémoire un certain nombre de lignes sans être obligé de déplacer constamment le curseur provenant du SGBD.

Ce cache est composé d'un ensemble de buffers. Chaque buffer permet de stocker une ligne complète de données.

La classe TDataSet décide elle-même du nombre de buffers à gérer. En principe, il s'agit plus ou moins du nombre de lignes visibles dans les composant TDBGrid liés. Ainsi, les DBGrids bénéficient d'un mécanisme spécial permettant de lire rapidement les données sans être obligé de toucher au curseur de la base de données.

Ces buffers servent également en modification. Ils permettent de stocker temporairement les valeurs de la ligne en cours de modification, avant que cette dernière ne soit envoyée au SGBD. Ils permettent également de mémoriser les valeurs de la ligne avant modification pour pouvoir les annuler si besoin.

Un buffer correspond à une ligne complète. Il contient les valeurs de tous les champs de la ligne. Tous les buffers ont la même taille. Ceci pose un problème pour les champs de taille variable. En effet, en principe ils doivent être stockés à l'intérieur du buffer. Comme ce dernier a une taille fixe, on est obligé de prévoir des buffers suffisamment grands pour stocker des champs de taille maximale. Ainsi si on définit un champ varchar(4000) dans la base de données, le champ occupera bel et bien 4000 octets une fois chargé à l'intérieur du DataSet. Et ce quel que soit la taille réelle du champ, même si ce dernier est à NULL !

Les allocations et destructions de buffers sont gérées automatiquement par la classe TDataSet. A notre niveau, on doit simplement calculer la taille d'un buffer et fournir une méthode d'allocation et de libération de la mémoire.


II-B. Ouverture/Fermeture du curseur

Le principe du DataSet c'est d'encapsuler un curseur sur une source de données. Bien évidemment, la classe TDataSet ne peut pas créer ce curseur elle-même. On doit donc définir des méthodes pour créer ce curseur.

On remarquera au passage, que c'est généralement au moment où on ouvre le curseur base de données qu'on peut connaître la structure du jeu de résultat. C'est donc à ce moment qu'on peut connaître les champs qui font partis du DataSet, calculer la taille des buffers et créer les composants TField.


II-C. Navigation

La classe TDataSet définit les fonctions de navigation de base. C'est-à-dire : First, Next, Prior et Last. Mais toutes ces opérations peuvent ne pas être disponibles en fonction du type de dataset. Sur un dataset unidirectionnel, on peut uniquement faire un Next.

Lorsqu'on définit un dataset personnalisé, il faut implémenter ces méthodes pour se déplacer sur la source de données et charger les buffers avec les données des lignes correspondantes.

Les fonctions de navigation et de recherche plus évoluées (Locate, FindKey, GotoKey...) ne sont pas gérées par la classe TDataSet. C'est aux descendants de les définir et de les implémenter si nécessaire.

En revanche, la gestion des bookmarks fait partie du TDataSet et ses derniers doivent être implémentés dans les classes dérivées (sauf pour les dataset unidirectionnel).


II-D. Modification des données

La classe TDataSet gère automatiquement les méthodes Insert, Append, Edit, Cancel et Post. Elle assure la gestion des buffers et maintient la définition de l'état du dataset. En revanche, les classes dérivées doivent s'occuper de répercuter ces modifications sur la source de données si elles sont validées.

De plus, la classe TDataSet ne définit pas le mode de stockage des champs à l'intérieur du buffer. Chaque descendant est libre d'organiser la structure interne des buffers comme bon lui semble. Aussi, lorsque les TField essaient de lire/écrire la valeur d'un champ dans le buffer, il faut implémenter les méthodes d'extraction/écriture de cette valeur.

Si on implémente toutes les méthodes dans chacune de ces catégories, on obtient un DataSet bidirectionnel, complètement opérationnel et pouvant être affiché dans une grille.

Si on connecte ce dataset à un tableau en mémoire en guise de source de données, on obtient un dataset en mémoire.

C'est ce que nous allons mettre en pratique immédiatement en développant la classe TMemoryDataSet.


III. La classe TMemoryDataSet


III-A. Organisation des données en mémoire

Le TMemoryDataSet va devoir stocker les lignes qui seront lues par le DataSet. Pour cela on définit simplement un tableau de buffers. Par contre, par la suite il faudra que les lignes occupent des espaces mémoires contigus.

C'est pourquoi ces dernières seront regroupées par Pages. Chaque page correspondra à un bloc mémoire pouvant contenir un certain nombre de lignes (propriété PageSize). Ainsi, les allocations mémoires s'effectueront page par page et non pas ligne par ligne. On évitera ainsi de faire de l'allocation mémoire à outrance.

Lorsqu'une page est pleine, on en allouera une deuxième, puis une troisième...

Enfin, on va maintenir une liste des pages.


III-B. Gestion des buffers


III-B-1. Définition des Champs du TMemoryDataSet

La classe TDataSet prévoit qu'on utilise une collection TFieldDefs pour décrire les champs du DataSet. Inutile de réinventer la roue, c'est ce que nous allons faire.

En principe, on définit les TFieldDef pour indiquer les champs que l'on souhaite gérer dans le DataSet. Cette définition doit être effectuée avant son ouverture.

Au moment de l'ouverture du TMemoryDataSet, ce dernier créera automatiquement les TField nécessaires pour accéder aux données.

Avec les composants base de données de Delphi, il est possible de définir les TField en design, alors que le dataset est fermé. Cela permet de configurer les paramètres d'affichage des TField depuis l'IDE. On peut alors les réordonner, définir les DisplayFormat, DisplayLabel...

Cependant, je ne conseille pas cette façon de travailler. En effet, si elle permet de développer rapidement une maquette fonctionnelle, cette approche pose ensuite de gros problèmes de maintenance. Si on ajoute un champ dans la base, il faut repasser sur tout les datasets qui ont été ainsi configurés, sinon les nouveaux champs ne sont pas visibles. Si un champ change de taille, on obtient une erreur à l'ouverture du DataSet...

C'est pourquoi, la classe TMemoryDataSet ignorera purement et simplement les TField créés en design. Ils seront systématiquement détruit et recréer à l'ouverture du DataSet.


III-B-2. Gestion des champs LOB

Les champs LOB (Memo, Binaire,…) posent un problème particulier. Ils peuvent avoir une taille importante, voir très importante. Leur taille n'est pas limitée, tandis qu'au contraire les buffers des lignes ont une taille fixe.

Cette simple particularité interdit purement et simplement de les stocker directement dans les buffers.

On va devoir les stocker séparément. Une technique simple consiste à ne stocker à l'intérieur du buffer qu'un pointeur sur un autre bloc mémoire qui lui contient réellement les données du LOB.

Avec cette technique, il faut faire attention à ne pas créer de fuite mémoire. Pour s'en assurer, on va travailler légèrement différemment. Les champs LOB alloués sont stockés dans une liste indépendante à l'intérieur de TMemoryDataSet. Dans le buffer d'une ligne, au lieu de stocker directement le pointeur sur le contenu du LOB, on va stocker le numéro de LOB dans la liste des LOB. De cette façon, on conserve toujours les références sur les LOB qui ont été alloués.

Au pire, la destruction du TMemoryDataSet libérera les LOBs oubliés.


III-B-3. Organisation d'un buffer

La première chose à faire est de définir la structure d'un buffer. Ce dernier doit pouvoir contenir les données de tous les champs du DataSet.

Sur le principe, il va donc s'agir de la juxtaposition des valeurs de chaque champ. Cependant, la valeur d'un champ n'est pas suffisante. Pour chaque champ on doit également savoir si le champ est à NULL ou s'il possède une valeur. Pour les champs de taille variable, il faut également qu'on sache qu'elle est la longueur courante du champ.

Aussi, pour chaque champ, nous allons définir la structure suivante :

  // La structure TFieldData représente le format de stockage d'un champ
  // à l'intérieur d'un buffer du DataSet.
  PFieldData = ^TFieldData;
  TFieldData = packed record
    NullStatus : integer;  // Indique si le champ est à NULL.
    LengthValue : integer;  // Indique la longueur actuelle du champ.
                            // LengthValue n'a de sens que pour les champs de
                            // taille variable.
    Data : array[0..0] of byte; // Début des données du champ.
  end;
On utilise un record et non pas une classe car ces structures vont ensuite être alignées à l'intérieur de chaque buffer. Si on avait définit une classe, Delphi n'aurait pas stocké la structure à l'intérieur du buffer, mais un pointeur sur l'instance.

J'ai utilisé un integer pour indiquer si un champ est à NULL. Ca peut paraître comme un gaspillage mémoire (et ça en est bien un) car un simple boolean aurait pu faire l'affaire. On aurait également pu optimiser la mémoire en définissant un tableau de bits à l'intérieur du buffer.

Cependant si toutes ces solutions optimisent la mémoire, elles n'optimisent pas les performances. A terme, on utilisera la classe TMemoryDataSet pour charger les données retournées par OLEDB. Hors pour obtenir les meilleures performances avec OLEDB, il vaut mieux que ces indicateurs soient des entiers sur 32 bits.

Le champ Data indique simplement un tableau d'octets. Sa taille réelle dépendra du type du champ stocké.

Outre les données relatives aux champs, un buffer doit également contenir trois informations supplémentaires nécessaires à la gestion du Dataset :

Les BookmarkFlags : Il s'agit d'une information utilisée par TDataSet pour mémoriser l'état du buffer par rapport à l'ensemble de données. Elle indique au dataset, s'il s'agit de la première ligne (bfBOF), de la dernière ligne (bfEOF), d'une ligne ordinaire (bfCurrent) ou d'une ligne en cours d'insertion (bfInserted).

Identifiant du buffer : Les buffers doivent posséder un identifiant unique. Il peut s'agir d'un simple entier. Cet identifiant permet au dataset de positionner la source de données sur un buffer en particulier.

Bookmark : Le bookmark est un identifiant indiquant précisément la ligne sur laquelle on se trouve. C'est cette valeur qui est retournée lorsqu'on fait un GetBookmark. Lorsqu'on veut effectuer un GotoBookmark, il faut rechercher la ligne qui possède le bookmark voulu. Cette information est un peu redondante avec l'identifiant du buffer. C'est pourquoi il est fréquent d'utiliser le même identifiant comme identifiant de buffer et valeur de bookmark.

Ainsi la structure d'un buffer est la suivante :

  // TLineBuffer désigne la structure d'un buffer.
  TLineBuffer = packed record
    BkFlags : TBookmarkFlag;    // Bookmark flag
    Bookmark : cardinal;        // Valeur du bookmark/identifiant du buffer
    Data : array[0..0] of byte; // Début de la zone des données des champs
  end;
Data est en réalité en tableau de structures TFieldData. Cependant on ne peut pas le déclarer comme tel car chaque élément possède une taille variable.


III-B-4. Implémentation des buffers


III-B-4-a. GetRecordSize

La méthode GetRecordSize doit retourner la taille d'un buffer en octets. Cette dernière dépend du mode d'organisation choisit pour les buffers, mais aussi des champs présents dans le dataset. La taille d'un buffer peut donc être calculée au moment de l'ouverture du dataset et mémorisée jusqu'à sa fermeture :

// Calcule la taille d'un buffer, en fonction des champs définis dans
// FieldDefs.
// Une fois calculé, le résultat est mémorisé dans FRecordSize.
procedure TMemoryDataSet.CalcRecordSize;
var
  i : integer;
  currentOffset : cardinal;
begin
  SetLength(FFieldInfo, FieldDefs.Count);

  // Initialisation de la taille initiale du buffer.
  currentOffset := sizeof(TLineBuffer) - 1;

  // On agrandit le buffer pour stocker chaque champ un par un. On en profite
  // également pour indexer les champs à l'intérieur du buffer et initialiser
  // le tableau FFieldInfo.
  for i := 0 to FieldDefs.Count -1 do
  begin
    FFieldInfo[i].Offset := currentOffset; // On mémorise la position du champ
                                           // à l'intérieur du buffer.
    FFieldInfo[i].Size := GetFieldSize(FieldDefs[i]);
    inc(currentOffset, sizeof(TFieldData) - 1 + FFieldInfo[i].Size);
  end;
  FRecordSize := currentOffset;
end;
FFieldInfo est un tableau mémorisant les informations nécessaires pour accéder directement à un champ à l'intérieur d'un buffer. Il est déclaré de la façon suivante :

    FFieldInfo : array of TFieldInfo;
Avec :

  TFieldInfo = record
    Offset : cardinal; // Offset d'un champ dans un buffer, par rapport au début
                       // du buffer.
    Size : cardinal;   // Taille de la zone des données du champ à l'intérieur
                       // du buffer.
  end;
La taille du buffer est calculée et mémorisée dans FRecordSize. Ainsi GetRecordSize n'a qu'à renvoyer cette valeur :

function TMemoryDataSet.GetRecordSize: word;
begin
  result := FRecordSize; // On renvoie la valeur calculée lors de l'ouverture
                         // du DataSet.
end;
Pour effectuer le calcul, on fait appel à une méthode GetFieldSize. Cette dernière est chargée de calculer l'espace nécessaire pour stocker un champ en fonction de sa déclaration et du mode de stockage choisit :

// Retourne la taille en octets nécessaire pour stocker le champ décrit dans
// <FldDef>.
function TMemoryDataSet.GetFieldSize(FldDef: TFieldDef): cardinal;
begin
  case FldDef.DataType of
  ftString, ftFixedChar:
    result := FldDef.Size; // La taille correspond à la taille du champ.
  ftSmallint, ftWord: // Entier court
    result := 2;
  ftAutoInc, ftInteger: // Entier long
    result := 4;
  ftBoolean: // Les boolean sont stockés comme des entiers courts.
    result := 2;
  ftFloat: // Les floats sont stockés dans des double.
    result := sizeof(double);
  ftCurrency:
    result := sizeof(currency);
  ftBCD, ftFMTBcd : // Les BCD sont convertis en double.
    result := sizeof(double);
  ftDateTime, ftDate, ftTime :
    result := sizeof(TDateTime);

  ftWideMemo, ftOraBlob, ftOraClob, ftBlob, ftBytes, ftVarBytes,
  ftMemo, ftGraphic, ftFmtMemo: // Les LOB font l'objet d'une gestion spéciale
    result := 4;

  ftFixedWideChar, ftWideString: // Chaînes unicode.
    result := FldDef.Size*2;

  ftLargeint:
    result := sizeof(int64);
  ftVariant: // Le variant est stocké dans un variant...
    result := sizeof(variant);
  ftGuid:
    result := sizeof(TGUID);
  else raise MemoryDataSetNotSupported.Create(FldDef.Name, FldDef.DataType);
  end;
end;

III-B-4-b. AllocRecordBuffer

Cette méthode doit être implémentée pour allouer un nouveau buffer. On peut se contenter d'effectuer une simple allocation mémoire. On initialise également les champs à l'intérieur du buffer à la valeur NULL :

// Cette méthode est appelée par TDataSet pour allouer un nouveau buffer.
function TMemoryDataSet.AllocRecordBuffer: PChar;
begin
  // On trace l'appel à AllocRecordBuffer au niveau TRACE_LEVEL_VERBOSE.
  // On pourra ainsi suivre les allocations/libération de buffer effectuées par
  // le TDataSet et analyser son fonctionnement en détails si besoin.
  SQLLogger.Trace(EVENT_TRACE_TYPE_INFO, 'AllocRecordBuffer',
    TRACE_LEVEL_VERBOSE);

  // On alloue simplement un bloc mémoire de la taille d'un buffer.
  GetMem(Result, GetRecordSize);

  // Ensuite on initialise le buffer pour définir tous les champs à NULL.
  InternalInitRecord(Result);
end;

III-B-4-c. FreeRecordBuffer

FreeRecordBuffer est le symétrique de AllocRecordBuffer. Cette méthode doit être implémentée pour libérer les buffers alloués avec AllocRecordBuffer. Comme nos buffers sont de simples allocations mémoire, il suffit de faire le free correspondant :

// Libère la mémoire utilisée par le buffer <Buffer>. <Buffer> doit contenir
// un buffer préallablement alloué avec AllocRecordBuffer.
procedure TMemoryDataSet.FreeRecordBuffer(var Buffer: PChar);
begin
  // On trace l'appel à FreeRecordBuffer au niveau TRACE_LEVEL_VERBOSE.
  // On pourra ainsi suivre les allocations/libération de buffer effectuées par
  // le TDataSet et analyser son fonctionnement en détails si besoin.
  SQLLogger.Trace(EVENT_TRACE_TYPE_INFO, 'FreeRecordBuffer',
    TRACE_LEVEL_VERBOSE);

  // Libération de la mémoire.
  FreeMem(Buffer);
end;

III-B-4-d. InternalInitRecord

La méthode InternalInitRecord peut être surchargée pour initialiser un buffer avec une valeur spécifique. La méthode de base dans TDataSet ne fait rien.

Nous la surchargeons pour forcer tous les champs à NULL dans les nouveaux buffers :

// Permet d'initialiser une nouveau <Buffer>. Le <Buffer est initialisé en
// définissant tous les champs à NULL.
procedure TMemoryDataSet.InternalInitRecord(Buffer: PChar);
var i : integer;
begin
  // On trace l'appel à InternalInitRecord au niveau TRACE_LEVEL_VERBOSE.
  // On pourra ainsi suivre les allocations/libération de buffer effectuées par
  // le TDataSet et analyser son fonctionnement en détails si besoin.
  SQLLogger.Trace(EVENT_TRACE_TYPE_INFO, 'InternalInitRecord',
    TRACE_LEVEL_VERBOSE);

  // On définit tous les champs à NULL un par un.
  for i := 0 to high(FFieldInfo) do
  begin
    ClearField(Buffer, FFieldInfo[i].Offset);
  end;
end;
ClearField est une procédure utilitaire inline pour forcer le statut d'un champ à NULL.


III-B-4-e. Accès direct aux champs

Par la suite, nous aurons constamment besoin d'accéder aux champs individuellement. Comme ces derniers sont alignés à l'intérieur d'un buffer, nous avons besoin d'une série de fonctions permettant d'accéder directement à la structure d'un champ. Ces dernières doivent être particulièrement efficaces. De plus elles sont très simples.

Aussi pour avoir les meilleures performances, nous allons les définir inline :

// Renvoie un pointeur sur le champ qui se trouve à l'offset <Offset> dans
// le buffer <Buffer>.
function GetPFieldData(Buffer : PChar; Offset : cardinal) : PFieldData; inline;
begin
  result := PFieldData(Buffer + Offset);
end;

// Renvoie true si le champ pointé par <Field> vaut NULL.
function IsNull(Field : PFieldData) : boolean; inline; overload;
begin
  result := Field.NullStatus < 0;
end;

// Renvoie true si le champ qui se trouve à l'offset <Offset> du buffer
// <Buffer> vaut NULL.
function IsNull(Buffer : PChar; Offset : cardinal) : boolean; inline; overload;
begin
  result := IsNull(GetPFieldData(Buffer, Offset));
end;

// Indique que le champ qui se trouve à l'offset <Offset> dans le buffer
// <Buffer> contient une valeur autre que NULL.
procedure ClearField(Buffer : PChar; Offset : cardinal); inline;
begin
  GetPFieldData(Buffer, Offset).NullStatus := DBNULL_VALUE;
end;

III-C. Ouverture/Fermeture

Voyons à présent les méthodes à implémenter pour ouvrir le DataSet.


III-C-1. InternalOpen

InternalOpen est appelée par TDataSet lorsque l'utilisateur veut ouvrir le dataset. Cette méthode doit initialiser le curseur sur la source de données et créer les TField. C'est le moment pour effectuer toutes les initialisations du DataSet :

// Procède à l'ouverture du DataSet.
procedure TMemoryDataSet.InternalOpen;
var
  currentTime : int64;
begin
  SQLLogger.TraceBegin(EVENT_TRACE_TYPE_START, 'InternalOpen', currentTime,
    TRACE_LEVEL_VERBOSE);
  try
    FCursor := -1; // On se positionne au début du DataSet.

    // On va ouvrir le DataSet. C'est le moment pour calculer la taille des
    // buffers.
    CalcRecordSize;

    // Si les TField n'ont pas été créés en Design, on détruit les champs
    // existant pour les recréer à partir de leur définition actuelle.
    if DefaultFields
    then begin
      DestroyFields;
      CreateFields;
    end;

    // A présent, il faut faire mapper les TField sur les champs définis
    // dans TFieldDefs. En effet, les TField ont pu être créés en Design. Dans
    // ce cas, l'ordre des champs dans Fields peut ne pas être le même que dans
    // FieldDefs. Hors il faut initialiser la propriété FieldNo de chaque champ
    // pour connaître sa position à l'intérieure des buffers.
    BindFields(true);

    FIsCursorOpen := true; // L'ouverture vient d'être faite.

  finally
    SQLLogger.TraceEnd(EVENT_TRACE_TYPE_END, 'InternalOpen', currentTime,
      TRACE_LEVEL_VERBOSE);
  end;
end;

III-C-2. InternalClose

Lors de la fermeture du DataSet, on a juste besoin de détruire les éléments qui ont été créés au moment de l'ouverture :

// Procède à la fermeture du DataSet.
procedure TMemoryDataSet.InternalClose;
var
  currentTime : int64;
begin
  SQLLogger.TraceBegin(EVENT_TRACE_TYPE_START, 'InternalClose', currentTime,
    TRACE_LEVEL_VERBOSE);
  try
    // Annule le binding des TField.
    BindFields(False);

    // Si les champs ont été créés à l'ouverture du DataSet, c'est le moment
    // de les détruire.
    if DefaultFields
    then DestroyFields;

    FIsCursorOpen := false; // Le curseur vient d'être fermé.
  finally
    SQLLogger.TraceEnd(EVENT_TRACE_TYPE_END, 'InternalClose', currentTime,
      TRACE_LEVEL_VERBOSE);
  end;
end;

III-C-3. IsCursorOpen

Cette fonction est utilisée par certaines méthodes du TDataSet pour savoir si le curseur de la source de données a déjà été ouvert. Il suffit de retourner la valeur mémorisée dans FIsCursorOpen.

// Indique si le curseur a été ouvert.
function TMemoryDataSet.IsCursorOpen: Boolean;
begin
  result := FIsCursorOpen;
end;

III-C-4. InternalInitFieldDefs

Cette méthode est appelée par TDataSet lorsque la classe de base veut récupérer la définition des champs TFieldDefs de la table, sans procéder pour autant à son ouverture.

Comme dans l'implémentation du TMemoryDataSet les TFieldDef doivent être créés manuellement en premier lieu, on n'a pas besoin de faire quoi que ce soit dans cette méthode.

On va simplement se contenter de l'instrumenter pour qu'on puisse par la suite étudier le fonctionnement du TDataSet.

// Doit être surchargé pour initialiser la liste des FieldDefs, sans ouvrir
// le curseur sur la source de données.
procedure TMemoryDataSet.InternalInitFieldDefs;
begin
  SQLLogger.Trace(EVENT_TRACE_TYPE_INFO, 'InternalInitFieldDefs',
    TRACE_LEVEL_VERBOSE);

  // Comme la structure du dataset est définie manuellement par l'intermédiaire
  // des FieldDefs, on n'a rien à faire. Ils ont déjà été définits.
end;

III-D. Navigation


III-D-1. GetRecord

On arrive à présent au cœur du DataSet. La méthode GetRecord est certainement la plus importante et doit faire l'objet de la plus grande attention.

Elle joue un double rôle : Elle procède à la lecture des buffers sur la source de données et elle effectue la plupart des fonctions de navigation :

// Retourne la taille d'un buffer.
function TMemoryDataSet.GetRecord(Buffer: PCha