code

Utiliser SQLite à travers .NET

data | interface| api

Dans le cadre de plusieurs projets, j’ai été amené à utiliser une base de données SQLite dans un contexte .NET. C’est une technologie très intéressante dans le cas où l’on a besoin d’embarquer un stockage de données simple et léger dans une application cliente. Je souhaite revenir avec vous sur le fonctionnement de cette technologie et son utilisation avec l’ORM SQLite.NET. 

Qu’est-ce que SQLite ?

SQLite est un moteur de base de données relationnelles écrit en C dans les années 2000 par Richard Hipp. Sa principale différence par rapport aux SGBD standards est son fonctionnement en mode local et non en mode client/serveur. Grâce à sa présence sur de nombreuses plateformes, c’est le moteur de base de données le plus utilisé au monde. On peut citer par exemple les OS pour Smartphone (iOS, Android, Symbian…), des logiciels grands publics (Firefox, Chrome, Skype, Evernote, Adobe Photoshop Lightroom, DropBox…) ainsi que dans des bibliothèques standards de langages comme PHP ou Python.

Une base de données embarquée

Comme indiqué ci-dessus, SQLite ne fonctionne plus sur le paradigme client/serveur, et est donc parfaitement autonome. Le moteur de base de données est inclus dans la bibliothèque, qui elle-même est directement intégrée dans l’application (bibliothèque compatible avec énormément de cibles étant donné qu’il suffit d’avoir à disposition un compilateur C-ANSI). L’ensemble des données est stocké dans un unique fichier, qui a la particularité d’être indépendant du système. Ce fichier contient l’ensemble des éléments de la base (tables, index, données…) et l’accès aux données se fait simplement via l’ouverture du fichier correspondant. SQLite offre une bonne alternative à l’utilisation de fichiers textes, en apportant un cadre et une structure à la gestion de nos données tout en conservant un haut niveau de performances. Il existe également une rétrocompatibilité entre chaque version majeure, ce qui est rassurant pour la pérennité de l’applicatif.

schéma d'une solution

A propos des performances justement, étant donné que l’on n’a pas besoin de serveur, on supprime mécaniquement la latence réseau induite par les architectures de type client/serveur. On est à peu près au même niveau que des simples lectures/écritures sur le système de fichiers. Il est même possible de ne pas utiliser de fichier et de stocker l’ensemble des données en mémoire vive.

Cette architecture a de très nombreux avantages, mais pose tout de même quelques soucis :

⇒ Dès qu’un utilisateur débute une opération d’écriture, la lecture est verrouillée (et inversement) et l’utilisateur suivant sera mis en attente. Les accès simultanés par de nombreux utilisateurs peuvent donc poser des problèmes de performances.
⇒ Contrairement au mode client/serveur, il est beaucoup plus compliqué de mettre en place des mécanismes de répartition de charge.
⇒ Il n’est pas possible de découper le fichier d’une base SQLite, ce qui peut être problématique lors de l’utilisation de grosses bases de données sur certains systèmes de fichiers (je pense notamment à la limitation de 4Go par fichier sur les systèmes FAT32, mais ça laisse quand même pas mal de marge).

Attention tout de même, les inconvénients que je viens de citer sont à pondérer. Il s’agit en effet de problématiques qui sortent des cas d’utilisation standards pour lesquels SQLite a été imaginé. SQLite est plutôt destiné à être utilisé dans des scenarii où la taille des données est raisonnable (application cliente desktop ou application mobile) et où la centralisation des données est effectuée par un autre biais (service de synchronisation vers un SGBD standard par exemple). Il est particulièrement adapté dans le cas des applications ayant besoin d’un stockage local pour mode hors-ligne.

Dernier point avant de passer à la suite, SQLite ne propose pas de gestion des droits (GRANT/REVOKE). La seule possibilité est de gérer les droits directement au niveau du fichier via les mécanismes natifs du système d’exploitation.

Fonctionnement

Nous allons maintenant voir comment, à partir d’une requête SQL, le moteur se charge de réaliser l’exécution et de retourner les résultats demandés :

  1. L’interface est le point d’entrée dans la bibliothèque SQLite. Elle permet d’accéder à l’ensemble des méthodes publiques.
  2. C’est ensuite le processeur de commande SQL qui prend la main pour réaliser les étapes de compilation de la requête SQL :
    ⇒ Le tokenizer (séquenceur) a pour rôle de diviser la requête en tokens (jetons) et de les transmettre un par un au parser. Il va pour cela se baser sur les mots-clés du langage (SELECT, FROM, GROUP BY…).
    ⇒ A partir des tokens reçus, le parser (analyseur) va se charger de vérifier leur syntaxe et de leur donner un sens en fonction du contexte.
    ⇒ Enfin, à partir de l’ensemble des tokens générés, le générateur de code va produire le bytecode (code intermédiaire) correspondant.
  3. Le bytecode produit est exécuté par la machine virtuelle SQLite pour réaliser le travail demandé par la requête SQL
  4. La machine virtuelle va interpréter les instructions du bytecode en faisant appel au gestionnaire d’arbres. En effet, c’est de cette façon que SQLite représente les données contenues dans la base de données (un arbre pour chaque table et chaque index).
  5. Le gestionnaire d’arbre va chercher les données dans les pages du disque via le pager. C’est ce dernier qui est chargé des opérations de rollback/commit (gestion de l’atomicité) ainsi que du verrouillage du fichier.
  6. Enfin, l’accès effectif aux données est réalisé via des objets VFS (Virtual File System). Ces objets permettent le fonctionnement cross-platform, en fournissant des méthodes pour l’ouverture, la fermeture, la lecture et l’écriture propres à chaque système.
schéma d'une solution

SQLite utilise un typage dynamique, c’est-à-dire qu’au moment de la création de la table, un type dit « d’affinité » est déterminé. Lors de l’insertion, SQLite essaye de convertir la donnée vers ce type, si c’est possible. Quoi qu’il arrive, c’est toujours la cellule qui est chargée de déterminer le type « final » qui correspond à sa représentation en mémoire. A partir de la version 3, SQLite utilise des types différents pour le stockage des données et leur représentation en mémoire (avant cette version, tout était stocké sous forme de chaine de caractères). Voilà la liste des types disponibles :

⇒ NULL
⇒ INTEGER
⇒ REAL
⇒ TEXT
⇒ BLOB

Attention, SQLite ne propose pas de gestion native des dates. Il est néanmoins possible de les stocker sous la forme de chaine de caractères (format ISO 8601) ou d’entier (timestamp), et de les manipuler ensuite via des fonctions natives.

SQLite gère bien évidemment les contraintes (PRIMARY KEY, UNIQUE, FOREIGN KEY, NOT NULL, DEFAULT…). Attention tout de même, le support des FOREIGN KEY est géré à partir de la version 3.6.19 et n’est pas activé par défaut. Du côté des déclencheurs (TRIGGERS), le support est lui aussi présent, même chose pour les vues (en lecture seule uniquement) ainsi que pour les tables temporaires et les index (simple ou multi colonnes). Par contre, il n’existe pas de support pour les procédures stockées (mais ce n’est pas forcément gênant au vue de la cible de SQLite).

Du côté des transactions, SQLite va en créer une pour chaque requête visant à modifier la base de données (transaction implicite avec autocommit). Ce fonctionnement permet de garantir l’atomicité des requêtes (mais peut diminuer les performances ; cf. le chapitre à ce sujet).

Configuration

Pour terminer ce chapitre, je vais revenir sur un sujet important : la configuration. De base, SQLite ne nécessite pas de configuration particulière. Mais il est tout de même possible de paramétrer plus ou moins finement certains points de notre base de données via les directives PRAGMA :

⇒ Gestion des index automatiques : PRAGMA automatic_index = true/false
⇒ Gestion de la case lors de l’utilisation du « LIKE » dans une requête : PRAGMA case_sensitive_like = true/false
⇒ Activation/désactivation des clés étrangères : PRAGMA foreign_keys = true/false
⇒ Passage de la base en lecture seule : PRAGMA query_only = true/false
⇒ Gestion de l’encodage : PRAGMA encoding = « UTF-8/16/16le/16be »
⇒ Opérations de nettoyage et d’optimisation des données : PRAGMA auto_vacuum / PRAGMA optimize / PRAGMA shrink_memory
⇒ Gestion du fichier des transactions (plus de détails dans le chapitre sur les performances)

collaborateur chez C2S

Matthieu Anceret, Ingénieur d’Etudes

Vous avez trouvé cet article intéressant ? Partagez le sur vos réseaux sociaux !

Partager sur linkedin
LinkedIn
Partager sur twitter
Twitter
Siège C2S – Guyancourt Parc Ariane III, Rue Alfred Kastler,
78280 Guyancourt
E-mail: contact@c2s.fr
Téléphone: +33 1 30 60 82 00