Zodra een Discord-bot gebruikersgegevens gaat opslaan, wordt het duo discord.py SQLite een van de meest praktische antwoorden: een database in één bestand, zonder configuratie en zonder server. Maar er zit hier een belangrijke valkuil; de standaard sqlite3-module is blokkerend, en elke query bevriest de event loop van de bot, waardoor al je commando's trager worden. De oplossing is aiosqlite: een bibliotheek die SQLite naar de async/await-wereld brengt en perfect aansluit op discord.py 2.x. In dit artikel bouwen we een laag die economie-/levelgegevens asynchroon en veilig opslaat.
Waarom aiosqlite en niet sqlite3?
discord.py is gebouwd op een volledig asynchrone architectuur: een single-threaded event loop verwerkt binnenkomende berichten en interacties één voor één. Elke query die je met de standaard sqlite3 uitvoert, blokkeert die loop; terwijl de database naar schijf schrijft, kan je bot zelfs geen pings beantwoorden. aiosqlite daarentegen geeft de query door aan een achtergrondthread en levert het resultaat als een awaitbare coroutine. De praktische winst:
- Niet-blokkerende I/O: een trage query vergrendelt je andere commando's niet.
- Natuurlijke syntaxis:
async withenawaitpassen rechtstreeks in je discord.py-code. - Zelfstandig: geen aparte server zoals MySQL/PostgreSQL nodig; de database is één
.db-bestand.
Installeren is één regel: pip install aiosqlite. De onderliggende SQLite wordt al met Python meegeleverd.
De database aan de bot koppelen
De nette aanpak is om de verbinding te openen in setup_hook bij het opstarten van de bot en deze als attribuut aan het bot-object te hangen. Zo deelt elke cog dezelfde verbinding via self.bot.db. We sluiten de verbinding ook netjes af bij het afsluiten:
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")
# Verbetert gelijktijdig lezen/schrijven
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) zorgt dat lezers de schrijver niet blokkeren; dat maakt een merkbaar verschil op een bot met veel gebruikers. Dankzij CREATE TABLE IF NOT EXISTS wordt de tabel bij elke start veilig voorbereid.
Gegevens schrijven: geparametriseerde queries
Plak gebruikersinvoer nooit rechtstreeks in de SQL-tekst; dat opent de deur naar SQL-injectie. Gebruik in plaats daarvan ?-plaatshouders en een tuple met parameters. De INSERT ... ON CONFLICT-syntaxis van SQLite doet een insert-bij-afwezig, update-bij-aanwezig (upsert) in één query:
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()
Cruciaal punt: aiosqlite maakt wijzigingen niet automatisch blijvend. Als je na elke schrijfactie geen await db.commit() aanroept, worden de gegevens nooit naar schijf weggeschreven en gaan ze verloren bij een herstart van de bot.
Gegevens lezen: fetchone en fetchall
Aan de leeskant werk je via een cursor. Een async with-blok sluit de cursor automatisch:
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
Voor meerdere rijen gebruik je fetchall; bijvoorbeeld een levelranglijst (leaderboard):
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()
Wil je resultaten op kolomnaam benaderen, stel dan db.row_factory = aiosqlite.Row in; dan kun je lezen als row["balance"].
Gebruik binnen een commando
De hulpfuncties koppelen aan een commando binnen een cog is heel kort. Omdat self.bot.db overal bereikbaar is, richt het commando zich alleen op de bedrijfslogica:
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 dagelijkse munten geclaimd. Saldo: {bal}")
async def setup(bot):
await bot.add_cog(Economy(bot))
Tips voor prestaties en veiligheid
- Eén gedeelde verbinding: houd één verbinding voor de hele levensduur van de bot in plaats van er per commando een te openen en sluiten.
- Voeg indexen toe: een
CREATE INDEXop vaak bevraagde kolommen (bijv.xpvoor sorteren) versnelt grote tabellen. - Batchbewerkingen: bij het verwerken van veel rijen tegelijk gebruik je
executemanyen ééncommitaan het eind. - Back-ups: SQLite is één bestand; om het te kopiëren zonder de bot te raken, overweeg
VACUUM INTOof de online back-up-API.
SQLite is ruim voldoende voor middelgrote bots op één server. Zodra je honderdduizenden gebruikers bereikt met veel gelijktijdig schrijven, is een overstap naar PostgreSQL het overwegen waard; maar tot dat punt is aiosqlite een nette, snelle keuze.
Veelgestelde vragen
Wat gebeurt er als ik commit vergeet aan te roepen?
Je geschreven gegevens blijven alleen zichtbaar binnen de verbinding van die sessie, maar worden nooit naar schijf bewaard. Bij een herstart van de bot gaat elke wijziging na de laatste commit verloren. Maak er een gewoonte van om na elke INSERT/UPDATE/DELETE await db.commit() aan te roepen.
Kan ik niet gewoon de standaard sqlite3-module gebruiken?
Technisch werkt het, maar elke query blokkeert de event loop van de bot en vertraagt alle commando's. Op een bot met weinig verkeer merk je het misschien niet; in echt gebruik is aiosqlite de enige juiste keuze omdat het bij de asynchrone architectuur past.
Kunnen meerdere cogs veilig dezelfde database gebruiken?
Ja. Zolang je de verbinding deelt via self.bot.db, gebruiken alle cogs dezelfde enkele verbinding. De WAL-modus versoepelt gelijktijdig lezen; schrijfacties worden in de wachtrij gezet en veilig door SQLite uitgevoerd.
Wil je een solide datalaag voor je bot bouwen? Voor het ontwerpen van een aiosqlite-opzet, het migreren van je bestaande sqlite3-code naar async of het bouwen van een economie-/levelsysteem, neem contact met me op.