games / fivem
Base de données : oxmysql et requêtes asynchrones
Explication
Ce que vous allez apprendre
- Comprendre pourquoi toutes les requêtes FiveM/MySQL doivent être asynchrones
- Utiliser l'API moderne
oxmysqlbasée sur.await(single, scalar, insert, update) - Écrire des requêtes paramétrées pour éviter totalement l'injection SQL
- Regrouper plusieurs requêtes liées dans une transaction atomique
- Ajouter un index sur une colonne filtrée fréquemment
Dans quel contexte ?
Un joueur se déconnecte après avoir accumulé de l'argent et des objets pendant des heures de jeu. Sans base de données, tout redeviendrait comme au premier jour dès le prochain redémarrage du serveur. Cette leçon pose les bases indispensables pour que la progression d'un joueur survive à une déconnexion, un redémarrage, ou même un crash du serveur.
| Fonction oxmysql | Usage |
|---|---|
MySQL.single.await(...) | Une seule ligne de résultat |
MySQL.scalar.await(...) | Une seule valeur (ex: un solde) |
MySQL.insert.await(...) | Insertion, retourne l'id inséré |
MySQL.transaction.await({...}) | Plusieurs requêtes garanties atomiques |
Piège fréquent
Construire une requête SQL en concaténant directement une valeur reçue d'un joueur dans une chaîne de texte ouvre la porte à l'injection SQL. Toujours utiliser des requêtes paramétrées (? ou @nom), jamais de concaténation de chaînes dans le SQL.
Sans base de données, rien ne survit
Toutes les données manipulées jusqu'ici (argent, inventaire) n'existent que dans la mémoire du serveur : dès que celui-ci redémarre, tout disparaît. Pour qu'un joueur retrouve sa progression d'une session à l'autre, il faut stocker ces données quelque part de façon durable — une base de données, ici MySQL, gérée depuis Lua grâce à la bibliothèque oxmysql.
Pourquoi tout est asynchrone
C'est le concept le plus important de cette leçon. Une requête vers une base de données prend du temps (quelques millisecondes, parfois plus), et le serveur FiveM tourne sur un seul fil d'exécution principal partagé par tous les joueurs. Si une requête "bloquait" ce fil en attendant sa réponse, tout le serveur se figerait le temps de la requête, pour tout le monde. Une requête asynchrone, au contraire, part en arrière-plan pendant que le reste du serveur continue de fonctionner normalement, et prévient le code via un callback (ou un await) une fois le résultat disponible.
Le danger de la concaténation SQL
Construire une requête SQL en collant directement des valeurs reçues d'un joueur dans une chaîne de texte ouvre la porte à l'injection SQL : un joueur malveillant pourrait glisser du code SQL dans une donnée censée être une simple valeur, et ainsi manipuler la base à sa guise. Les requêtes paramétrées (avec ? ou @nom) évitent totalement ce risque : la valeur est toujours traitée comme une simple donnée, jamais comme du code SQL exécutable, quoi qu'elle contienne.
Les transactions : tout ou rien
Une opération comme un transfert d'argent implique deux écritures liées (retirer d'un côté, ajouter de l'autre). Si le serveur plantait entre les deux écritures, l'argent disparaîtrait sans jamais réapparaître. Une transaction garantit que l'ensemble des requêtes qui la composent réussit entièrement, ou échoue entièrement en annulant tout ce qui avait déjà été fait — jamais un état intermédiaire incohérent.
Commandes & code
Base de données : oxmysql
oxmysql est la librairie standard pour parler à MySQL depuis une ressource serveur, toujours de façon asynchrone.
-- schema.sql
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
identifier VARCHAR(60) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
money INT DEFAULT 0,
inventory JSON DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_identifier ON users (identifier); -- accélère les lookups très fréquents-- server.cfg
-- set mysql_connection_string "mysql://user:password@localhost/technologik_db?charset=utf8mb4"
-- SELECT asynchrone avec callback (ne bloque jamais le thread principal serveur)
MySQL.Async.fetchAll('SELECT * FROM users WHERE identifier = @identifier', {
['@identifier'] = identifier
}, function(result)
if result[1] then
print('Utilisateur trouvé: ' .. result[1].name)
end
end)-- API moderne oxmysql, basée sur des coroutines Lua (syntax "await")
local function GetUser(identifier)
return MySQL.single.await('SELECT * FROM users WHERE identifier = ?', { identifier })
end
local function GetUserMoney(identifier)
return MySQL.scalar.await('SELECT money FROM users WHERE identifier = ?', { identifier }) or 0
end
-- INSERT
local function CreateUser(identifier, name)
return MySQL.insert.await('INSERT INTO users (identifier, name, money) VALUES (?, ?, ?)', {
identifier, name, 5000
})
end
-- UPDATE
local function SetUserMoney(identifier, newMoney)
MySQL.update.await('UPDATE users SET money = ? WHERE identifier = ?', { newMoney, identifier })
end-- Transaction : plusieurs requêtes garanties atomiques (tout ou rien)
local function TransferMoney(fromIdentifier, toIdentifier, amount)
local success = MySQL.transaction.await({
{ query = 'UPDATE users SET money = money - ? WHERE identifier = ? AND money >= ?',
values = { amount, fromIdentifier, amount } },
{ query = 'UPDATE users SET money = money + ? WHERE identifier = ?',
values = { amount, toIdentifier } },
})
if not success then
print('[DB] Transfert échoué, rollback automatique')
end
return success
endRésumé
- Toujours utiliser des requêtes paramétrées (
?ou@nom) : jamais de concaténation de strings dans le SQL. .awaitsimplifie l'écriture asynchrone en évitant l'imbrication de callbacks.- Les transactions garantissent la cohérence pour les opérations multi-requêtes (ex: transferts d'argent).
- Un index sur les colonnes filtrées fréquemment (
identifier) évite des scans de table coûteux à l'échelle.
Exercices pratiques
Mission : colmater une faille d'injection dans la banque
Objectif : Identifier pourquoi une requête concaténée est dangereuse, la corriger en requête paramétrée, puis sécuriser un transfert d'argent avec une transaction.
Contexte
La ressource technologik_bank contient encore une ancienne fonction qui construit sa requête ainsi : MySQL.Async.fetchAll("SELECT * FROM users WHERE identifier = '" .. identifier .. "'", {}, function(result) ... end). Un audit de sécurité vient de signaler cette ligne. Par ailleurs, la fonction TransferMoney de la leçon utilise déjà MySQL.transaction.await, mais un développeur junior propose de la simplifier en deux appels séparés MySQL.update.await pour "gagner en lisibilité".