Dès qu'un bot Discord se met à stocker des données utilisateur, le duo discord.py SQLite devient l'une des réponses les plus pratiques : une base de données en un seul fichier, sans configuration et sans serveur. Mais il y a un piège majeur ; le module standard sqlite3 est bloquant, et chaque requête gèle la boucle d'événements du bot, ralentissant toutes vos commandes. La solution est aiosqlite : une bibliothèque qui amène SQLite dans le monde de l'async/await et s'intègre parfaitement à discord.py 2.x. Dans cet article, nous construisons une couche qui stocke des données d'économie/de niveau de façon asynchrone et sûre.
Pourquoi aiosqlite et pas sqlite3 ?
discord.py repose sur une architecture entièrement asynchrone : une boucle d'événements mono-thread traite les messages et interactions les uns après les autres. Chaque requête lancée avec le sqlite3 standard bloque cette boucle ; pendant que la base écrit sur le disque, votre bot ne peut même plus répondre aux pings. aiosqlite, lui, délègue la requête à un thread d'arrière-plan et renvoie le résultat sous forme de coroutine à « awaiter ». Les gains concrets :
- E/S non bloquantes : une requête lente ne verrouille pas vos autres commandes.
- Syntaxe naturelle :
async withetawaits'intègrent directement à votre code discord.py. - Autonome : aucun serveur séparé comme MySQL/PostgreSQL à installer ; la base est un seul fichier
.db.
L'installation tient en une ligne : pip install aiosqlite. Le SQLite sous-jacent est déjà livré avec Python.
Brancher la base de données au bot
L'approche la plus propre est d'ouvrir la connexion dans setup_hook au démarrage du bot et de l'attacher à l'objet bot comme attribut. Ainsi chaque cog partage la même connexion via self.bot.db. Nous fermons aussi proprement la connexion à l'arrêt :
import discord
from discord.ext import commands
import aiosqlite
class MyBot(commands.Bot):
def __init__(self):
intents = discord.Intents.default()
super().__init__(command_prefix="!", intents=intents)
self.db: aiosqlite.Connection | None = None
async def setup_hook(self):
self.db = await aiosqlite.connect("data.db")
# Améliore les lectures/écritures concurrentes
await self.db.execute("PRAGMA journal_mode=WAL;")
await self.init_db()
async def init_db(self):
await self.db.execute("""
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY,
balance INTEGER NOT NULL DEFAULT 0,
xp INTEGER NOT NULL DEFAULT 0
)
""")
await self.db.commit()
async def close(self):
if self.db is not None:
await self.db.close()
await super().close()
PRAGMA journal_mode=WAL (Write-Ahead Logging) empêche les lecteurs de bloquer l'écrivain ; cela fait une différence nette sur un bot multi-utilisateurs. Grâce à CREATE TABLE IF NOT EXISTS, la table est préparée en toute sécurité à chaque démarrage.
Écrire des données : requêtes paramétrées
Ne collez jamais une entrée utilisateur directement dans le texte SQL ; cela ouvre la porte à l'injection SQL. Utilisez plutôt les marqueurs ? et un tuple de paramètres. La syntaxe INSERT ... ON CONFLICT de SQLite réalise un insert-si-absent, update-si-présent (upsert) en une seule requête :
async def add_balance(db, user_id: int, amount: int):
await db.execute(
"""
INSERT INTO users (user_id, balance)
VALUES (?, ?)
ON CONFLICT(user_id)
DO UPDATE SET balance = balance + excluded.balance
""",
(user_id, amount),
)
await db.commit()
Point crucial : aiosqlite ne rend pas les modifications persistantes automatiquement. Si vous n'appelez pas await db.commit() après chaque écriture, les données ne sont jamais écrites sur le disque et sont perdues au redémarrage du bot.
Lire des données : fetchone et fetchall
Côté lecture, vous travaillez via un curseur. Un bloc async with ferme le curseur automatiquement :
async def get_balance(db, user_id: int) -> int:
async with db.execute(
"SELECT balance FROM users WHERE user_id = ?",
(user_id,),
) as cursor:
row = await cursor.fetchone()
return row[0] if row else 0
Pour plusieurs lignes, utilisez fetchall ; par exemple un classement (leaderboard) de niveaux :
async def top_users(db, limit: int = 10):
async with db.execute(
"SELECT user_id, xp FROM users ORDER BY xp DESC LIMIT ?",
(limit,),
) as cursor:
return await cursor.fetchall()
Si vous voulez accéder aux résultats par nom de colonne, définissez db.row_factory = aiosqlite.Row ; vous pourrez alors lire comme row["balance"].
L'utiliser dans une commande
Brancher les fonctions utilitaires sur une commande à l'intérieur d'un cog est très court. Comme self.bot.db est accessible partout, la commande se concentre uniquement sur la logique métier :
from discord.ext import commands
class Economy(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
async def daily(self, ctx):
await add_balance(self.bot.db, ctx.author.id, 100)
bal = await get_balance(self.bot.db, ctx.author.id)
await ctx.send(f"100 pièces quotidiennes réclamées. Solde : {bal}")
async def setup(bot):
await bot.add_cog(Economy(bot))
Conseils de performance et de sécurité
- Une connexion partagée : gardez une seule connexion pour toute la durée de vie du bot au lieu d'en ouvrir/fermer une par commande.
- Ajoutez des index : définir un
CREATE INDEXsur les colonnes souvent interrogées (par ex.xppour le tri) accélère les grandes tables. - Opérations par lots : pour traiter de nombreuses lignes d'un coup, utilisez
executemanyet un seulcommità la fin. - Sauvegardes : SQLite est un seul fichier ; pour le copier sans toucher au bot, envisagez
VACUUM INTOou l'API de sauvegarde en ligne.
SQLite est largement suffisant pour des bots de taille moyenne sur un seul serveur. Une fois atteintes des centaines de milliers d'utilisateurs avec de fortes écritures concurrentes, un passage à PostgreSQL mérite réflexion ; mais jusque-là, aiosqlite reste un choix simple et rapide.
Questions fréquentes
Que se passe-t-il si j'oublie d'appeler commit ?
Les données écrites restent visibles uniquement dans la connexion de cette session, mais ne sont jamais persistées sur le disque. Au redémarrage du bot, toutes les modifications postérieures au dernier commit sont perdues. Prenez l'habitude d'appeler await db.commit() après chaque INSERT/UPDATE/DELETE.
Ne puis-je pas simplement utiliser le module sqlite3 standard ?
Techniquement, ça marche, mais chaque requête bloque la boucle d'événements du bot et retarde toutes les commandes. Sur un bot à faible trafic, vous ne le remarquerez peut-être pas ; en usage réel, aiosqlite est le seul bon choix car il épouse l'architecture asynchrone.
Plusieurs cogs peuvent-ils utiliser la même base en toute sécurité ?
Oui. Tant que vous partagez la connexion sur self.bot.db, tous les cogs utilisent la même connexion unique. Le mode WAL facilite les lectures concurrentes ; les écritures sont mises en file et exécutées en toute sécurité par SQLite.
Vous voulez bâtir une couche de données solide pour votre bot ? Pour concevoir une architecture aiosqlite, migrer votre code sqlite3 existant vers l'asynchrone ou créer un système d'économie/de niveau, contactez-moi.