Database Explorer

Explorer offre une façon intuitive et efficace de travailler avec votre base de données. Il gère automatiquement les relations entre les tables et optimise les requêtes, ce qui vous permet de vous concentrer sur la logique de votre application. Il fonctionne immédiatement, sans configuration. Si vous avez besoin du contrôle total sur les requêtes SQL, vous pouvez utiliser l'approche SQL.

  • Le travail avec les données est naturel et facile à comprendre
  • Il génère des requêtes SQL optimisées qui ne récupèrent que les données nécessaires
  • Il donne un accès simple aux données liées sans avoir à écrire de requêtes JOIN
  • Il fonctionne immédiatement, sans configuration ni génération d'entités

Le travail avec Explorer commence par l'appel de la méthode table() sur l'objet Nette\Database\Explorer (voir Connexion et configuration pour les détails de la mise en place de la connexion) :

$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. D'autres méthodes peuvent être chaînées sur cet objet pour filtrer et trier les résultats. La requête n'est assemblée et exécutée qu'au moment où les données sont demandées, par exemple lors d'un parcours en 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 simplifie énormément le travail avec les relations entre les tables. L'exemple suivant montre avec quelle facilité nous pouvons afficher des données de tables liées (les livres et leurs auteurs). Remarquez qu'aucune requête JOIN n'a besoin d'être écrite ; Nette les génère pour nous :

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

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

Nette Database Explorer optimise les requêtes pour une efficacité maximale. L'exemple ci-dessus n'exécute que deux requêtes SELECT, que nous traitions 10 ou 10 000 livres.

De plus, Explorer suit les colonnes utilisées dans le code et ne récupère que celles-là depuis la base, ce qui améliore encore les performances. Ce comportement est totalement automatique et adaptatif. Si vous modifiez plus tard le code pour utiliser d'autres colonnes, Explorer ajuste automatiquement les requêtes. Vous n'avez rien à configurer ni à réfléchir aux colonnes qui seront nécessaires : laissez cela à Nette.

Filtrer et trier

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

where($condition, ...$params) Ajoute une condition WHERE. Plusieurs conditions sont combinées par AND
whereOr(array $conditions) Ajoute un groupe de conditions WHERE combinées par OR
wherePrimary($value) Ajoute une condition WHERE sur la clé primaire
order($columns, ...$params) Définit le tri avec ORDER BY
select($columns, ...$params) Indique quelles colonnes récupérer
limit($limit, $offset = null) Limite le nombre de lignes (LIMIT) et fixe éventuellement l'OFFSET
page($page, $itemsPerPage, &$numOfPages = null) Met en place la pagination
group($columns, ...$params) Groupe les lignes (GROUP BY)
having($condition, ...$params) Ajoute une condition HAVING pour filtrer les lignes groupées

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

Dans ces méthodes, vous pouvez aussi utiliser des notations particulières pour accéder aux données des tables liées.

Échappement et identifiants

Les méthodes échappent automatiquement les paramètres et mettent les identifiants (noms de tables et de colonnes) entre quotes, ce qui empêche l'injection SQL. Pour que cela fonctionne correctement, quelques règles doivent être respectées :

  • Écrivez les mots-clés, les noms de fonctions, de procédures, etc. en majuscules.
  • Écrivez les noms de colonnes et de tables en minuscules.
  • Passez toujours les chaînes par des paramètres.
where('name = ' . $name);         // FAILLE CRITIQUE : injection SQL
where('name LIKE "%search%"');    // MAUVAIS : complique la mise entre quotes automatique
where('name LIKE ?', '%search%'); // CORRECT : la valeur est passée en paramètre

where('name like ?', $name);     // MAUVAIS : génère : `name` `like` ?
where('name LIKE ?', $name);     // CORRECT : génère : `name` LIKE ?
where('LOWER(name) = ?', $value);// CORRECT : LOWER(`name`) = ?

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

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

Usage 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 de l'opérateur approprié, vous n'avez pas à traiter les différents cas particuliers : Nette les résout 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 le placeholder ? sans opérateur :
$table->where('id ?', 1);        // WHERE `id` = 1

La méthode traite correctement 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 prise en charge

Vous pouvez aussi passer comme paramètre le résultat d'une autre requête sur une table, ce qui crée une sous-requête :

// 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'));

Les conditions peuvent aussi être passées sous forme de tableau, dont les éléments sont combinés par 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 là encore automatiquement les bons opérateurs :

// 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. Cela convient aux conditions complexes avec des opérateurs précisément définis :

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

Des appels répétés à where() combinent automatiquement les conditions par AND.

whereOr (array $parameters)static

Comme where(), cette méthode ajoute des conditions, mais les combine par OR :

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

Des expressions plus complexes peuvent aussi être utilisées ici :

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

wherePrimary (mixed $key)static

Ajoute une condition sur 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 exemple foo_id, bar_id), passez-la sous forme de tableau :

// 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

Indique l'ordre dans lequel les lignes sont renvoyées. Vous pouvez trier par une ou plusieurs colonnes, en ordre croissant ou dé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

Indique les colonnes à renvoyer depuis la base de données. Par défaut, Nette Database Explorer ne renvoie que les colonnes réellement utilisées dans le code. Utilisez la méthode select() lorsque vous avez besoin de récupérer des expressions précises :

// 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 d'AS sont ensuite 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 renvoyées (LIMIT) et permet éventuellement de fixer un décalage :

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

Pour la pagination, il est plus approprié d'utiliser la méthode page().

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

Facilite la pagination des résultats. Elle accepte le numéro de page (à partir de 1) et le nombre d'éléments par page. Vous pouvez éventuellement passer une référence vers une variable où sera stocké le nombre total de pages :

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

group (string $columns, …$parameters)static

Groupe les lignes selon les colonnes indiquées (GROUP BY). Elle s'utilise généralement 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 de filtrage des lignes groupées (HAVING). Elle s'utilise avec la méthode group() et des 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);

Lire les données

Pour lire les données de la base, plusieurs méthodes utiles sont disponibles :

foreach ($table as $key => $row) Parcourt toutes les lignes, $key est la valeur de la clé primaire, $row un objet ActiveRow
$row = $table->get($key) Renvoie une seule ligne d'après la clé primaire
$row = $table->fetch() Renvoie la ligne courante et déplace le pointeur sur la suivante
$array = $table->fetchPairs() Crée un tableau associatif à partir des résultats
$array = $table->fetchAll() Renvoie toutes les lignes sous forme de tableau
count($table) Renvoie le nombre de lignes de l'objet Selection

L'objet ActiveRow est en lecture seule. Cela signifie que vous ne pouvez pas changer les valeurs de ses propriétés. Cette restriction garantit la cohérence des données et évite les effets de bord inattendus. Les données sont chargées depuis la base, et toute modification doit être faite explicitement et de façon maîtrisée.

foreach – parcourir toutes les lignes

La façon la plus simple d'exécuter une requête et de récupérer les lignes est de les parcourir dans une boucle foreach. Elle exécute automatiquement la requête SQL.

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

get ($key): ?ActiveRow

Exécute la requête SQL et renvoie la ligne d'après la clé primaire, ou null si elle n'existe pas.

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

fetch(): ?ActiveRow

Renvoie la ligne courante et déplace le pointeur interne sur la suivante. S'il n'y a plus de lignes, renvoie null.

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

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

Renvoie les résultats sous forme de tableau associatif. Le premier argument indique le nom de la colonne à utiliser comme clé du tableau, le second le nom de la colonne à utiliser comme valeur :

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

Si seul le premier paramètre est fourni, la valeur sera la ligne entière, c'est-à-dire l'objet ActiveRow :

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

En cas de clés en double, c'est la valeur de la dernière ligne qui est utilisée. En utilisant null comme clé, le tableau sera indexé numériquement à partir de zéro (aucune collision ne se produit alors) :

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

fetchPairs (Closure $callback)array

Vous pouvez aussi passer en paramètre un callback, qui renverra pour chaque ligne soit une seule valeur, soit une paire clé-valeur.

$titles = $explorer->table('book')
	->fetchPairs(fn($row) => "$row->title ({$row->author->name})");
// ['First Book (John Novak)', ...]

// Le callback peut aussi renvoyer un tableau formant une paire clé & valeur :
$titles = $explorer->table('book')
	->fetchPairs(fn($row) => [$row->title, $row->author->name]);
// ['First Book' => 'John Novak', ...]

fetchAll(): array

Renvoie toutes les lignes sous forme de tableau associatif d'objets ActiveRow, où les clés sont les valeurs de la clé primaire.

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

count(): int

La méthode count() sans paramètre renvoie le nombre de lignes de l'objet Selection :

$table->where('category', 1);
$count = $table->count();
$count = count($table); // variante

Remarque : count() avec un paramètre exécute la fonction d'agrégation COUNT dans la base de données, voir plus bas.

ActiveRow::toArray(): array

Convertit l'objet ActiveRow en tableau associatif, où les clés sont les noms des colonnes et les valeurs 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 permettant d'exécuter facilement des fonctions d'agrégation (COUNT, SUM, MIN, MAX, AVG, etc.).

count($expr) Compte le nombre de lignes
min($expr) Renvoie la valeur minimale d'une colonne
max($expr) Renvoie la valeur maximale d'une colonne
sum($expr) Renvoie la somme des valeurs d'une colonne
aggregation($function) Permet n'importe quelle fonction d'agrégation, comme AVG() ou GROUP_CONCAT()

count (string $expr): int

Exécute une requête SQL avec la fonction COUNT et renvoie le résultat. La méthode sert à déterminer combien de lignes correspondent à une condition donnée :

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

Remarque : count() sans paramètre ne renvoie que le nombre de lignes de l'objet Selection.

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

Les méthodes min() et max() renvoient les valeurs minimale et maximale de la colonne ou de l'expression indiquée :

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

sum (string $expr): mixed

Renvoie la somme des valeurs de la colonne ou de l'expression indiquée :

// 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)mixed

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

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

// réunit les tags des produits en une seule chaîne
$tags = $products->where('id', 1)
	->aggregation('GROUP_CONCAT(tag.name) AS tags')
	->fetch()
	->tags;

Si nous avons besoin d'agréger des résultats qui proviennent déjà d'une fonction d'agrégation et d'un groupement (par exemple SUM(value) sur des lignes groupées), nous indiquons en deuxième argument 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 de chaque catégorie (SUM(price * stock) AS category_total) et groupons les résultats par category_id. Nous utilisons ensuite aggregation('SUM(category_total)', 'SUM') pour additionner ces totaux intermédiaires category_total. Le deuxième argument 'SUM' indique que la fonction SUM doit être appliquée aux résultats intermédiaires.

Insert, Update & Delete

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

Selection::insert (iterable $data)

Insère de nouveaux enregistrements dans la table.

Insertion d'un seul enregistrement :

Passez le nouvel enregistrement sous forme de tableau associatif ou d'objet itérable (comme l'ArrayHash utilisé dans 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 renvoie un objet ActiveRow, rechargé depuis la base pour refléter les éventuelles modifications faites au niveau de la base (triggers, valeurs par défaut des colonnes, calcul des colonnes auto-incrémentées). Cela garantit la cohérence des données, et l'objet contient toujours les données actuelles de la base. Si la table n'a pas de clé primaire, aucune ligne n'est identifiable et la méthode renvoie null.

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

Insertion de plusieurs enregistrements à la fois :

La méthode insert() permet d'insérer plusieurs enregistrements par une seule requête SQL. Dans ce cas, elle renvoie 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 vaudra 2

Un objet Selection contenant une sélection de données peut aussi être passé en paramètre.

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

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

Insertion de valeurs particulières :

Nous pouvons aussi passer comme valeurs des fichiers, des objets DateTime ou des littéraux SQL :

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

Selection::update (iterable $data)int

Met à jour les lignes de la table selon le filtre indiqué. Renvoie le nombre de lignes réellement modifiées.

Passez les colonnes à modifier sous forme de tableau associatif ou d'objet itérable (comme l'ArrayHash utilisé dans les formulaires), où les clés correspondent aux noms des colonnes de la table :

$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 modifier des valeurs numériques, vous pouvez utiliser les opérateurs += et -= :

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

Selection::delete(): int

Supprime les lignes de la table selon le filtre indiqué. Renvoie le nombre de lignes supprimées.

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

Lors de l'appel d'update() ou de delete(), n'oubliez pas d'utiliser where() pour indiquer les lignes à modifier ou à supprimer. Si where() n'est pas utilisée, l'opération portera sur toute la table !

ActiveRow::update (iterable $data)bool

Met à jour les données de la ligne représentée par l'objet ActiveRow. Elle accepte un itérable contenant les données à mettre à jour (les clés sont les noms des colonnes). Pour modifier des valeurs numériques, vous pouvez utiliser les opérateurs += et -= :

Après la mise à jour, l'ActiveRow est automatiquement rechargé depuis la base pour refléter les éventuelles modifications faites au niveau de la base (par exemple par des triggers). La méthode ne renvoie true que si un changement réel des données a eu lieu.

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

Cette méthode ne met à jour qu'une seule ligne précise de la base. Pour la mise à jour en masse de plusieurs lignes, utilisez la méthode Selection::update().

ActiveRow::delete(): int

Supprime de la base la ligne représentée par l'objet ActiveRow. Renvoie le nombre de lignes supprimées, qui devrait être 1.

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

Cette méthode ne supprime qu'une seule ligne précise de la base. Pour la 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 entre plusieurs tables et reliées entre elles par des clés étrangères. Nette Database Explorer offre une façon révolutionnaire de travailler avec ces relations : sans écrire de requêtes JOIN et sans avoir besoin de configurer ni de générer quoi que ce soit.

Pour illustrer le travail avec les relations, nous utiliserons une base de données de livres en exemple (à retrouver sur GitHub). Dans cette base, nous avons les tables :

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

Dans notre base de livres, nous trouvons plusieurs types de relations (même si le modèle est 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 une suite.
  • Plusieurs-à-plusieurs (M:N) – Un livre peut avoir plusieurs tags, et un tag peut être attribué à plusieurs livres.

Dans ces relations, il y a toujours une table parente et une table enfant. Par exemple, dans la relation entre les auteurs et les livres, la table author est la parente et la table book l'enfant : on peut se le représenter en disant qu'un livre “appartient” toujours à un auteur. Cela se reflète aussi dans la structure de la base : la table enfant book contient la clé étrangère author_id, qui référence la table parente author.

Si nous avons besoin de lister les livres avec le nom de leurs auteurs, nous avons deux possibilités. Soit récupérer les données par une seule requête SQL avec un JOIN :

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

Soit récupérer les données en deux étapes – d'abord les livres, puis leurs auteurs – et les assembler ensuite en PHP :

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

La deuxième approche est en réalité plus efficace, même si cela peut surprendre. Les données ne sont récupérées qu'une fois et peuvent être mieux exploitées dans le cache. C'est exactement ainsi que fonctionne Nette Database Explorer : il s'occupe de tout sous le capot 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 un enregistrement de la table 'author'
	echo 'traduit par : ' . $book->translator?->name;
}

Accéder à la table parente

Accéder à la table parente est simple. Il s'agit de relations comme un livre a un auteur ou un livre peut avoir un traducteur. L'enregistrement lié s'obtient par une propriété de l'objet ActiveRow, dont le nom correspond au nom de la colonne de clé étrangère sans le suffixe _id :

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

Lors de l'accès à la propriété $book->author, Explorer cherche dans la table book une colonne dont le nom contient la chaîne author (c'est-à-dire author_id). D'après la valeur de cette colonne, il charge l'enregistrement correspondant de la table author et le renvoie comme ActiveRow. De la même façon, $book->translator utilise la colonne translator_id. Comme la colonne translator_id peut contenir null, nous utilisons dans le code l'opérateur nullsafe ?->.

Une approche alternative est offerte par la méthode ref(), qui accepte deux arguments – le nom de la table cible et le nom de la colonne de jointure – et renvoie une instance d'ActiveRow ou null :

echo $book->ref('author', 'author_id')->name;      // relation vers l'auteur
echo $book->ref('author', 'translator_id')->name;  // relation vers le traducteur

La méthode ref() est utile lorsque l'accès par propriété ne peut pas être employé, par exemple parce que la table contient une colonne du même nom (c'est-à-dire author). Dans les autres cas, l'accès par propriété est recommandé pour une meilleure lisibilité.

Explorer optimise automatiquement les requêtes à la base. Lorsque nous parcourons les livres dans une boucle et accédons à leurs enregistrements liés (auteurs, traducteurs), Explorer ne génère pas une requête pour chaque livre séparément. Il n'exécute qu'une seule requête SELECT par type de relation, ce qui réduit nettement la charge de la base. Par exemple :

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

Ce code n'exécute que ces trois requêtes ultra-rapides vers la base :

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

La logique de recherche de la colonne de jointure est déterminée par l'implémentation des Conventions. Nous recommandons d'utiliser DiscoveredConventions, qui analyse les clés étrangères et vous permet de travailler facilement avec les relations existantes entre les tables.

Accéder à la table enfant

L'accès à la table enfant fonctionne dans l'autre sens. Nous demandons maintenant quels livres cet auteur a-t-il écrits ou quels livres ce traducteur a-t-il traduits. Pour ce type de requête, nous utilisons la méthode related(), qui renvoie une Selection contenant les enregistrements liés. Prenons 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 traduits par l'auteur
foreach ($author->related('book.translator_id') as $book) {
	echo "A traduit : $book->title";
}

La méthode related() accepte la description de la jointure soit en un seul argument avec la notation par point, soit en deux arguments distincts :

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

Explorer sait détecter automatiquement la bonne colonne de jointure d'après le nom de la table parente. Dans ce cas, il joint via la colonne book.author_id, car le nom de la table source est author :

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

Si plusieurs liens possibles existent, Explorer lèvera une AmbiguousReferenceKeyException.

Nous pouvons bien sûr utiliser la méthode related() en parcourant plusieurs enregistrements dans une boucle, et Explorer optimisera là aussi automatiquement les requêtes :

$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 ultra-rapides :

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

Relation plusieurs-à-plusieurs

Pour une relation plusieurs-à-plusieurs (M:N), une table de jonction est nécessaire (dans notre cas book_tag), contenant deux colonnes de clés étrangères (book_id, tag_id). Chacune de ces colonnes renvoie à la clé primaire de l'une des tables liées. Pour récupérer les données liées, nous obtenons d'abord les enregistrements de la table de jonction à l'aide de related('book_tag'), puis nous passons aux données cibles :

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

$tag = $explorer->table('tag')->get(1);
// ou dans l'autre sens : affiche les titres des livres portant ce tag
foreach ($tag->related('book_tag') as $bookTag) {
	echo $bookTag->book->title; // affiche le titre du livre
}

Explorer optimise de nouveau les requêtes SQL sous une forme efficace :

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

Requêter à travers les tables liées

Dans les méthodes where(), select(), order() et group(), vous pouvez utiliser des notations particulières pour accéder aux colonnes d'autres tables. Explorer crée automatiquement les JOINs nécessaires.

La notation par point (table_parente.colonne) s'utilise pour les relations 1:N du point de vue de la table enfant :

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

// Trouve les livres dont le nom de l'auteur commence 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');

La notation par deux-points (:table_enfant.colonne) s'utilise pour les relations 1:N du point de vue de la table parente :

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

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

// Compte le nombre de livres de 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 de clé étrangère n'est pas précisée. Explorer détecte automatiquement la bonne colonne d'après le nom de la table parente. Dans ce cas, il joint via la colonne book.author_id, car le nom de la table source est author. Si plusieurs liens possibles existent, Explorer lèvera une AmbiguousReferenceKeyException.

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

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

Les notations peuvent être chaînées pour accéder aux données à travers plusieurs tables :

// Trouve les auteurs de livres portant le tag 'PHP'
$authors->where(':book:book_tag.tag.name', 'PHP')
	->group('author.id');

Étendre les conditions du JOIN

La méthode joinWhere() étend les conditions indiquées lors de la jointure des tables en SQL, après le mot-clé ON.

Disons que nous voulons trouver les livres traduits par un traducteur précis :

// Trouve les livres traduits par un 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 de joinWhere(), vous pouvez utiliser les mêmes constructions que dans la méthode where() : opérateurs, placeholders, tableaux de valeurs ou expressions SQL.

Pour les requêtes plus complexes comportant plusieurs JOINs, vous pouvez 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 de la clause ON lors de la jointure des tables.