Bases de données

Betula a été initialement développé pour faciliter l’accès aux données. Voila pourquoi 14 classes s’occupent de gérer les connexions, de récupérer des résultats et de traiter les enregistrements.

Il faut noter qu’une analyse de données n’est pas obligatoire dans le projet. En effet, la première connexion à une table va récupérer sa structure physique. Par cette méthode « sans analyse », il n’est donc plus nécessaire de « synchroniser les structures » de données (qu’il va donc falloir faire manuellement). C’est principalement intéressant dans les BD comme SQL Server, Oracle, … qui sont gérés par d’autres outils, voire d’autres personnes (DBA). Il est cependant conseillé de garder une analyse lorsqu’on utilise des bases de données Hyperfile puisque la mise à jour de leur structure doit exclusivement se faire depuis WinDev.

Une autre « particularité » du framework est d’utiliser la nomenclature utilisée pour toutes les bases de données. Ce sont donc les termes de « table » et de « colonne » qui sont utilisés à la place de « fichier » et « rubrique ».

Connexion

Si une analyse existe, une (ou plusieurs) connexions peuvent y être paramétrées. Ces connexions sont habituellement décrites pour l’environnement de développement. L’analyse n’étant plus obligatoire, et pour plus de souplesse et de clarté, Betula propose des méthodes de connexion pour être utilisées de manière explicite dans le code.

Pour faire une connexion, il faut instancier la classe cBaseDeDonnéesXXXX relative à la base de données choisie. Ensuite, utiliser la fonction Connecte.

Une connexion renvoie un entier. Cet entier sert ensuite de référence dans le reste de la programmation, notamment lors des appels aux fonctions de cEnregistrement et de cSourceDeDonnées. Pour une connexion utilisée partout dans un votre projet, il est donc conseillé d’utiliser une variable globale (de type entier) au projet.

L’accès aux données peut se faire soit via les accès natifs, soit via ODBC (quand c’est permis). En accès natif, le choix est aussi proposé d’utiliser les fonctions hxxx ou les requêtes SQL. Les connexions OleDB ne sont pas géré car réputées plus lente que ODBC. D’ailleurs, contrairement à l’ODBC qui est passé en version 4, l’OleDB a été abandonnée en 2017 par Microsoft. Le framework profitera donc de cet abandon pour réutiliser les constantes OleDBxxx autrement (voir le particularités ci-dessous).

La fonction Connecte sait que l’utilisation de l’ODBC implique parfois l’existence d’autres fichiers (dont il va vérifier l’existence) et une configuration dans Windows (qui sera créée automatiquement si nécessaire). Tous les paramètres de connexion peuvent être récupérés à l’extérieur du programme (ex : dans un fichier INI) via la classe cAppParamètres si tant est que ces paramètres sont identifiés par un tag spécifique (ex : [SQLServer] pour une connexion SQL Server, [Oracle] pour une connexion Oracle, … voir ces tag dans les classes).

Il existe aussi une fonction Connect_ (avec un souligné). Cette dernière peut également récupérer ses paramètres via cAppParamètres mais en spécifiant le tag principal (par exemple [MonSQLServerAMoi] au lieu de [SQLServer]). Une seconde syntaxe aurait pu être créée mais les environnements Android et Java ne les acceptent pas (encore) …

Si une analyse existe (voir remarque en haut de page), la connexion est alors associée aux tables du même type (selon le cadre coloré autour de la table dans l’analyse) ou à un groupe de tables donné dans le paramètre sGroupeOuTypeOuNomDeFichiers de la fonction de Connecte(). Ce paramètre peut aussi accepter une liste de table à associer à ladite connexion. Tout ceci est nécessaire quand plusieurs connexions sont nécessaires simultanément.

A la première connexion à une base de données, le framework va récupérer la version du serveur. Ceci peut influencer le fonctionnement de la classe cBaseDeDonnéesxxx (voir p.e. cBaseDeDonnéesSQLServer.Tables).

La classe cBaseDeDonnées

La classe cBaseDeDonnées est « généraliste ». Attention, les autres classe cBaseDeDonnéesXXXX n’en hérite pas.

Elle ne peut pas, elle-même, se connecter à une base de données (puisqu’elle n’est pas typée). Par contre :

  • Toutes les connexions « physiques » y sont mémorisées dans un tableau structuré global : cBaseDeDonnées.mg_tabConnexion . Cela permet de pouvoir utiliser toutes les informations de toutes les connexions à tout moment.  Un autre tableau global mg_taUtilisationServeur mémorise toutes les demandes de connexion par appel aux classes cBaseDeDonnéesXXXX (ajoute 1 quand on utilise Connecte, retire 1 quand on utilise Déconnecte) .  Ce tableau permet d’utiliser une connexion déjà établie et ainsi éviter d’ouvrir plusieurs connexions sur un même serveur avec les mêmes accès.
  • En lui passant en paramètre l’entier d’une connexion faite au préalable, on peut faire la même chose qu’avec une classe typée : liste des tables, des colonnes, transactions, déconnexion, …
  • Elle peut identifier l’environnement de base de données utilisé durant l’exécution du programme pour voir si on est en dev, en préprod/test ou en prod. Pour cela, d’abord appeler EnvironnementAjoute pour identifier tous les serveurs et leurs bases de données et leur « environnement » correspondant (ex : le serveur SQLServer1 et sa base CRMDEV est un environnement de développement). Il suffit ensuite d’appeler la propriété p_sEnvironnement (qui utilise la méthode Environnement aussi accessible) pour l’afficher, par exemple, dans la barre de titre de l’application. Ainsi, moins de confusion, pour le développeur, entre l’environnement de production et les autres !

La classe cTable

Cette classe permet d’accéder à une table en donnant le numéro de connexion (l’entier récupéré par Connecte) et le nom de la table (ex : Client). Elle peut être utilisée directement (pour des raisons techniques) mais servira essentiellement à la classe cEnregistrement (voir ci-dessous).

La première connexion à une table va récupérer, dans le tableau global mg_taInfo, toutes ses colonnes et ses indexes. Voilà pourquoi une analyse n’est pas toujours indispensable.

La classe cEnregistrement

C’est une des deux classes privilégiée pour accéder aux données, l’autre étant cSourceDeDonnées (voir ci-dessous). Pour accéder à une table de votre base de données, créez une classe qui hérite de cEnregistrement. Par convention, les classes enregistrement commencent par ce (ex : ceClient).

En lui passant le numéro de connexion (l’entier récupéré par Connecte) et le nom de la table qu’on veut parcourir (une chaîne s’il n’y a pas d’analyse, NomDeLaTable..nom s’il y en a une), elle va utiliser cTable (voir ci-dessus) pour connaître sa structure. Il ne reste plus qu’à utiliser la fonction Recherche (à l’identique) pour trouver ce que l’on veut.

Vous remarquerez que les méthodes Ajoute, Modifie et Supprime sont privées, donc pas accessibles même par un héritage. En effet, pour simplifier, l’enregistrement a un statut (voir cette énumération au début de la classe). La méthode Écrit va alors, selon ce statut ou par comparaison avec l’enregistrement chargé avant, ajouter, modifier ou supprimer physiquement l’enregistrement.  Ce statut peut être modifié par programmation grâce à la propriété p_eStatutEnregistrement.

Des méthodes virtuelles permettent, dans la classe héritée, de faire du traitement AvantÉcriture et/ou AprèsLecture (par exemple pour désérialiser un mémo texte contenant du JSON, pour mettre une date/heure de création ou de mise à jour de l’enregistrement, …). Une autre classe virtuelle Vérifie permet de ne pas oublier de vérifier la cohérence des données avant l’écriture (ex : la validité des dates). Cette fonction est proche de la base de données, elle ne doit donc pas contenir de vérification métier.

Chaque enregistrement possède un GUID qui lui est unique qui sera utile dans la classe cSourceDeDonnées.  Chaque enregistrement a aussi, en mémoire, un hash de lui-même à chaque lecture. Ceci permet de savoir, au moment de l’écriture, si l’enregistrement a changé (s’il n’a pas changé, pas besoin de l’écrire > gain en performance).

Un enregistrement peut avoir des valeurs par défaut. Pour cela, utiliser ValeurDéfaut pour initialiser ces valeurs par défaut. Le groupe de valeur en paramètre de cette fonction permet de n’imposer des valeurs par défaut qu’à un groupe de colonnes (pas à toutes).  Pour utiliser ce principe, instancier un objet enregistrement de votre application (ceClient) et appelez ceClient.RazDéfaut.  Il existe aussi une valeur par défaut des mémos image grâce à MemoImageDéfaut (dans le cas où une colonne memo est vide, ex : la photo que quelqu’un sera remplacée par un icône si inexistante). L’image passée en paramètre est automatiquement redimensionnée par le framework aux dimensions spécifiées.

Cette classe est compatible avec les clés composées et les valeurs nulles.

Gestion des mémos et cache

Le framework permet d’optimiser la gestion des mémos (varchar, varbinay, blob, mémobinaire, …) pour gagner en performance. Dans les paramètres d’appel de la classe cEnregistrement, on peut préciser si l’on récupère (ou pas) le contenu des mémos à chaque lecture et si ces « mémos » sont placés à l’extérieurs de la table (par défaut, ce sont les mêmes paramètres que ceux définis dans la connexion aux données).

Si l’on ne récupère pas les mémos directement, dans la classe héritée (ex : ceClient), il suffit de définir une propriété en lecture/écriture pour appeler respectivement MémoTélécharge et MémoTéléverse. Le « mémo » est alors géré dans son membre en attendant l’utilisation la méthode Écrit. Cette dernière vérifie d’ailleurs, au moment de la mise à jour de l’enregistrement, si les mémos ont changés (s’ils n’ont pas changé, pas besoin de l’écrire > gain en performance). Voir Particularités ci-dessous.

Gestion des mémos hors des tables

Le framework est conçu pour gérer les mémos hors des tables, toujours dans un esprit d’optimisation du temps de réponse. Il est donc possible de mettre les « fichiers joints » (mémos) dans un répertoire local, un répertoire réseau ou un serveur FTP (dans la version actuelle). Voir Particularités ci-dessous.

La classe cSourceDeDonnées

Une « source de données » désigne habituellement, en WLangage, le type de variable qui récupère le résultat d’une requête (hExécuteRequêteSQL). Dans le framework, elle contient un tableau de cEnregistrement dynamiques (m_tabEnregistrement).  Par convention, les classes cSourceDeDonnées commencent par csd (ex : csdClient). Pour lier un cSourceDeDonnées aux objets cEnregistrement correspondants, il suffit de passer, dans le 3eme paramètre, soit rien (le nom de l’objet est par convention ce+NomDeLaTable passé en 1er paramètre), soit le nom de l’objet (ex : ceClient), soit le nom de la classe cEnregistrement spécifique (ex : « ceClientSaisonnier »).

Son chargement peut se faire par la méthode Liste qui analyse les critères et le type d’accès (FonctionH, RequêteSQL ou ODBC) pour optimiser la demande au serveur. L’on retrouve, dans les paramètres de la méthode Liste, les éléments utilisés lors d’une requête. Si les critères sont plus complexes, il y a toujours la méthode ListeRequête qui attend une requête au complet. Habituellement, la méthode Liste suffira à l’essentiel des appels.  Le chargement de cette liste peut être lié à une jauge (voir la classe cJauge).

Les méthodes Premier, Suivant, DernierPrécédent, Recherche, Trie et Occurrence manipulent les cEnregistrement déjà en mémoire suite à l’utilisation d’un Liste. Aucune lecture n’est faite dans la base de données à ce moment. Pour les méthodes Supprime, SupprimeTout, Ajoute et Modifie, c’est au moment où l’on appelle EcritTout que ces enregistrements sont mis à jour dans la base de données. Pour éviter de mélanger les enregistrements « actifs » (à ajouter ou modifier) de ceux à supprimer, ces derniers sont déplacés dans un autre tableau d’objets dynamiques : m_tabSupprimé.

Pour lire un de ces objets cEnregistrement, les fonctions Lit (et son équivalent LitSupprimé) acceptent soit le GUID de l’enregistrement, soit son indice dans le tableau manipulé.

La classe cSQL

Cette classe contient toutes les fonctions d’accès SQLxxxx utiles pour l’ODBC (mises ici pour éviter d’encadrer tous les appels à ces fonctions, dans cEnregistrement, cSourceDeDonnées, …  par du code cible) mais surtout une méthode pour l’exécution d’une Requête. cSQL.Requête récupère l’ID automatique en cas d’ajout. Voila pourquoi elle renvoie soit un booléen (en cas de SELECT ou d’UPDATE) soit l’entier de l’ID automatique.

cSQL contient le membre m_sd (une source de données) qui va servir dans … cSourceDeDonnées (vous l’avez compris).

Cette classe peut être instanciée directement s’il ne faut faire qu’une simple requête. Lui passer juste l’indice de connexion pour savoir sur quelle BD exécuter ladite requête.

Particularités

  1. La propriété « provider » de la connexion faite par ODBC sera identifiée, non pas comme hODBC (trop généraliste) mais comme hOledbXXX (ex : hOledbSQLServer) pour distinguer la connexion d’une autre mais en accès natif (constante hAccèsNatifSQLServer dans ce cas)
  2. Des méthodes permettent de gérer les « environnements » (ex : dev, test et prod). En ajoutant, au démarrage du projet, toutes les bases de données et le nom des environnements qu’elles représentent, une méthode « Environnement » permet de récupérer sous quel environnement (de données donc) le logiciel tourne. Cette information peut, par exemple, être affichée dans le titre de la fenêtre principale.
  3. Pour vérifier qu’un mémo en cache doit être mis à jour dans un enregistrement, plusieurs cas de figure :
    1. soit le mémo est dans la base de données :
      1. Si une seule colonne mémo est disponible : il serait nécessaire de refaire une lecture du mémo pour le comparer à celui dans l’objet manipulé. Pour éviter ça, le framework rajoute un hash de 32 caractères dans le contenu du mémo. Ainsi, la lecture ne récupère que ces 32 caractères (et non les Ko ou Mo du mémo complet) et le vérifie au hash du mémo contenu dans l’objet. Si cette comparaison est identique, le mémo ne sera pas mis à jour. Cette méthode implique aussi que le mémo ne peut plus être lu « tel quel » dans la base de données puisqu’on lui ajoute 32 caractères au début. Il est donc obligatoire d’utiliser le framework pour toute lecture dans ce cas.
      2. Si la table possède une autre colonne qui a pour nom celui du mémo avec « hash » à la fin : si une colonne mémo s’appelle Photo et qu’une autre colonne PhotoHash (varbinary 32) existe dans la même table, le framework comprend qu’il peut manipuler PhotoHash pour mettre à jour ou pas la photo. Cette méthode, contrairement à la précédente, permet d’ouvrir les mémos « tels quels » en dehors du framework.
    2. soit le mémo est hors de la base de données : il est alors sous forme d’un fichier (dans un répertoire ou sur FTP). Le nom du fichier est le hash du mémo (32 caractères donc). La comparaison est donc facile entre le hash du membre et le nom du fichier pour savoir si ce dernier doit être mis à jour.
  4. Il existe aussi une classe cEnregistrementLDAP car d’accès à une telle base de données est particulière (manipulation d’attributs et de propriétés). Elle doit être utilisée après l’appel de cBaseDeDonnéesLDAP.
  5. La version actuelle ne gère pas les tables sans clé unique.