Accès SQL
Nette Database offre deux voies : vous pouvez écrire vous-même des requêtes SQL (accès SQL), ou les laisser générer automatiquement (voir Explorer). L'accès SQL vous donne un contrôle total sur les requêtes tout en assurant leur construction sécurisée.
Les détails sur la connexion et la configuration de la base de données se trouvent dans le chapitre Connexion et configuration.
Requêtes de base
Pour interroger la base de données, utilisez la méthode query()
. Elle renvoie un objet ResultSet qui représente le résultat de la
requête. En cas d'échec, la méthode lève une exception. Nous
pouvons parcourir le résultat de la requête à l'aide d'une boucle foreach
, ou utiliser l'une des fonctions d'aide.
$result = $database->query('SELECT * FROM users');
foreach ($result as $row) {
echo $row->id;
echo $row->name;
}
Pour insérer des valeurs en toute sécurité dans les requêtes SQL, nous utilisons des requêtes paramétrées. Nette Database les rend extrêmement simples – il suffit d'ajouter une virgule et la valeur après la requête SQL :
$database->query('SELECT * FROM users WHERE name = ?', $name);
Avec plusieurs paramètres, vous avez deux options d'écriture. Vous pouvez soit “intercaler” les paramètres dans la requête SQL :
$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age);
Soit écrire d'abord la requête SQL complète, puis joindre tous les paramètres :
$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age);
Protection contre l'injection SQL
Pourquoi est-il important d'utiliser des requêtes paramétrées ? Parce qu'elles vous protègent contre une attaque appelée injection SQL, où un attaquant pourrait injecter ses propres commandes SQL et ainsi obtenir ou endommager des données dans la base de données.
N'insérez jamais de variables directement dans la requête SQL ! Utilisez toujours des requêtes paramétrées, qui vous protègent contre l'injection SQL.
// ❌ CODE DANGEREUX - vulnérable à l'injection SQL
$database->query("SELECT * FROM users WHERE name = '$name'");
// ✅ Requête paramétrée sécurisée
$database->query('SELECT * FROM users WHERE name = ?', $name);
Familiarisez-vous avec les risques de sécurité possibles.
Techniques de requête
Conditions WHERE
Les conditions WHERE peuvent être écrites sous forme de tableau associatif, où les clés sont les noms des colonnes et les valeurs sont les données à comparer. Nette Database choisit automatiquement l'opérateur SQL le plus approprié en fonction du type de valeur.
$database->query('SELECT * FROM users WHERE', [
'name' => 'John',
'active' => true,
]);
// WHERE `name` = 'John' AND `active` = 1
Vous pouvez également spécifier explicitement l'opérateur de comparaison dans la clé :
$database->query('SELECT * FROM users WHERE', [
'age >' => 25, // utilise l'opérateur >
'name LIKE' => '%John%', // utilise l'opérateur LIKE
'email NOT LIKE' => '%example.com%', // utilise l'opérateur NOT LIKE
]);
// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%'
Nette gère automatiquement les cas spéciaux comme les valeurs null
ou les tableaux.
$database->query('SELECT * FROM products WHERE', [
'name' => 'Laptop', // utilise l'opérateur =
'category_id' => [1, 2, 3], // utilise IN
'description' => null, // utilise IS NULL
]);
// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL
Pour les conditions négatives, utilisez l'opérateur NOT
:
$database->query('SELECT * FROM products WHERE', [
'name NOT' => 'Laptop', // utilise l'opérateur <>
'category_id NOT' => [1, 2, 3], // utilise NOT IN
'description NOT' => null, // utilise IS NOT NULL
'id' => [], // sera omis
]);
// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL
Pour joindre des conditions, l'opérateur AND
est utilisé. Cela peut être modifié à l'aide du placeholder ?or.
Règles ORDER BY
Le tri ORDER BY
peut être écrit à l'aide d'un tableau. Dans les clés, nous indiquons les colonnes et la valeur
sera un booléen indiquant s'il faut trier par ordre croissant :
$database->query('SELECT id FROM author ORDER BY', [
'id' => true, // croissant
'name' => false, // décroissant
]);
// SELECT id FROM author ORDER BY `id`, `name` DESC
Insertion de données (INSERT)
Pour insérer des enregistrements, la commande SQL INSERT
est utilisée.
$values = [
'name' => 'John Doe',
'email' => 'john@example.com',
];
$database->query('INSERT INTO users ?', $values);
$userId = $database->getInsertId();
La méthode getInsertId()
renvoie l'ID de la dernière ligne insérée. Pour certaines bases de données (par
exemple PostgreSQL), il est nécessaire de spécifier en paramètre le nom de la séquence à partir de laquelle l'ID doit être
généré à l'aide de $database->getInsertId($sequenceId)
.
Nous pouvons également passer des valeurs-speciales comme paramètres, telles que des fichiers, des objets DateTime ou des types enum.
Insertion de plusieurs enregistrements à la fois :
$database->query('INSERT INTO users ?', [
['name' => 'User 1', 'email' => 'user1@mail.com'],
['name' => 'User 2', 'email' => 'user2@mail.com'],
]);
L'INSERT multiple est beaucoup plus rapide car une seule requête de base de données est exécutée, au lieu de plusieurs requêtes individuelles.
Avertissement de sécurité : N'utilisez jamais de données non validées comme $values
. Familiarisez-vous
avec les risques possibles.
Mise à jour des données (UPDATE)
Pour mettre à jour des enregistrements, la commande SQL UPDATE
est utilisée.
// Mise à jour d'un seul enregistrement
$values = [
'name' => 'John Smith',
];
$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1);
Le nombre de lignes affectées est renvoyé par $result->getRowCount()
.
Pour UPDATE, nous pouvons utiliser les opérateurs +=
et -=
:
$database->query('UPDATE users SET ? WHERE id = ?', [
'login_count+=' => 1, // incrémentation de login_count
], 1);
Exemple d'insertion ou de modification d'un enregistrement s'il existe déjà. Nous utilisons la technique
ON DUPLICATE KEY UPDATE
:
$values = [
'name' => $name,
'year' => $year,
];
$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?',
$values + ['id' => $id],
$values,
);
// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978)
// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978
Notez que Nette Database reconnaît le contexte dans lequel le paramètre avec le tableau est inséré dans la commande SQL et
construit le code SQL en conséquence. Ainsi, à partir du premier tableau, il a construit
(id, name, year) VALUES (123, 'Jim', 1978)
, tandis que le second a été converti sous la forme
name = 'Jim', year = 1978
. Nous en discutons plus en détail dans la section Conseils pour la construction SQL.
Suppression de données (DELETE)
Pour supprimer des enregistrements, la commande SQL DELETE
est utilisée. Exemple d'obtention du nombre de lignes
supprimées :
$count = $database->query('DELETE FROM users WHERE id = ?', 1)
->getRowCount();
Conseils pour la construction SQL
Un hint est un placeholder spécial dans la requête SQL qui indique comment la valeur du paramètre doit être réécrite en expression SQL :
Hint | Description | S'applique automatiquement |
---|---|---|
?name |
utilisé pour insérer le nom de la table ou de la colonne | – |
?values |
génère (key, ...) VALUES (value, ...) |
INSERT ... ? , REPLACE ... ? |
?set |
génère l'affectation key = value, ... |
SET ? , KEY UPDATE ? |
?and |
joint les conditions du tableau avec l'opérateur AND |
WHERE ? , HAVING ? |
?or |
joint les conditions du tableau avec l'opérateur OR |
– |
?order |
génère la clause ORDER BY |
ORDER BY ? , GROUP BY ? |
Pour insérer dynamiquement des noms de tables et de colonnes dans la requête, le placeholder ?name
est utilisé.
Nette Database s'occupe du traitement correct des identifiants selon les conventions de la base de données donnée (par exemple,
en les entourant de backticks en MySQL).
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table);
// SELECT `name` FROM `users` WHERE id = 1 (en MySQL)
Avertissement : utilisez le symbole ?name
uniquement pour les noms de tables et de colonnes provenant
d'entrées validées, sinon vous vous exposez à un risque de sécurité.
Les autres hints n'ont généralement pas besoin d'être spécifiés, car Nette utilise une autodétection intelligente lors de
la composition de la requête SQL (voir la troisième colonne du tableau). Mais vous pouvez l'utiliser, par exemple, dans une
situation où vous souhaitez joindre des conditions à l'aide de OR
au lieu de AND
:
$database->query('SELECT * FROM users WHERE ?or', [
'name' => 'John',
'email' => 'john@example.com',
]);
// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com'
Valeurs spéciales {#valeurs-speciales}
En plus des types scalaires courants (string, int, bool), vous pouvez également passer des valeurs spéciales comme paramètres :
- fichiers :
fopen('image.gif', 'r')
insère le contenu binaire du fichier - date et heure : les objets
DateTime
sont convertis au format de la base de données - types enum : les instances
enum
sont converties en leur valeur - littéraux SQL : créés à l'aide de
Connection::literal('NOW()')
sont insérés directement dans la requête
$database->query('INSERT INTO articles ?', [
'title' => 'My Article',
'published_at' => new DateTime,
'content' => fopen('image.png', 'r'),
'state' => Status::Draft,
]);
Pour les bases de données qui n'ont pas de support natif pour le type de données datetime
(comme SQLite et
Oracle), DateTime
est converti en une valeur spécifiée dans la configuration de la base de données par l'élément
formatDateTime
(la valeur par défaut est U
– timestamp unix).
Littéraux SQL
Dans certains cas, vous devez spécifier directement du code SQL comme valeur, mais celui-ci ne doit pas être interprété
comme une chaîne et échappé. C'est à cela que servent les objets de la classe Nette\Database\SqlLiteral
. Ils sont
créés par la méthode Connection::literal()
.
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
'year >' => $database::literal('YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR())
Ou alternativement :
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
$database::literal('year > YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR())
Les littéraux SQL peuvent contenir des paramètres :
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
$database::literal('year > ? AND year < ?', $min, $max),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017)
Grâce à cela, nous pouvons créer des combinaisons intéressantes :
$result = $database->query('SELECT * FROM users WHERE', [
'name' => $name,
$database::literal('?or', [
'active' => true,
'role' => $role,
]),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin')
Obtention des données {#Získání dat}
Raccourcis pour les requêtes SELECT
Pour simplifier la récupération des données, Connection
propose plusieurs raccourcis qui combinent l'appel de
query()
avec le fetch*()
suivant. Ces méthodes acceptent les mêmes paramètres que
query()
, c'est-à-dire la requête SQL et les paramètres optionnels. Une description complète des méthodes
fetch*()
se trouve ci-dessous.
fetch($sql, ...$params): ?Row |
Exécute la requête et renvoie la première ligne comme objet Row |
fetchAll($sql, ...$params): array |
Exécute la requête et renvoie toutes les lignes comme tableau d'objets Row |
fetchPairs($sql, ...$params): array |
Exécute la requête et renvoie un tableau associatif, où la première colonne représente la clé et la seconde la valeur |
fetchField($sql, ...$params): mixed |
Exécute la requête et renvoie la valeur du premier champ de la première ligne |
fetchList($sql, ...$params): ?array |
Exécute la requête et renvoie la première ligne comme tableau indexé |
Exemple :
// fetchField() - renvoie la valeur de la première cellule
$count = $database->query('SELECT COUNT(*) FROM articles')
->fetchField();
foreach
– itération sur les lignes
Après l'exécution de la requête, un objet ResultSet est renvoyé, permettant de parcourir les
résultats de plusieurs manières. La manière la plus simple d'exécuter une requête et d'obtenir les lignes est d'itérer dans
une boucle foreach
. Cette méthode est la plus économe en mémoire car elle renvoie les données progressivement et
ne les stocke pas toutes en mémoire en même temps.
$result = $database->query('SELECT * FROM users');
foreach ($result as $row) {
echo $row->id;
echo $row->name;
// ...
}
ResultSet
ne peut être itéré qu'une seule fois. Si vous avez besoin d'itérer plusieurs fois, vous
devez d'abord charger les données dans un tableau, par exemple à l'aide de la méthode fetchAll()
.
fetch(): ?Row .{toc: fetch()}wiki-method {#fetch()}
Renvoie une ligne comme objet Row
. S'il n'y a plus de lignes, renvoie null
. Déplace le pointeur
interne à la ligne suivante.
$result = $database->query('SELECT * FROM users');
$row = $result->fetch(); // lit la première ligne
if ($row) {
echo $row->name;
}
fetchAll(): array
Renvoie toutes les lignes restantes du ResultSet
sous forme de tableau d'objets Row
.
$result = $database->query('SELECT * FROM users');
$rows = $result->fetchAll(); // lit toutes les lignes
foreach ($rows as $row) {
echo $row->name;
}
fetchPairs (string|int|null $key = null, string|int|null $value = null): array
Renvoie les résultats sous forme de tableau associatif. Le premier argument spécifie le nom de la colonne à utiliser comme clé dans le tableau, le second argument spécifie le nom de la colonne à utiliser comme valeur :
$result = $database->query('SELECT id, name FROM users');
$names = $result->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]
Si nous ne spécifions que le premier paramètre, la valeur sera la ligne entière, c'est-à-dire l'objet
Row
:
$rows = $result->fetchPairs('id');
// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...]
En cas de clés dupliquées, la valeur de la dernière ligne est utilisée. En utilisant null
comme clé, le
tableau sera indexé numériquement à partir de zéro (il n'y a alors pas de collisions) :
$names = $result->fetchPairs(null, 'name');
// [0 => 'John Doe', 1 => 'Jane Doe', ...]
fetchPairs (Closure $callback): array
Alternativement, vous pouvez spécifier un callback comme paramètre, qui renverra pour chaque ligne soit la valeur elle-même, soit une paire clé-valeur.
$result = $database->query('SELECT * FROM users');
$items = $result->fetchPairs(fn($row) => "$row->id - $row->name");
// ['1 - John', '2 - Jane', ...]
// Le callback peut également renvoyer un tableau avec une paire clé & valeur :
$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]);
// ['John' => 46, 'Jane' => 21, ...]
fetchField(): mixed
Renvoie la valeur du premier champ de la ligne actuelle. S'il n'y a plus de lignes, renvoie null
. Déplace le
pointeur interne à la ligne suivante.
$result = $database->query('SELECT name FROM users');
$name = $result->fetchField(); // lit le nom de la première ligne
fetchList(): ?array
Renvoie une ligne comme tableau indexé. S'il n'y a plus de lignes, renvoie null
. Déplace le pointeur interne à
la ligne suivante.
$result = $database->query('SELECT name, email FROM users');
$row = $result->fetchList(); // ['John', 'john@example.com']
getRowCount(): ?int
Renvoie le nombre de lignes affectées par la dernière requête UPDATE
ou DELETE
. Pour
SELECT
, c'est le nombre de lignes renvoyées, mais celui-ci peut ne pas être connu – dans ce cas, la méthode
renvoie null
.
getColumnCount(): ?int
Renvoie le nombre de colonnes dans le ResultSet
.
Informations sur les requêtes
À des fins de débogage, nous pouvons obtenir des informations sur la dernière requête exécutée :
echo $database->getLastQueryString(); // affiche la requête SQL
$result = $database->query('SELECT * FROM articles');
echo $result->getQueryString(); // affiche la requête SQL
echo $result->getTime(); // affiche le temps d'exécution en secondes
Pour afficher le résultat sous forme de tableau HTML, on peut utiliser :
$result = $database->query('SELECT * FROM articles');
$result->dump();
ResultSet fournit des informations sur les types de colonnes :
$result = $database->query('SELECT * FROM articles');
$types = $result->getColumnTypes();
foreach ($types as $column => $type) {
echo "$column est de type $type->type"; // par ex. 'id est de type int'
}
Journalisation des requêtes
Nous pouvons implémenter notre propre journalisation des requêtes. L'événement onQuery
est un tableau de
callbacks qui sont appelés après chaque requête exécutée :
$database->onQuery[] = function ($database, $result) use ($logger) {
$logger->info('Query: ' . $result->getQueryString());
$logger->info('Time: ' . $result->getTime());
if ($result->getRowCount() > 1000) {
$logger->warning('Large result set: ' . $result->getRowCount() . ' rows');
}
};