035.3 Leçon 1
Certification : |
Web Development Essentials |
---|---|
Version : |
1.0 |
Thème : |
035 Programmation de serveur NodeJS |
Objectif : |
035.3 Bases de SQL |
Leçon : |
1 sur 1 |
Introduction
Bien que vous puissiez écrire vos propres fonctions pour mettre en œuvre le stockage persistant, il peut être plus pratique d’utiliser un système de gestion de base de données pour accélérer le développement et garantir une meilleure sécurité et stabilité pour les données sous forme de tables. La stratégie la plus populaire pour stocker des données organisées dans des tables interdépendantes, en particulier lorsque ces tables sont fortement interrogées et mises à jour, consiste à installer une base de données relationnelle qui prend en charge le langage SQL (Structured Query Language), un langage orienté vers les bases de données relationnelles. Node.js prend en charge divers systèmes de gestion de bases de données SQL. Conformément aux principes de portabilité et d’exécution en espace utilisateur adoptés par Node.js Express, SQLite est un choix approprié pour le stockage persistant des données utilisées par ce type de serveur HTTP.
SQL
Le langage de requête structuré est spécifique aux bases de données. Les opérations d’écriture et de lecture sont exprimées dans des phrases appelées déclarations et requêtes. Les déclarations et les requêtes sont composées de clauses, qui définissent les conditions d’exécution de l’opération.
Les noms et les adresses électroniques, par exemple, peuvent être stockés dans une table de base de données qui contient les champs name
et email
. Une base de données peut contenir plusieurs tables, donc chaque table doit avoir un nom unique. Si nous utilisons le nom contacts
pour la table des noms et des adresses électroniques, un nouvel enregistrement peut être inséré avec l’instruction suivante :
INSERT INTO contacts (name, email) VALUES ("Carol", "carol@example.com");
Cette instruction d’insertion est composée de la clause INSERT INTO
, qui définit la table et les champs où les données seront insérées. La deuxième clause, VALUES
, définit les valeurs qui seront insérées. Il n’est pas nécessaire de mettre la majuscule aux clauses, mais c’est une pratique courante, afin de mieux reconnaître les mots-clés SQL dans une instruction ou une requête.
Une requête sur la table des contacts est effectuée de manière similaire, mais en utilisant la clause SELECT
:
SELECT email FROM contacts;
dave@example.com
carol@example.com
Dans ce cas, la clause SELECT email
sélectionne un champ parmi les entrées de la table contacts
. La clause WHERE
restreint la requête à des lignes spécifiques :
SELECT email FROM contacts WHERE name = "Dave";
dave@example.com
SQL possède de nombreuses autres clauses, et nous en examinerons certaines dans les sections suivantes. Mais d’abord il faut voir comment intégrer la base de données SQL avec Node.js.
SQLite
SQLite est probablement la solution la plus simple pour incorporer des fonctionnalités de base de données SQL dans une application. Contrairement à d’autres systèmes de gestion de base de données populaires, SQLite n’est pas un serveur de base de données auquel un client se connecte. Au lieu de cela, SQLite fournit un ensemble de fonctions qui permettent au développeur de créer une base de données comme un fichier classique. Dans le cas d’un serveur HTTP mis en œuvre avec Node.js Express, ce fichier est généralement situé dans le même répertoire que le script du serveur.
Avant d’utiliser SQLite dans Node.js, vous devez installer le module sqlite3
. Exécutez la commande suivante dans le répertoire d’installation du serveur, c’est-à-dire le répertoire contenant le script Node.js que vous allez exécuter.
$ npm install sqlite3
Sachez qu’il existe plusieurs modules qui supportent SQLite, comme better-sqlite3
, dont l’utilisation est subtilement différente de sqlite3
. Les exemples de cette leçon sont pour le module sqlite3
, donc ils pourraient ne pas fonctionner comme prévu si vous choisissez un autre module.
Ouverture de la base de données
Pour démontrer comment un serveur Node.js Express peut fonctionner avec une base de données SQL, écrivons un script qui stocke et affiche les messages envoyés par un client identifié par un cookie. Les messages sont envoyés par le client via la méthode HTTP POST et la réponse du serveur peut être formatée en JSON ou en HTML (à partir d’un modèle), selon le format demandé par le client. Cette leçon n’entrera pas dans le détail de l’utilisation des méthodes HTTP, des cookies et des modèles. Les extraits de code présentés ici supposent que vous disposez déjà d’un serveur Node.js Express où ces fonctionnalités sont configurées et disponibles.
La façon la plus simple de stocker les messages envoyés par le client est de les stocker dans un tableau global, où chaque message précédemment envoyé est associé à une clé d’identification unique pour chaque client. Cette clé peut être envoyée au client sous la forme d’un cookie, qui est présenté au serveur lors de futures demandes de récupération de ses messages précédents.
Cependant, cette approche présente une faiblesse : comme les messages sont stockés uniquement dans un tableau global, tous les messages seront perdus lorsque la session actuelle du serveur sera terminée. C’est l’un des avantages de travailler avec des bases de données, car les données sont stockées de manière persistante et ne sont pas perdues si le serveur est redémarré.
En utilisant le fichier index.js
comme script principal du serveur, nous pouvons incorporer le module sqlite3
et indiquer le fichier qui sert de base de données, comme suit :
const sqlite3 = require('sqlite3')
const db = new sqlite3.Database('messages.sqlite3');
S’il n’existe pas déjà, le fichier messages.sqlite3
sera créé dans le même répertoire que le fichier index.js
. Dans ce fichier unique, toutes les structures et les données respectives seront stockées. Toutes les opérations de base de données effectuées dans le script seront relayées par la constante db
, qui est le nom donné au nouvel objet sqlite3
qui ouvre le fichier messages.sqlite3
.
Structure d’une table
Aucune donnée ne peut être insérée dans la base de données tant qu’au moins une table n’a pas été créée. Les tables sont créées avec l’instruction CREATE TABLE
:
db.run('CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, uuid CHAR(36), message TEXT)')
La méthode db.run()
est utilisée pour exécuter des instructions SQL dans la base de données. L’instruction elle-même est écrite en tant que paramètre de la méthode. Bien que les instructions SQL doivent se terminer par un point-virgule lorsqu’elles sont saisies dans un processeur de ligne de commande, le point-virgule est facultatif dans les instructions passées comme paramètres dans un programme.
Comme la méthode run
sera exécutée à chaque fois que le script sera exécuté avec node index.js
, l’instruction SQL inclut la clause conditionnelle IF NOT EXISTS
pour éviter les erreurs lors des futures exécutions lorsque la table messages
existe déjà.
Les champs qui composent la table messages
sont id
, uuid
, et message
. Le champ id
est un entier unique utilisé pour identifier chaque entrée de la table, il est donc créé comme PRIMARY KEY
. Les clés primaires ne peuvent pas être nulles et il ne peut pas y avoir deux clés primaires identiques dans la même table. Par conséquent, presque toutes les tables SQL ont une clé primaire afin de suivre le contenu de la table. Bien qu’il soit possible de choisir explicitement la valeur de la clé primaire d’un nouvel enregistrement (à condition qu’il n’existe pas encore dans la table), il est pratique que la clé soit générée automatiquement. L’indicateur AUTOINCREMENT
du champ id
est utilisé à cet effet.
Note
|
La définition explicite des clés primaires dans SQLite est facultative, car SQLite crée lui-même une clé primaire automatiquement. Comme indiqué dans la documentation de SQLite : “En SQLite, les lignes d’une table ont normalement un entier signé de 64 bits |
Les champs uuid
et message
stockent respectivement l’identification du client et le contenu du message. Un champ de type CHAR(36)
stocke un nombre fixe de 36 caractères, et un champ de type TEXT
stocke des textes de longueur arbitraire.
Entrée des données
La fonction principale de notre exemple de serveur est de stocker les messages qui sont liés au client qui les a envoyés. Le client envoie le message dans le champ message
du corps de la requête envoyée avec la méthode HTTP POST. L’identification du client se trouve dans un cookie appelé uuid
. Avec ces informations, nous pouvons écrire la route Express pour insérer de nouveaux messages dans la base de données :
app.post('/', (req, res) => {
let uuid = req.cookies.uuid
if ( uuid === undefined )
uuid = uuidv4()
// Insert new message into the database
db.run('INSERT INTO messages (uuid, message) VALUES (?, ?)', uuid, req.body.message)
// If an error occurrs, err object contains the error message.
db.all('SELECT id, message FROM messages WHERE uuid = ?', uuid, (err, rows) => {
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(rows)
else
res.render('index', {title: "My messages", rows: rows})
})
})
Cette fois, la méthode db.run()
exécute une déclaration d’insertion, mais notez que le uuid
et le req.body.message
ne sont pas écrits directement dans la ligne de déclaration. Au lieu de cela, des points d’interrogation ont été substitués aux valeurs. Chaque point d’interrogation correspond à un paramètre qui suit l’instruction SQL dans la méthode db.run()
.
L’utilisation de points d’interrogation comme caractères de remplissage dans l’instruction qui est exécutée dans la base de données permet à SQLite de distinguer plus facilement les éléments statiques de l’instruction et ses données variables. Cette stratégie permet à SQLite d’échapper ou d’assainir le contenu des variables qui font partie de l’instruction, empêchant ainsi une faille de sécurité courante appelée injection SQL. Dans cette attaque, les utilisateurs malveillants insèrent des instructions SQL dans les données variables dans l’espoir que les instructions seront exécutées par inadvertance ; la désinfection déjoue l’attaque en désactivant les caractères dangereux dans les données.
Requêtes
Comme le montre l’exemple de code, notre intention est d’utiliser la même route pour insérer de nouveaux messages dans la base de données et pour générer la liste des messages précédemment envoyés. La méthode db.all()
renvoie la collection de toutes les entrées de la table qui correspondent aux critères définis dans la requête.
Contrairement aux instructions effectuées par db.run()
, db.all()
génère une liste d’enregistrements qui sont traités par la fonction flèche désignée dans le dernier paramètre :
(err, rows) => {}
Cette fonction, à son tour, prend deux paramètres : err
et rows
. Le paramètre err
sera utilisé si une erreur se produit et empêche l’exécution de la requête. En cas de succès, tous les enregistrements sont disponibles dans le tableau rows
, où chaque élément est un objet correspondant à un seul enregistrement de la table. Les propriétés de cet objet correspondent aux noms des champs indiqués dans la requête : uuid
et message
.
Le tableau rows
est une structure de données JavaScript. En tant que tel, il peut être utilisé pour générer des réponses avec des méthodes fournies par Express, telles que res.json()
et res.render()
. Lorsqu’elle est rendue à l’intérieur d’un modèle EJS, une boucle conventionnelle peut lister tous les enregistrements :
<ul>
<% rows.forEach( (row) => { %>
<li><strong><%= row.id %></strong>: <%= row.message %></li>
<% }) %>
</ul>
Au lieu de remplir le tableau rows
avec tous les enregistrements retournés par la requête, dans certains cas, il peut être plus pratique de traiter chaque enregistrement individuellement avec la méthode db.each()
. La syntaxe de la méthode db.each()
est similaire à celle de la méthode db.all()
, mais le paramètre row
dans (err, row) ⇒ {}
correspond à un seul enregistrement à la fois.
Modification du contenu de la base de données
Jusqu’à présent, notre client ne peut qu’ajouter et interroger des messages sur le serveur. Puisque le client connaît maintenant le id
des messages précédemment envoyés, nous pouvons implémenter une fonction pour modifier un enregistrement spécifique. Le message modifié peut également être envoyé à une route de méthode HTTP POST, mais cette fois avec un paramètre de route pour attraper le id
donné par le client dans le chemin de la requête :
app.post('/:id', (req, res) => {
let uuid = req.cookies.uuid
if ( uuid === undefined ){
uuid = uuidv4()
// 401 Unauthorized
res.sendStatus(401)
}
else {
// Update the stored message
// using named parameters
let param = {
$message: req.body.message,
$id: req.params.id,
$uuid: uuid
}
db.run('UPDATE messages SET message = $message WHERE id = $id AND uuid = $uuid', param, function(err){
if ( this.changes > 0 )
{
// A 204 (No Content) status code means the action has
// been enacted and no further information is to be supplied.
res.sendStatus(204)
}
else
res.sendStatus(404)
})
}
})
Ce parcours montre comment utiliser les clauses UPDATE
et WHERE
pour modifier un enregistrement existant. Une différence importante avec les exemples précédents est l’utilisation de paramètres nommés, où les valeurs sont regroupées dans un seul objet (param
) et passées à la méthode db.run()
au lieu de spécifier chaque valeur par elle-même. Dans ce cas, les noms des champs (précédés de $
) sont les propriétés de l’objet. Les paramètres nommés permettent d’utiliser les noms de champs (précédés de $
) comme caractères de remplissage au lieu de points d’interrogation.
Une instruction comme celle de l’exemple ne provoquera aucune modification de la base de données si la condition imposée par la clause WHERE
ne correspond pas à un enregistrement de la table. Pour évaluer si des enregistrements ont été modifiés par l’instruction, une fonction de rappel peut être utilisée comme dernier paramètre de la méthode db.run()
. Dans la fonction, le nombre d’enregistrements modifiés peut être demandé à partir de this.changes
. Notez que les fonctions flèches ne peuvent pas être utilisées dans ce cas, car seules les fonctions régulières de la forme function(){}
définissent l’objet this
.
La suppression d’un enregistrement est très similaire à sa modification. Nous pouvons, par exemple, continuer à utiliser le paramètre de route :id
pour identifier le message à supprimer, mais cette fois dans une route invoquée par la méthode HTTP DELETE du client :
app.delete('/:id', (req, res) => {
let uuid = req.cookies.uuid
if ( uuid === undefined ){
uuid = uuidv4()
res.sendStatus(401)
}
else {
// Named parameters
let param = {
$id: req.params.id,
$uuid: uuid
}
db.run('DELETE FROM messages WHERE id = $id AND uuid = $uuid', param, function(err){
if ( this.changes > 0 )
res.sendStatus(204)
else
res.sendStatus(404)
})
}
})
Les enregistrements sont supprimés d’une table avec la clause DELETE FROM
. Nous avons à nouveau utilisé la fonction de rappel pour évaluer combien d’entrées ont été supprimées de la table.
Fermeture de la base de données
Une fois défini, l’objet db
peut être référencé à tout moment pendant l’exécution du script, car le fichier de la base de données reste ouvert pendant toute la session en cours. Il n’est pas courant de fermer la base de données pendant l’exécution du script.
Une fonction de fermeture de la base de données est toutefois utile pour éviter de fermer brusquement la base de données lorsque le processus du serveur se termine. Bien que peu probable, l’arrêt brutal de la base de données peut entraîner des incohérences si les données en mémoire ne sont pas encore enregistrées dans le fichier. Par exemple, un arrêt brutal de la base de données avec perte de données peut se produire si le script est terminé par l’utilisateur en appuyant sur le raccourci clavier Ctrl+C.
Dans le scénario Ctrl+C que nous venons de décrire, la méthode process.on()
peut intercepter les signaux envoyés par le système d’exploitation et exécuter un arrêt ordonné de la base de données et du serveur :
process.on('SIGINT', () => {
db.close()
server.close()
console.log('HTTP server closed')
})
Le raccourci Ctrl+C invoque le signal SIGINT
du système d’exploitation, qui met fin à un programme de premier plan dans le terminal. Avant de terminer le processus à la réception du signal SIGINT
, le système invoque la fonction de rappel (le dernier paramètre de la méthode process.on()
). A l’intérieur de la fonction callback, vous pouvez mettre n’importe quel code de nettoyage, en particulier la méthode db.close()
pour fermer la base de données et server.close()
, qui ferme gracieusement l’instance Express elle-même.
Exercices guidés
-
Quel est le rôle d’une clé primaire dans une table de base de données SQL ?
-
Quelle est la différence entre les requêtes utilisant
db.all()
etdb.each()
? -
Pourquoi est-il important d’utiliser des caractères de remplacement et de ne pas inclure les données envoyées par le client directement dans une instruction ou une requête SQL ?
Exercices d’exploration
-
Quelle méthode du module
sqlite3
peut être utilisée pour renvoyer une seule entrée de table, même si la requête correspond à plusieurs entrées ? -
Supposons que le tableau
rows
soit passé comme paramètre à une fonction de callback et qu’il contienne le résultat d’une requête effectuée avecdb.all()
. Comment un champ appeléprice
, qui est présent en première position derows
, peut-il être référencé dans la fonction de rappel ? -
La méthode
db.run()
exécute les instructions de modification de la base de données, telles queINSERT INTO
. Après avoir inséré un nouvel enregistrement dans une table, comment pouvez-vous récupérer la clé primaire de l’enregistrement nouvellement inséré ?
Résumé
Cette leçon couvre l’utilisation de base des bases de données SQL dans les applications Node.js Express. Le module sqlite3
offre un moyen simple de stocker des données persistantes dans une base de données SQLite, où un seul fichier contient toute la base de données et ne nécessite pas un serveur de base de données spécialisé. Cette leçon aborde les concepts et procédures suivants :
-
Comment établir une connexion à une base de données depuis Node.js.
-
Comment créer une table simple et le rôle des clés primaires.
-
Utilisation de l’instruction SQL
INSERT INTO
pour ajouter de nouvelles données depuis le script. -
Les requêtes SQL utilisant les méthodes SQLite standard et les fonctions de rappel.
-
Modifier des données dans la base de données en utilisant les instructions SQL
UPDATE
etDELETE
.
Réponses aux exercices guidés
-
Quel est le rôle d’une clé primaire dans une table de base de données SQL ?
La clé primaire est le champ d’identification unique de chaque enregistrement dans une table de base de données.
-
Quelle est la différence entre les requêtes utilisant
db.all()
etdb.each()
?La méthode
db.all()
invoque la fonction de rappel avec un tableau unique contenant toutes les entrées correspondant à la requête. La méthodedb.each()
invoque la fonction de rappel pour chaque ligne de résultat. -
Pourquoi est-il important d’utiliser des caractères de remplacement et de ne pas inclure les données envoyées par le client directement dans une instruction ou une requête SQL ?
Avec les caractères de remplacement, les données soumises par l’utilisateur sont protégées avant d’être incluses dans la requête ou l’instruction. Cela permet d’éviter les attaques par injection SQL, qui consistent à placer des instructions SQL dans des données variables afin d’effectuer des opérations arbitraires sur la base de données.
Réponses aux exercices d’exploration
-
Quelle méthode du module
sqlite3
peut être utilisée pour renvoyer une seule entrée de table, même si la requête correspond à plusieurs entrées ?La méthode
db.get()
a la même syntaxe quedb.all()
, mais ne retourne que la première entrée correspondant à la requête. -
Supposons que le tableau
rows
soit passé comme paramètre à une fonction de callback et qu’il contienne le résultat d’une requête effectuée avecdb.all()
. Comment un champ appeléprice
, qui est présent en première position derows
, peut-il être référencé dans la fonction de rappel ?Chaque élément de
rows
est un objet dont les propriétés correspondent aux noms des champs de la base de données. Ainsi, la valeur du champprice
dans le premier résultat est dansrows[0].price
. -
La méthode
db.run()
exécute les instructions de modification de la base de données, telles queINSERT INTO
. Après avoir inséré un nouvel enregistrement dans une table, comment pouvez-vous récupérer la clé primaire de l’enregistrement nouvellement inséré ?Une fonction régulière de la forme
function(){}
peut être utilisée comme fonction de rappel de la méthodedb.run()
. À l’intérieur de cette fonction, la propriététhis.lastID
contient la valeur de la clé primaire du dernier enregistrement inséré.