SQLite ou PostgreSQL : une base de code, deux moteurs, une variable d'environnement
La plupart des boilerplates choisissent une base de données et la scellent
dans le code. GoVueKit supporte les deux, sélectionnées par DB_DRIVER, et
le même SQL tourne sur l'une comme sur l'autre. Cet article explique
comment, ce que ça coûte, et — la partie qui compte vraiment — quand SQLite
est un choix de production légitime plutôt qu'un jouet.
Pourquoi s'embêter à supporter les deux
Le développement. SQLite veut dire que make dev fonctionne sur un
portable neuf sans service à installer, sans conteneur à démarrer, sans
identifiants à inventer. Le fichier est créé et migré au premier
lancement.
La petite production. Un produit mono-serveur avec quelques milliers d'utilisateurs et un trafic surtout en lecture tourne parfaitement sur SQLite, avec des sauvegardes qui sont une copie de fichier.
Des tests qui veulent dire quelque chose. La CI fait tourner toute la suite Go deux fois, une par moteur. Les bugs qui n'apparaissent que sur un moteur sont trouvés par la machine, pas par un client.
L'optionalité. Si vous grandissez jusqu'aux écritures concurrentes, vous changez une variable au lieu de réécrire des requêtes.
Comment la portabilité est réellement obtenue
Quatre règles, moins contraignantes qu'elles n'en ont l'air.
1. Des types portables uniquement. TEXT pour les identifiants (des
UUID générés en Go, jamais SERIAL), TIMESTAMP pour le temps, BOOLEAN,
INTEGER. Pas de type de colonne propre à un moteur, pas de JSONB, pas de
tableaux.
2. RETURNING est autorisé. Les deux moteurs le supportent, ce qui
supprime la danse du dernier identifiant inséré et garde les insertions à
un aller-retour.
3. Une seule génération de code. sqlc génère contre le dialecte
PostgreSQL avec des placeholders $1 — et le pilote SQLite en pur Go
(modernc.org/sqlite) accepte $N nativement. Un seul jeu de types Go
générés sert donc les deux moteurs au lieu de deux packages divergents.
4. Pas de CGO. modernc.org/sqlite est SQLite traduit en Go. C'est ce
qui permet à go build de compiler en croisé un binaire statique sans
chaîne d'outils C, et c'est la raison pour laquelle l'histoire du « un seul
fichier à déployer » tient sur les deux moteurs.
Ce à quoi vous renoncez : les opérateurs JSONB, la recherche plein texte
de PostgreSQL, les colonnes tableau, les vues matérialisées,
LISTEN/NOTIFY. Si la fonctionnalité centrale de votre produit a besoin de
l'un d'eux, prenez PostgreSQL dès le premier jour — le kit y tourne
nativement et vous cessez simplement de vous soucier du chemin SQLite.
Quand SQLite suffit en production
La limite de SQLite, ce sont les écritures concurrentes : un écrivain à la fois, en série. En pratique, en mode WAL, ça supporte bien plus que ce que les gens supposent — des milliers d'écritures par seconde sur un matériel correct, parce que chaque écriture est courte.
C'est un bon choix quand :
- un seul serveur applicatif suffit (SQLite est un fichier, pas un service réseau) ;
- le trafic est surtout en lecture, ce qui décrit la plupart des SaaS B2B ;
- vous préférez la simplicité d'exploitation à la montée en charge horizontale ;
- votre plan de sauvegarde peut être « copier le fichier, plus le WAL ».
Passez à PostgreSQL quand :
- il vous faut plus d'une instance applicative partageant l'état ;
- les écritures sont soutenues et concurrentes (ingestion d'événements, chat, télémétrie) ;
- vous voulez des sauvegardes managées, des réplicas, une restauration à un instant donné ;
- vos requêtes ont besoin de ce que seul PostgreSQL offre, selon la liste ci-dessus.
Le résumé honnête : SQLite va plus loin que sa réputation, et au moment où il vous faut deux serveurs applicatifs, plus rien de tout ça ne compte — le fichier ne se partage pas, et vous migrez.
Passer de l'un à l'autre
Le schéma est le même ; seules les données ont à voyager. Le chemin est un export et un chargement, pendant une fenêtre de maintenance :
- Arrêtez l'application.
- Exportez chaque table depuis SQLite (CSV ou instructions
INSERT). - Démarrez avec
DB_DRIVER=postgresetAUTO_MIGRATE=truepour que goose crée le schéma. - Chargez les données, puis vérifiez le nombre de lignes par table et relancez la suite de tests contre la nouvelle base.
- Redémarrez l'application.
Il n'y a aucune dérive de schéma générée par un ORM à réconcilier, parce que les migrations sont les mêmes fichiers. Faites-le tôt — déplacer 50 000 lignes est une soirée, 50 millions un projet.
Les règles à garder si vous éditez le SQL
La portabilité est une discipline, pas une bibliothèque. Trois habitudes la maintiennent :
- Écrivez la requête dans
internal/db/queries/*.sql, jamais en ligne dans le Go, pour que chaque instruction soit relisible à un seul endroit. - Lancez
make testavant de commiter. La CI fait tourner les deux moteurs ; votre machine tourne sur SQLite par défaut, etTEST_DB_DRIVER=postgres(avecTEST_POSTGRES_DSNpointant vers un serveur) fait tourner la même suite en local. - Évitez l'extension tentante. À l'instant où vous écrivez
JSONB, décidez explicitement que PostgreSQL devient obligatoire, et notez-le dansDECISIONS.md.
Ce dernier point est le vrai compromis. La portabilité vous coûte les fonctionnalités avancées des deux moteurs ; en échange, le développement n'a aucune dépendance et la production a une porte de sortie.
La question qui dérange
Supporter deux moteurs vaut-il le coup pour votre produit ? Si vous savez
que vous tournerez sur PostgreSQL, vous pouvez supprimer le chemin SQLite et
utiliser JSONB librement. Le kit ne vous en empêche pas — c'est votre
code source.
Ce que vous perdriez, c'est la propriété qui rend le premier lancement gratuit : cloner le dépôt, démarrer le serveur, et la base est un fichier qui n'existait pas une seconde plus tôt. C'est aussi ce qui rend la suite de tests assez rapide pour tourner à chaque sauvegarde.
Gardez les deux tant que vous êtes petit. Abandonnez-en un délibérément,
avec une ligne dans DECISIONS.md, si votre produit dépasse le compromis.
Voyez comment la couche de données s'assemble, ou lisez le guide de déploiement pour la mise en production sur l'un ou l'autre moteur.