Database Explorer

Explorer offre un moyen intuitif et efficace de travailler avec votre base de données. Il gère automatiquement les relations entre les tables et l'optimisation des requêtes, vous permettant de vous concentrer sur votre application. Il fonctionne immédiatement sans aucune configuration. Si vous avez besoin d'un contrôle total sur vos requêtes SQL, vous pouvez utiliser l'approche SQL.

  • Le travail avec les données est naturel et facile à comprendre
  • Génère des requêtes SQL optimisées qui ne chargent que les données nécessaires
  • Permet un accès facile aux données liées sans avoir à écrire de requêtes JOIN
  • Fonctionne immédiatement sans aucune configuration ni génération d'entités

Vous commencez avec Explorer en appelant la méthode table() sur l'objet Nette\Database\Explorer (les détails sur la connexion se trouvent dans le chapitre Connexion et configuration) :

$books = $explorer->table('book'); // 'book' est le nom de la table

La méthode renvoie un objet Selection, qui représente une requête SQL. Vous pouvez enchaîner d'autres méthodes sur cet objet pour filtrer et trier les résultats. La requête est construite et exécutée seulement au moment où vous commencez à demander des données, par exemple, en parcourant une boucle foreach. Chaque ligne est représentée par un objet ActiveRow :

foreach ($books as $book) {
	echo $book->title;        // Affiche la colonne 'title'
	echo $book->author_id;    // Affiche la colonne 'author_id'
}

Explorer facilite considérablement le travail avec les relations entre les tables. L'exemple suivant montre avec quelle facilité vous pouvez afficher les données de tables liées (livres et leurs auteurs). Notez que vous n'avez pas besoin d'écrire de requêtes JOIN ; Nette les crée pour vous :

$books = $explorer->table('book');

foreach ($books as $book) {
	echo 'Livre : ' . $book->title;
	echo 'Auteur : ' . $book->author->name; // Crée un JOIN sur la table 'author'
}

Nette Database Explorer optimise les requêtes pour qu'elles soient aussi efficaces que possible. L'exemple ci-dessus n'exécute que deux requêtes SELECT, que vous traitiez 10 ou 10 000 livres.

De plus, Explorer surveille les colonnes utilisées dans votre code et ne charge que celles-ci depuis la base de données, économisant ainsi des ressources. Ce comportement est entièrement automatique et adaptatif. Si vous modifiez ultérieurement votre code et commencez à utiliser d'autres colonnes, Explorer ajuste automatiquement les requêtes. Vous n'avez rien à configurer ni à vous soucier des colonnes dont vous aurez besoin – laissez Nette s'en charger.

Filtrage et tri

La classe Selection fournit des méthodes pour filtrer et trier la sélection de données.

where($condition, ...$params) Ajoute une condition WHERE. Plusieurs conditions sont liées par l'opérateur AND
whereOr(array $conditions) Ajoute un groupe de conditions WHERE liées par l'opérateur OR
wherePrimary($value) Ajoute une condition WHERE basée sur la clé primaire
order($columns, ...$params) Définit le tri ORDER BY
select($columns, ...$params) Spécifie les colonnes à charger
limit($limit, $offset = null) Limite le nombre de lignes (LIMIT) et définit éventuellement OFFSET
page($page, $itemsPerPage, &$total = null) Définit la pagination
group($columns, ...$params) Regroupe les lignes (GROUP BY)
having($condition, ...$params) Ajoute une condition HAVING pour filtrer les lignes groupées

Les méthodes peuvent être enchaînées (ce qu'on appelle une interface fluide) : $table->where(...)->order(...)->limit(...).

Dans ces méthodes, vous pouvez également utiliser une notation spéciale pour accéder aux données des tables liées.

Échappement et identifiants

Les méthodes échappent automatiquement les paramètres et protègent les identifiants (noms de tables et de colonnes), prévenant ainsi les injections SQL. Pour un fonctionnement correct, il est nécessaire de respecter quelques règles :

  • Écrivez les mots-clés SQL, noms de fonctions, procédures, etc. en MAJUSCULES.
  • Écrivez les noms de colonnes et de tables en minuscules.
  • Insérez toujours les chaînes de caractères via des paramètres.
where('name = ' . $name);         // ⚠️ VULNÉRABILITÉ CRITIQUE : injection SQL
where('name LIKE "%search%"');    // ❌ MAUVAIS : complique la protection automatique des identifiants
where('name LIKE ?', '%search%'); // ✅ CORRECT : valeur insérée via un paramètre

where('name like ?', $name);     // ❌ MAUVAIS : génère : `name` `like` ? (mot-clé en minuscule)
where('name LIKE ?', $name);     // ✅ CORRECT : génère : `name` LIKE ?
where('LOWER(name) = ?', $value);// ✅ CORRECT : génère : LOWER(`name`) = ?

where (string|array $condition, …$parameters)static

Filtre les résultats à l'aide de conditions WHERE. Sa force réside dans sa gestion intelligente des différents types de valeurs et dans le choix automatique des opérateurs SQL appropriés.

Utilisation de base :

$table->where('id', $value);     // WHERE `id` = 123
$table->where('id > ?', $value); // WHERE `id` > 123
$table->where('id = ? OR name = ?', $id, $name); // WHERE `id` = 1 OR `name` = 'Jon Snow'

Grâce à la détection automatique des opérateurs appropriés, vous n'avez pas besoin de gérer différents cas spéciaux. Nette s'en charge pour vous :

$table->where('id', 1);          // WHERE `id` = 1
$table->where('id', null);       // WHERE `id` IS NULL
$table->where('id', [1, 2, 3]);  // WHERE `id` IN (1, 2, 3)
// Vous pouvez aussi utiliser un placeholder (?) sans opérateur :
$table->where('id ?', 1);        // WHERE `id` = 1

La méthode gère correctement également les conditions négatives et les tableaux vides :

$table->where('id', []);         // WHERE `id` IS NULL AND FALSE -- ne trouve rien
$table->where('id NOT', []);     // WHERE `id` IS NULL OR TRUE -- trouve tout
$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- trouve tout
// $table->where('NOT id ?', $ids); Attention - cette syntaxe n'est pas supportée

Comme paramètre, vous pouvez également passer le résultat d'une autre table (Selection) – une sous-requête sera créée :

// WHERE `id` IN (SELECT `id` FROM `tableName`)
$table->where('id', $explorer->table($tableName));

// WHERE `id` IN (SELECT `col` FROM `tableName`)
$table->where('id', $explorer->table($tableName)->select('col'));

Vous pouvez également passer les conditions sous forme de tableau associatif, dont les éléments seront liés par l'opérateur AND :

// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`)
$table->where([
	'price_final < price_original',
	'stock_count > min_stock',
]);

Dans le tableau, vous pouvez utiliser des paires clé => valeur, et Nette choisira à nouveau automatiquement les opérateurs corrects :

// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3))
$table->where([
	'status' => 'active',
	'id' => [1, 2, 3],
]);

Dans le tableau, vous pouvez combiner des expressions SQL avec des placeholders (?) et plusieurs paramètres. C'est utile pour des conditions complexes avec des opérateurs définis précisément :

// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5)
$table->where([
	'age > ?' => 18,
	'ROUND(score, ?) > ?' => [2, 75.5], // Deux paramètres passés comme tableau
]);

Les appels multiples à where() lient automatiquement les conditions avec l'opérateur AND.

whereOr (array $parameters)static

Similaire à where(), cette méthode ajoute des conditions, mais les lie avec l'opérateur OR :

// WHERE (`status` = 'active') OR (`deleted` = 1)
$table->whereOr([
	'status' => 'active',
	'deleted' => true,
]);

Ici aussi, vous pouvez utiliser des expressions plus complexes :

// WHERE (`price` > 1000) OR (`price_with_tax` > 1500)
$table->whereOr([
	'price > ?' => 1000,
	'price_with_tax > ?' => 1500,
]);

wherePrimary (mixed $key)static

Ajoute une condition pour la clé primaire de la table.

// WHERE `id` = 123
$table->wherePrimary(123);

// WHERE `id` IN (1, 2, 3)
$table->wherePrimary([1, 2, 3]);

Si la table a une clé primaire composite (par ex. foo_id, bar_id), passez-la comme un tableau associatif :

// WHERE `foo_id` = 1 AND `bar_id` = 5
$table->wherePrimary(['foo_id' => 1, 'bar_id' => 5])->fetch();

// WHERE (`foo_id`, `bar_id`) IN ((1, 5), (2, 3))
$table->wherePrimary([
	['foo_id' => 1, 'bar_id' => 5],
	['foo_id' => 2, 'bar_id' => 3],
])->fetchAll();

order (string $columns, …$parameters)static

Détermine l'ordre dans lequel les lignes seront retournées. Nous pouvons trier par une ou plusieurs colonnes, par ordre décroissant ou croissant, ou selon une expression personnalisée :

$table->order('created');                   // ORDER BY `created`
$table->order('created DESC');              // ORDER BY `created` DESC
$table->order('priority DESC, created');    // ORDER BY `priority` DESC, `created`
$table->order('status = ? DESC', 'active'); // ORDER BY `status` = 'active' DESC

select (string $columns, …$parameters)static

Spécifie les colonnes à retourner de la base de données. Par défaut, Nette Database Explorer ne retourne que les colonnes réellement utilisées dans le code. Nous utilisons donc la méthode select() dans les cas où nous avons besoin de retourner des expressions spécifiques :

// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date`
$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y');

Les alias définis à l'aide de AS sont alors accessibles comme propriétés de l'objet ActiveRow :

foreach ($table as $row) {
	echo $row->formatted_date;   // accès à l'alias
}

limit (?int $limit, ?int $offset = null)static

Limite le nombre de lignes retournées (LIMIT) et permet éventuellement de définir un offset :

$table->limit(10);        // LIMIT 10 (retourne les 10 premières lignes)
$table->limit(10, 20);    // LIMIT 10 OFFSET 20

Pour la pagination, il est préférable d'utiliser la méthode page().

page (int $page, int $itemsPerPage, &$numOfPages = null)static

Facilite la pagination des résultats. Accepte le numéro de page (compté à partir de 1) et le nombre d'éléments par page. Il est possible de passer en option une référence à une variable dans laquelle sera stocké le nombre total de pages :

$numOfPages = null;
$table->page(page: 3, itemsPerPage: 10, $numOfPages);
echo "Nombre total de pages : $numOfPages";

group (string $columns, …$parameters)static

Regroupe les lignes selon les colonnes spécifiées (GROUP BY). Est généralement utilisé en conjonction avec des fonctions d'agrégation :

// Compte le nombre de produits dans chaque catégorie
$table->select('category_id, COUNT(*) AS count')
	->group('category_id');

having (string $having, …$parameters)static

Définit une condition pour filtrer les lignes groupées (HAVING). Peut être utilisé en conjonction avec la méthode group() et les fonctions d'agrégation :

// Trouve les catégories qui ont plus de 100 produits
$table->select('category_id, COUNT(*) AS count')
	->group('category_id')
	->having('count > ?', 100);

Lecture des données

Pour lire les données de la base de données, vous disposez de plusieurs méthodes utiles :

foreach ($table as $key => $row) Itère sur toutes les lignes. $key est la valeur de la clé primaire, $row est l'objet ActiveRow.
$row = $table->get($key) Retourne une seule ligne identifiée par sa clé primaire.
$row = $table->fetch() Retourne la ligne suivante du résultat.
$array = $table->fetchPairs($key = null, $value = null) Crée un tableau associatif à partir des résultats.
$array = $table->fetchAll() Retourne toutes les lignes sous forme de tableau d'objets ActiveRow.
count($table) Retourne le nombre de lignes dans l'objet Selection (si déjà chargées) ou exécute COUNT(*) si non chargées.

L'objet ActiveRow est conçu pour la lecture seule. Cela signifie que vous ne pouvez pas modifier directement les valeurs de ses propriétés. Cette restriction garantit la cohérence des données et empêche les effets secondaires inattendus. Les données sont chargées depuis la base de données, et toute modification doit être effectuée explicitement (par exemple via la méthode update()).

foreach – itération sur toutes les lignes

La manière la plus simple d'exécuter une requête et d'obtenir les lignes est d'itérer sur l'objet Selection avec une boucle foreach. Cela déclenche automatiquement l'exécution de la requête SQL.

$books = $explorer->table('book');
foreach ($books as $key => $book) {
	// $key contient la valeur de la clé primaire, $book est un objet ActiveRow
	echo "$book->title ({$book->author->name})";
}

get ($key): ?ActiveRow

Exécute la requête SQL pour récupérer une seule ligne par sa clé primaire et la retourne sous forme d'objet ActiveRow, ou null si la ligne n'existe pas.

$book = $explorer->table('book')->get(123);  // Retourne ActiveRow avec l'ID 123 ou null
if ($book) {
	echo $book->title;
}

fetch(): ?ActiveRow

Retourne la ligne suivante du jeu de résultats sous forme d'objet ActiveRow et déplace le pointeur interne sur la suivante. S'il n'y a plus de lignes, retourne null.

$books = $explorer->table('book');
while ($book = $books->fetch()) {
	$this->processBook($book);
}

fetchPairs (string|int|null $key = null, string|int|null $value = null)array

Retourne les résultats sous forme de tableau associatif. Le premier argument $key spécifie le nom de la colonne à utiliser comme clé dans le tableau. Le second argument $value spécifie le nom de la colonne à utiliser comme valeur :

$authors = $explorer->table('author')->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]

Si vous ne spécifiez que le premier paramètre $key, la valeur de chaque élément du tableau sera la ligne entière (objet ActiveRow) :

$authors = $explorer->table('author')->fetchPairs('id');
// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...]

En cas de clés dupliquées, la valeur de la dernière ligne écrasera les précédentes. Si vous utilisez null comme $key, le tableau sera indexé numériquement à partir de zéro (évitant ainsi les collisions) :

$authors = $explorer->table('author')->fetchPairs(null, 'name');
// [0 => 'John Doe', 1 => 'Jane Doe', ...]

fetchPairs (Closure $callback)array

Alternativement, vous pouvez passer un callback comme unique paramètre. Ce callback sera appelé pour chaque ligne et devra retourner soit la valeur à ajouter au tableau, soit une paire [clé, valeur].

$titles = $explorer->table('book')
	->fetchPairs(fn($row) => "$row->title ({$row->author->name})");
// ['Premier livre (Jan Novák)', ...]

// Le callback peut aussi retourner un tableau [clé, valeur] :
$titles = $explorer->table('book')
	->fetchPairs(fn($row) => [$row->title, $row->author->name]);
// ['Premier livre' => 'Jan Novák', ...]

fetchAll(): array

Retourne toutes les lignes du résultat sous forme de tableau d'objets ActiveRow. Les clés du tableau sont les valeurs des clés primaires des lignes.

$allBooks = $explorer->table('book')->fetchAll();
// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...]

count(): int

La méthode count() sans argument retourne le nombre de lignes dans l'objet Selection (si les données ont déjà été chargées) ou exécute une requête SELECT COUNT(*) pour obtenir le nombre total de lignes correspondant aux conditions définies :

$selection = $explorer->table('book')->where('available', true);
$count = $selection->count();
$count = count($selection);   // Alternative, même comportement

Attention, count() avec un argument exécute une fonction d'agrégation COUNT() dans la base de données, voir Agrégation.

ActiveRow::toArray(): array

Convertit l'objet ActiveRow en tableau associatif PHP standard, où les clés sont les noms des colonnes et les valeurs sont les données correspondantes.

$book = $explorer->table('book')->get(1);
$bookArray = $book->toArray();
// $bookArray sera ['id' => 1, 'title' => '...', 'author_id' => ..., ...]

Agrégation

La classe Selection fournit des méthodes pour effectuer facilement des fonctions d'agrégation SQL (COUNT, SUM, MIN, MAX, AVG, etc.).

count($expr) Compte le nombre de lignes correspondant à l'expression.
min($expr) Retourne la valeur minimale de la colonne/expression.
max($expr) Retourne la valeur maximale de la colonne/expression.
sum($expr) Retourne la somme des valeurs de la colonne/expression.
aggregation($function, $groupFunction = null) Permet d'effectuer une fonction d'agrégation SQL arbitraire (ex: AVG(), GROUP_CONCAT()).

count (string $expr): int

Exécute une requête SQL avec la fonction COUNT et retourne le résultat. La méthode est utilisée pour compter le nombre de lignes correspondant à une condition ou une expression :

$count = $table->count('*');                 // SELECT COUNT(*) FROM `table`
$count = $table->count('DISTINCT column');   // SELECT COUNT(DISTINCT `column`) FROM `table`

Attention, count() sans argument retourne le nombre de lignes dans l'objet Selection (si déjà chargé) ou exécute COUNT(*) si les données ne sont pas chargées.

min (string $expr) et max(string $expr)

Les méthodes min() et max() retournent la valeur minimale et maximale dans la colonne ou l'expression spécifiée pour les lignes sélectionnées :

// SELECT MAX(`price`) FROM `products` WHERE `active` = 1
$maxPrice = $products->where('active', true)
	->max('price');

sum (string $expr)

Retourne la somme des valeurs dans la colonne ou l'expression spécifiée pour les lignes sélectionnées :

// SELECT SUM(`price` * `items_in_stock`) FROM `products` WHERE `active` = 1
$totalPrice = $products->where('active', true)
	->sum('price * items_in_stock');

aggregation (string $function, ?string $groupFunction = null)

Permet d'exécuter n'importe quelle fonction d'agrégation SQL.

// prix moyen des produits dans la catégorie
$avgPrice = $products->where('category_id', 1)
	->aggregation('AVG(price)');

// concatène les étiquettes du produit en une seule chaîne
$tags = $products->where('id', 1)
	->aggregation('GROUP_CONCAT(tag.name) AS tags')
	->fetch()
	->tags;

Si vous avez besoin d'agréger des résultats qui proviennent déjà eux-mêmes d'une fonction d'agrégation et d'un regroupement (par ex., calculer la somme de SUM(valeur) sur des lignes groupées), spécifiez comme deuxième argument $groupFunction la fonction d'agrégation à appliquer à ces résultats intermédiaires :

// Calcule le prix total des produits en stock pour chaque catégorie, puis additionne ces prix.
$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total')
	->group('category_id')
	->aggregation('SUM(category_total)', 'SUM');

Dans cet exemple, nous calculons d'abord le prix total des produits dans chaque catégorie (SUM(price * stock) AS category_total) et regroupons les résultats par category_id. Ensuite, nous utilisons aggregation('SUM(category_total)', 'SUM') pour additionner ces sous-totaux category_total. Le deuxième argument 'SUM' indique que la fonction SUM doit être appliquée aux résultats intermédiaires (category_total).

Insertion, Mise à jour & Suppression

Nette Database Explorer simplifie l'insertion, la mise à jour et la suppression de données. Toutes les méthodes mentionnées lèvent une exception Nette\Database\DriverException en cas d'erreur de base de données.

Selection::insert (iterable $data)

Insère un ou plusieurs nouveaux enregistrements dans la table.

Insertion d'un seul enregistrement :

Passez le nouvel enregistrement sous forme de tableau associatif ou d'objet itérable (par exemple, Nette\Utils\ArrayHash utilisé par les formulaires), où les clés correspondent aux noms des colonnes de la table.

Si la table a une clé primaire définie, la méthode retourne un objet ActiveRow représentant la ligne insérée. Cet objet est rechargé depuis la base de données pour refléter les éventuelles modifications effectuées au niveau de la base de données (triggers, valeurs par défaut, auto-incrément). Cela garantit la cohérence des données. Si la table n'a pas de clé primaire unique, la méthode retourne les données transmises sous forme de tableau.

$row = $explorer->table('users')->insert([
	'name' => 'John Doe',
	'email' => 'john.doe@example.com',
]);
// $row est une instance de ActiveRow et contient les données complètes de la ligne insérée,
// y compris l'ID généré automatiquement (si applicable) et les éventuelles modifications par triggers.
echo $row->id; // Affiche l'ID de l'utilisateur nouvellement inséré
echo $row->created_at; // Affiche l'heure de création, si définie par un trigger ou une valeur par défaut

Insertion de plusieurs enregistrements à la fois :

Passez un tableau de tableaux associatifs ou d'objets itérables. La méthode insert() exécute alors une seule requête SQL pour insérer toutes les lignes. Dans ce cas, elle retourne le nombre de lignes insérées.

$insertedRows = $explorer->table('users')->insert([
	[
		'name' => 'John',
		'year' => 1994,
	],
	[
		'name' => 'Jack',
		'year' => 1995,
	],
]);
// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995)
// $insertedRows sera 2

Comme paramètre, on peut également passer un objet Selection avec une sélection de données.

$newUsers = $explorer->table('potential_users')
	->where('approved', 1)
	->select('name, email');

$insertedRows = $explorer->table('users')->insert($newUsers);

Insertion de valeurs spéciales :

Comme valeurs, nous pouvons également passer des fichiers, des objets DateTime ou des littéraux SQL :

$explorer->table('users')->insert([
	'name' => 'John',
	'created_at' => new DateTime,           // Converti au format de base de données
	'avatar' => fopen('image.jpg', 'rb'),   // Insère le contenu binaire du fichier
	'uuid' => $explorer::literal('UUID()'), // Appelle la fonction SQL UUID()
]);

Selection::update (iterable $data)int

Met à jour les lignes de la table correspondant au filtre défini précédemment (par where()). Retourne le nombre de lignes réellement modifiées.

Passez les colonnes à modifier sous forme de tableau associatif ou d'objet itérable (par exemple, ArrayHash des formulaires), où les clés correspondent aux noms des colonnes :

$affected = $explorer->table('users')
	->where('id', 10)
	->update([
		'name' => 'John Smith',
		'year' => 1994,
	]);
// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10

Pour incrémenter ou décrémenter des valeurs numériques, vous pouvez utiliser les opérateurs += et -= dans les clés du tableau :

$explorer->table('users')
	->where('id', 10)
	->update([
		'points+=' => 1,  // augmente la valeur de la colonne 'points' de 1
		'coins-=' => 1,   // diminue la valeur de la colonne 'coins' de 1
	]);
// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10

Selection::delete(): int

Supprime les lignes de la table selon le filtre spécifié. Retourne le nombre de lignes supprimées.

$count = $explorer->table('users')
	->where('id', 10)
	->delete();
// DELETE FROM `users` WHERE `id` = 10

Lors de l'appel de update() et delete(), n'oubliez pas de spécifier les lignes à modifier/supprimer à l'aide de where(). Si vous n'utilisez pas where(), l'opération s'effectuera sur toute la table !

ActiveRow::update (iterable $data)bool

Met à jour les données de la ligne de base de données représentée par l'objet ActiveRow. Accepte comme paramètre un itérable (tableau associatif ou objet) avec les données à mettre à jour (les clés sont les noms des colonnes). Pour incrémenter/décrémenter des valeurs numériques, utilisez les opérateurs += et -= :

Après l'exécution de la mise à jour, les propriétés de l'objet ActiveRow sont automatiquement mises à jour avec les nouvelles valeurs (elles sont rechargées depuis la base de données pour refléter d'éventuels triggers). La méthode retourne true si une modification a réellement eu lieu, false sinon.

$article = $explorer->table('article')->get(1);
$article->update([
	'views += 1',  // augmentons le nombre de vues
]);
echo $article->views; // Affiche le nombre actuel de vues

Cette méthode met à jour uniquement la ligne spécifique représentée par l'objet ActiveRow. Pour une mise à jour en masse de plusieurs lignes, utilisez la méthode Selection::update().

ActiveRow::delete()

Supprime la ligne de la base de données représentée par l'objet ActiveRow.

$book = $explorer->table('book')->get(1);
$book->delete(); // Supprime le livre avec l'ID 1

Cette méthode supprime uniquement la ligne spécifique représentée par l'objet ActiveRow. Pour une suppression en masse de plusieurs lignes, utilisez la méthode Selection::delete().

Relations entre les tables

Dans les bases de données relationnelles, les données sont réparties dans plusieurs tables et reliées entre elles par des clés étrangères. Nette Database Explorer apporte une manière révolutionnaire de travailler avec ces relations – sans écrire de requêtes JOIN et sans avoir besoin de configurer ou de générer quoi que ce soit.

Pour illustrer le travail avec les relations, nous utiliserons l'exemple d'une base de données de livres (vous le trouverez sur GitHub). Dans la base de données, nous avons les tables :

  • author – écrivains et traducteurs (colonnes id, name, web, born)
  • book – livres (colonnes id, author_id, translator_id, title, sequel_id)
  • tag – étiquettes (colonnes id, name)
  • book_tag – table de liaison entre les livres et les étiquettes (colonnes book_id, tag_id)
Structure de la base de données utilisée dans les exemples ***

Dans notre exemple de base de données de livres, nous trouvons plusieurs types de relations (bien que le modèle soit simplifié par rapport à la réalité) :

  • Un-à-plusieurs 1:N – chaque livre a un auteur, un auteur peut écrire plusieurs livres
  • Zéro-à-plusieurs 0:N – un livre peut avoir un traducteur, un traducteur peut traduire plusieurs livres
  • Zéro-à-un 0:1 – un livre peut avoir un tome suivant
  • Plusieurs-à-plusieurs M:N – un livre peut avoir plusieurs étiquettes et une étiquette peut être attribuée à plusieurs livres

Dans ces relations, il existe toujours une table parente et une table enfant. Par exemple, dans la relation entre l'auteur et le livre, la table author est parente et book est enfant – on peut imaginer que le livre “appartient” toujours à un auteur. Cela se reflète également dans la structure de la base de données : la table enfant book contient une clé étrangère author_id, qui référence la table parente author.

Si nous avons besoin d'afficher les livres y compris les noms de leurs auteurs, nous avons deux options. Soit nous obtenons les données avec une seule requête SQL à l'aide de JOIN :

SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id

Soit nous chargeons les données en deux étapes – d'abord les livres, puis leurs auteurs – et ensuite nous les assemblons en PHP :

SELECT * FROM book;
SELECT * FROM author WHERE id IN (1, 2, 3);  -- ids des auteurs des livres obtenus

La deuxième approche est en fait plus efficace, même si cela peut être surprenant. Les données sont chargées une seule fois et peuvent être mieux utilisées dans le cache. C'est précisément de cette manière que fonctionne Nette Database Explorer – tout est géré en arrière-plan et vous offre une API élégante :

$books = $explorer->table('book');
foreach ($books as $book) {
	echo 'titre : ' . $book->title;
	echo 'écrit par : ' . $book->author->name; // $book->author est l'enregistrement de la table 'author'
	echo 'traduit par : ' . $book->translator?->name;
}

Accès à la table parente (Relation N:1)

Accéder à la table parente (la table référencée par une clé étrangère) est très simple. Cela correspond aux relations comme “un livre a un auteur” ou “un livre peut avoir un traducteur”. Vous obtenez l'enregistrement lié via une propriété dynamique de l'objet ActiveRow. Le nom de cette propriété correspond au nom de la colonne contenant la clé étrangère, sans le suffixe _id (par convention) :

$book = $explorer->table('book')->get(1);
echo $book->author->name;      // trouve l'auteur selon la colonne author_id
echo $book->translator?->name; // trouve le traducteur selon translator_id

Lorsque vous accédez à la propriété $book->author, Explorer recherche dans la table book une colonne dont le nom est dérivé de author (généralement author_id). Il utilise la valeur de cette colonne pour charger l'enregistrement correspondant dans la table author et le retourne sous forme d'objet ActiveRow. Le même mécanisme s'applique pour $book->translator via la colonne translator_id. Comme la colonne translator_id peut contenir null, nous utilisons l'opérateur ?-> dans le code.

Une alternative est la méthode ref(), qui prend deux arguments : le nom de la table cible et (optionnellement) le nom de la colonne de liaison. Elle retourne l'instance ActiveRow ou null :

echo $book->ref('author', 'author_id')->name;      // liaison à l'auteur
echo $book->ref('author', 'translator_id')->name;  // liaison au traducteur

La méthode ref() est utile si le nom de la propriété dynamique entre en conflit avec un nom de colonne existant dans la table (author dans cet exemple). Sinon, l'accès via la propriété est généralement plus lisible et recommandé.

Explorer optimise automatiquement les requêtes. Lorsque vous parcourez des livres dans une boucle et accédez à leurs enregistrements liés (auteurs, traducteurs), Explorer ne génère pas une requête distincte pour chaque livre. Au lieu de cela, il effectue une seule requête SELECT par type de relation pour récupérer tous les enregistrements liés nécessaires en une seule fois (technique connue sous le nom de “eager loading” implicite), réduisant considérablement la charge de la base de données :

$books = $explorer->table('book');
foreach ($books as $book) {
	echo $book->title . ': ';
	echo $book->author->name;
	echo $book->translator?->name;
}

Ce code n'exécutera que trois requêtes rapides, quel que soit le nombre de livres :

SELECT * FROM `book`;
SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id de la colonne author_id des livres sélectionnés
SELECT * FROM `author` WHERE (`id` IN (2, 3));    -- id de la colonne translator_id des livres sélectionnés

La logique de détection de la colonne de liaison est gérée par l'implémentation des Conventions. Nous recommandons l'utilisation de DiscoveredConventions, qui analyse les clés étrangères définies dans votre base de données et permet de travailler facilement avec les relations existantes.

Accès à la table enfant

L'accès à la table enfant fonctionne dans le sens inverse. Nous demandons maintenant quels livres cet auteur a-t-il écrits ou traduits. Pour ce type de requête, nous utilisons la méthode related(), qui retourne une Selection avec les enregistrements liés. Regardons un exemple :

$author = $explorer->table('author')->get(1);

// Affiche tous les livres de l'auteur
foreach ($author->related('book.author_id') as $book) {
	echo "A écrit : $book->title";
}

// Affiche tous les livres que l'auteur a traduits
foreach ($author->related('book.translator_id') as $book) {
	echo "A traduit : $book->title";
}

La méthode related() accepte la description de la liaison comme un seul argument avec la notation par points ou comme deux arguments séparés :

$author->related('book.translator_id');  // un argument
$author->related('book', 'translator_id');  // deux arguments

Explorer peut détecter automatiquement la colonne de liaison correcte en fonction du nom de la table parente. Dans ce cas, la liaison se fait via la colonne book.author_id, car le nom de la table source est author :

$author->related('book');  // utilise book.author_id

S'il existait plusieurs liaisons possibles, Explorer lèverait une exception AmbiguousReferenceKeyException.

Nous pouvons bien sûr utiliser la méthode related() également lors du parcours de plusieurs enregistrements dans une boucle, et Explorer optimise également automatiquement les requêtes dans ce cas :

$authors = $explorer->table('author');
foreach ($authors as $author) {
	echo $author->name . ' a écrit :';
	foreach ($author->related('book') as $book) {
		echo $book->title;
	}
}

Ce code ne génère que deux requêtes SQL rapides :

SELECT * FROM `author`;
SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id des auteurs sélectionnés

Relation Plusieurs-à-plusieurs

Pour une relation plusieurs-à-plusieurs (M:N), l'existence d'une table de liaison est nécessaire (dans notre cas book_tag), qui contient deux colonnes avec des clés étrangères (book_id, tag_id). Chacune de ces colonnes référence la clé primaire de l'une des tables liées. Pour obtenir les données liées, nous obtenons d'abord les enregistrements de la table de liaison à l'aide de related('book_tag') et continuons ensuite vers les données cibles :

$book = $explorer->table('book')->get(1);
// affiche les noms des étiquettes attribuées au livre
foreach ($book->related('book_tag') as $bookTag) {
	echo $bookTag->tag->name;  // affiche le nom de l'étiquette via la table de liaison
}

$tag = $explorer->table('tag')->get(1);
// ou inversement : affiche les noms des livres marqués avec cette étiquette
foreach ($tag->related('book_tag') as $bookTag) {
	echo $bookTag->book->title; // affiche le nom du livre
}

Explorer optimise à nouveau les requêtes SQL en une forme efficace :

SELECT * FROM `book`;
SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...));  -- id des livres sélectionnés
SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...));                 -- id des étiquettes trouvées dans book_tag

Interrogation via les tables liées

Dans les méthodes where(), select(), order() et group(), nous pouvons utiliser des notations spéciales pour accéder aux colonnes d'autres tables. Explorer crée automatiquement les JOIN nécessaires.

Notation par points (table_parente.colonne) est utilisée pour la relation 1:N du point de vue de la table enfant :

$books = $explorer->table('book');

// Trouve les livres dont l'auteur a un nom commençant par 'Jon'
$books->where('author.name LIKE ?', 'Jon%');

// Trie les livres par nom d'auteur décroissant
$books->order('author.name DESC');

// Affiche le titre du livre et le nom de l'auteur
$books->select('book.title, author.name');

Notation par deux-points (:table_enfant.colonne) est utilisée pour la relation 1:N du point de vue de la table parente :

$authors = $explorer->table('author');

// Trouve les auteurs qui ont écrit un livre avec 'PHP' dans le titre
$authors->where(':book.title LIKE ?', '%PHP%');

// Compte le nombre de livres pour chaque auteur
$authors->select('*, COUNT(:book.id) AS book_count')
	->group('author.id');

Dans l'exemple ci-dessus avec la notation par deux-points (:book.title), la colonne avec la clé étrangère n'est pas spécifiée. Explorer détecte automatiquement la colonne correcte en fonction du nom de la table parente. Dans ce cas, la liaison se fait via la colonne book.author_id, car le nom de la table source est author. S'il existait plusieurs liaisons possibles, Explorer lèverait une exception AmbiguousReferenceKeyException.

La colonne de liaison peut être explicitement indiquée entre parenthèses :

// Trouve les auteurs qui ont traduit un livre avec 'PHP' dans le titre
$authors->where(':book(translator_id).title LIKE ?', '%PHP%');

Les notations peuvent être enchaînées pour accéder via plusieurs tables :

// Trouve les auteurs de livres marqués avec l'étiquette 'PHP'
$authors->where(':book:book_tag.tag.name', 'PHP')
	->group('author.id');

Extension des conditions pour JOIN

La méthode joinWhere() étend les conditions qui sont spécifiées lors de la liaison des tables en SQL après le mot-clé ON.

Supposons que nous voulions trouver les livres traduits par un traducteur spécifique :

// Trouve les livres traduits par le traducteur nommé 'David'
$books = $explorer->table('book')
	->joinWhere('translator', 'translator.name', 'David');
// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David')

Dans la condition joinWhere(), nous pouvons utiliser les mêmes constructions que dans la méthode where() – opérateurs, points d'interrogation, tableaux de valeurs ou expressions SQL.

Pour des requêtes plus complexes avec plusieurs JOIN, nous pouvons définir des alias de tables :

$tags = $explorer->table('tag')
	->joinWhere(':book_tag.book.author', 'book_author.born < ?', 1950)
	->alias(':book_tag.book.author', 'book_author');
// LEFT JOIN `book_tag` ON `tag`.`id` = `book_tag`.`tag_id`
// LEFT JOIN `book` ON `book_tag`.`book_id` = `book`.`id`
// LEFT JOIN `author` `book_author` ON `book`.`author_id` = `book_author`.`id`
//    AND (`book_author`.`born` < 1950)

Notez que tandis que la méthode where() ajoute des conditions à la clause WHERE, la méthode joinWhere() étend les conditions dans la clause ON lors de la liaison des tables.

version: 4.0