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.
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.
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étro-compatibilité entre chaque version majeure, ce qui est rassurant pour la pérennité de l’applicatif.
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.
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 :
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.
Le 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).
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 :
⇒ Maintenance 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
⇒ Direction 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
⇒ Conduite du fichier des transactions (plus de détails dans le chapitre sur les performances)Ma
Matthieu Anceret, Ingénieur d’Etudes
Vous avez trouvé cet article intéressant ? Partagez le sur vos réseaux sociaux !
Siège C2S – WOJO, Issy-les-Moulineaux
41-43 rue Camille Desmoulins, 92130 Issy-les-Moulineaux
Depuis plus de 30 ans nous sommes l’Entreprise de Services du Numérique du groupe Bouygues.
Nous travaillons en étroite coopération avec les DSI et les métiers de la construction, des télécommunications et des médias du Groupe.