Accueil / Articles PiApplications. / La plate-forme Java / Librairies BasicLib

Création et utilisation d'un DbFile.

La librairie BasicLib a été créée pour se substituer à la librairie Jmp dès qu'une compatibilité est nécessaire entre des programmes Java et des programmes Android. En effet, il faut bien admettre que le langage sur les plates-formes Android a plusieurs longueurs de retard sur la version 1.8 du langage Java. Par exemple, il n'est pas possible d'utiliser les streams, les expressions lambda ou la simplification d'écriture apportée par l'interface Closeable dans l'écriture d'une clause try.

Cette librairie définit plusieurs packages. Nous allons nous intéresser ici au packageeu.piapplications.file et plus particulièrement à la classe DbFile.

Intérêt de la classe DbFile.

Il peut arriver qu'il faille stocker des données sur un support de masse sans que cela justifie l'emploi de l'artillerie lourde (base de données en particulier) notamment en l'absence d'accès simultané aux données..

Or l'emploi traditionnel des fichiers via des flux (objets dérivés des classes InputStream et OutPutStream) est assez peu performants. Fort heureusement, Java offre la classe RandomAccessFile qui permet un adressage direct du fichier. Toutefois, cela ne présente un réel intérêt que si l'on connaît l'indice de la première données (son offset) à lire et la talle de cette donné. Cette classe est donc parfaitement adaptée à la gestion de données de taille fixe ce qui n'est pas, hélas, le cas général.

L'idée est alors de manipuler les données à la manière d'une table indexée de base de données. Pour cela, nous nous appuyons sur deux fichiers : un fichier index et un fichier de données. L'index est de très faible taille (dans notre cas 13 octets par entrée) ce qui permet de la charger intégralement en mémoire. Chaque entrée du fichier index permet d'accéder à la donnée dans le fichier de données en livrant son offset et sa taille. De cette manière, la taille des données peut être quelconque.

Au chargement du DbFile, le fichier index est intégralement lu. Chaque entrée permet le chargement de la donnée correspondante et l'extraction de sa clef d'identification (via un objet qui implémente l'interface IKeyBuilder). Cette clef et les coordonnées de la donnée dans le fichier de données sont ensuite stockées en mémoire dans un arbre binaire équilibré. Ce dispositif permet, lorsque l'on à connaissance de la clef, d'accéder à la donnée en quelques milli secondes même si le fichier à une taille de plusieurs dizaines de méga-octets. Le fait de ne pas stocker la clef dans le fichier index permet de charger en mémoire des listes de très grandes tailles (un fichier index d'un million d'entrées ne "pèse" que 12 méga-octets).

Nous avons vu comment "lire" un fichier. Pour l'enrichir, cela est très simple, on ajoute la donnée directement à la fin du fichier de données, on ajoute une entrée en fin du fichier index et on ajoute la clef à l'arbre binaire en mémoire. Tout ceci est également très rapide car sur les fins de fichier on peut utiliser facilement l'adressage direct.

La suppression peut présenter, quant à elle, un obstacle majeur au maintien des performances. Ce serait le cas s'il fallait réorganiser les fichiers à chaque suppression. Nous allons l'éviter en utilisant la suppression logique : dans l'entrée de l'index un octet indique si la donnée est supprimée ou non. Ainsi, supprimer une donnée se résume à lever ce drapeau. Là encore, comme le fichier index utilise des enregistrements de taille fixe on peut facilement utiliser l'adressage direct pour lever cet indicateur ce qui fait de la suppression un opération également très rapide. Notez que pour supprimer toutes les données, il existe la méthode purge tout aussi rapide mais plus radicale : elle vide le fichier index via un troncature. L'intérêt de cette méthode sur la suppression une à une des données est le gain de taille du fichier index.

Les plus attentif auront noté que la suppression ne touche pas au fichier de données. Si l'on ne fait rien, le fichier d'index va croître (ce qui n'est pas très grave puisque l'on ne conserve en mémoire que les entrées "non supprimées"). Toutefois l'accroissement non limité du fichier de données peut entraîner à la longue un sérieux gaspillage de ressource. La classe DbFile fournit donc des méthodes qui permettent de connaître le taux de fragmentation de chacun des fichiers. La méthode pack permet alors de restructurer les fichiers pour éliminer leurs données inutiles.

Avant d'aborder des exemples concrets, il faut nous intéresser à la "clef". Une clef doit avoir deux vertus : permettre les comparaisons et être duplicable. Permettre les comparisons entre elles semble une évidence à partir du moment où il faut les classer dans un arbre binaire. Le besoin de duplication est plus subtil. Imaginez qu'il soit possible de modifier la clef. Sa place dans l'arbre binaire serait alors erronée et son accès peut être définitivement impossible. Or il peut être nécessaire d'obtenir la liste des clefs. Pour éviter tout risque, cette liste ne retourne pas les clefs mais une "copie profonde" de ces dernières. Si elles ont ensuite modifiées, cela ne changera pas leur valeur dans l'arbre et toutes les données resteront ainsi accessibles. C'est pour cette raison que les clefs doivent toujours implémenter l'interface IDbKey qui hérite elle-même de l'interface Comparable. Un exemple de clef sur chaîne de caractères est disponible grâce à la classe CloneableString.

Création, purge et compactage.

KeyBuilder kbl = new KeyBuilder();
DbFile dfl = DbFile.loadOrCreate(trn, log, new File(_sPath), _sName, kbl);
dfl.purge();
dfl.pack();

La classe KeyBuilder implémente l'interface IKeyBuilder. Son rôle est d'extraire la clef portée par la donnée. En voici un exemple simple :

import eu.piapplications.file.CloneableString;
import eu.piapplications.file.IKeyBuilder;
import eu.piapplications.file.IDbKey;

public class KeyBuilder implements IKeyBuilder
{
  @Override
  public IDbKey buildKey(byte[] ab)
  {
    // ab est le tableau d'octets représentant la donnée lu dans le fichier de données
    String s = new String(ab);
    String[] asField = s.split(";");
    return new CloneableString(asField[0]);
  }
}

Ajout de données au DbFile.

int iMaxItem = 1000000;
    for (int k = 0; k < iMaxItem; k++)
    {
      int iKey = k;
      CloneableString clnKey = new CloneableString(String.format("clef %d", iKey));
      String s = String.format("%s;Donnée associée à la clef %d", clnKey.get(), iKey);
      dfl.insertData(clnKey, s.getBytes());
    }

Suppression de données.

int k = 0;
    while (k <= dfl.count())
    {
      CloneableString clnKey = new CloneableString(String.format("clef %d", k));
      boolean f = dfl.deleteData(clnKey);
      k += 10;
      if (f)
        System.out.println(String.format("Donnée de clef [%s] supprimée.", clnKey.get()));
      else
        System.out.println(String.format("ECHEC DE SUPPRESSION de la donnée de clef [%s] (clef probablement inexistante).", clnKey.get()));
    }

Modification de données.

int iKey = 5;
    while (iKey < dfl.count())
    {
      iCounter++;
      CloneableString clnKey = new CloneableString(String.format("clef %s", iKey));
      String sValue = String.format("Donnée %d modifiée", iKey);
      dfl.updateData(clnKey, sValue.getBytes());
      byte[] ab = dfl.getData(clnKey);
      if (ab != null)
        System.out.println(String.format("Contenu de la donnée associée à la clef [%s] : %s", clnKey.get(), new String(ab)));
      else
        System.out.println(String.format("IMPOSSIBLE DE RETROUVER LES DONNEES ASSOCIEES A LA CLEF [%s].", clnKey.get()));
      iKey += 10;
    }

Voici quelques ordres de grandeur avec un DbFile de quelques dizaines de milliers d'entrées :

Chargement du DbFile de 90 909 données en 1662 ms
Création de 100 000 données en 3 694 ms
Suppression de 9 091 données du DbFile en 3 839 ms
Modification de 9 091 données du DbFile en 335 ms
Nombre d'entrées de l'index = 90 909
Nombre d'entrées supprimées = 9 091
   Fragmentation de l'index = 10,00 %
  Fragmentation des données = 17,37 %

(c) PiApplications 2016