Explorateur de bases de données

Avec Nette Database, vous pouvez travailler de deux manières : soit laisser les requêtes SQL être générées automatiquement (approche Explorer), soit les écrire vous-même(accès direct). L'explorateur simplifie considérablement l'accès aux données. Il gère les relations entre les tables afin que vous puissiez vous concentrer sur la logique de votre application.

  • Travailler avec des données est naturel et facile à comprendre
  • Génère des requêtes SQL optimisées qui ne récupèrent que les données nécessaires.
  • Fournit un accès facile aux données connexes sans qu'il soit nécessaire d'écrire des requêtes JOIN
  • Fonctionne immédiatement sans configuration ni génération d'entités

L'utilisation de l'explorateur commence par l'appel de la méthode table() sur l'objet Nette\Database\Explorer (pour plus d'informations sur la création de connexions et la configuration, voir la page dédiée) :

$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 enchaînées à cet objet pour filtrer et trier les résultats. La requête est assemblée et exécutée uniquement lorsque les données sont demandées, par exemple en itérant avec foreach. Chaque ligne est représentée par un objet ActiveRow:

foreach ($books as $book) {
	echo $book->title;        // produit la colonne "title" (titre)
	echo $book->author_id;    // sort la colonne "author_id".
}

Explorer simplifie grandement l'utilisation des relations entre les tables. L'exemple suivant montre comment nous pouvons facilement produire des données à partir de tables liées (les livres et leurs auteurs). Notez qu'il n'est pas nécessaire d'écrire des requêtes JOIN ; Nette les génère pour nous :

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

foreach ($books as $book) {
	echo 'Book: ' . $book->title;
	echo 'Author: ' . $book->author->name; // crée une jointure avec la table "auteur".
}

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

En outre, Explorer repère les colonnes utilisées dans le code et ne récupère que celles-ci dans la base de données, ce qui permet d'améliorer encore les performances. Ce comportement est entièrement automatique et adaptatif. Si vous modifiez ultérieurement le code pour utiliser des colonnes supplémentaires, Explorer ajuste automatiquement les requêtes. Vous n'avez pas besoin de configurer quoi que ce soit ou de réfléchir aux colonnes qui seront nécessaires – laissez cela à Nette.

Filtrage et tri

La classe Selection propose des méthodes pour filtrer et trier les données.

La classe where($condition, ...$params) Ajoute une condition WHERE. Les conditions multiples sont combinées à l'aide de AND.
whereOr(array $conditions) Ajoute un groupe de conditions WHERE combinées à l'aide de OR.
wherePrimary($value) Ajoute une condition WHERE basée sur la clé primaire.
order($columns, ...$params) Définit le tri à l'aide de ORDER BY
select($columns, ...$params) Spécifie les colonnes à extraire
limit($limit, $offset = null) Limite le nombre de lignes (LIMIT) et définit optionnellement 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 le filtrage des lignes groupées.

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

Ces méthodes permettent également d'utiliser des notations spéciales pour accéder aux données de tables connexes.

Échappatoires et identificateurs

Les méthodes échappent automatiquement les paramètres et les identificateurs de citation (noms de tables et de colonnes), ce qui permet d'éviter les injections SQL. Pour garantir un fonctionnement correct, quelques règles doivent être respectées :

  • Écrire les mots-clés, les noms de fonctions, les procédures, etc. en majuscules.
  • Écrire les noms de colonnes et de tables en minuscules.
  • Toujours passer des chaînes de caractères en utilisant des paramètres.
where('name = ' . $name);         // **DISASTER**: vulnérable à l'injection SQL
where('name LIKE "%search%"');    // **WRONG**: complique les citations automatiques
where('name LIKE ?', '%search%'); // **CORRECT**: valeur passée en paramètre

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

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

Filtre les résultats à l'aide de conditions WHERE. Sa force réside dans la gestion intelligente de différents types de valeurs et dans la sélection automatique d'opérateurs SQL.

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 adéquats, vous n'avez pas besoin de gérer les cas particuliers, 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)
// L'espace réservé ? peut être utilisé sans opérateur:
$table->where('id ?', 1);        // WHERE `id` = 1

La méthode traite également 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 supportée.

Vous pouvez également passer le résultat d'une autre requête de table en tant que paramètre, créant ainsi 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 également être transmises sous la forme d'un tableau dont les éléments sont combinés à l'aide de l'opérateur AND :

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

Dans le tableau, des paires clé-valeur peuvent être utilisées, et Nette choisira automatiquement les opérateurs corrects :

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

Nous pouvons également mélanger des expressions SQL avec des espaces réservés et des paramètres multiples. Ceci est utile pour les conditions complexes avec des opérateurs définis avec précision :

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

Plusieurs appels à where() combinent automatiquement les conditions à l'aide de AND.

whereOr (array $parameters)static

Similaire à where(), mais combine les conditions à l'aide de OR :

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

Des expressions plus complexes peuvent également être utilisées :

// 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 exemple, foo_id, bar_id), nous la transmettons 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

Spécifie l'ordre dans lequel les lignes sont renvoyées. Vous pouvez trier par une ou plusieurs colonnes, par ordre croissant ou décroissant, ou par une expression personnalisée :

$table->order('created');                   // ORDER BY `créé`
$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 à renvoyer de 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 devez récupérer des expressions spécifiques :

// SELECT *, DATE_FORMAT(`créé_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 en tant que propriétés de l'objet ActiveRow:

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

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

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

$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

Simplifie la pagination des résultats. Il accepte le numéro de page (à partir de 1) et le nombre d'éléments par page. En option, vous pouvez passer une référence à une variable dans laquelle le nombre total de pages sera stocké :

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

group (string $columns, …$parameters)static

Regroupe les lignes en fonction des colonnes spécifiées (GROUP BY). Il est généralement utilisé en combinaison 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 le filtrage des lignes groupées (HAVING). Elle peut être utilisée en combinaison avec la méthode group() et les fonctions d'agrégation :

// Recherche les catégories contenant 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, plusieurs méthodes utiles sont disponibles :

foreach ($table as $key => $row) Interroge toutes les lignes, $key est la valeur de la clé primaire, $row est un objet ActiveRow.
$row = $table->get($key) Retourne une seule ligne par clé primaire.
$row = $table->fetch() Retourne la ligne courante et avance le pointeur à la ligne suivante.
$array = $table->fetchPairs() Crée un tableau associatif à partir des résultats.
$array = $table->fetchAll() Retourne toutes les lignes sous forme de tableau

count($table) | Retourne le nombre de lignes dans l'objet Sélection | | Retourne le nombre de lignes dans l'objet Sélection

L'objet ActiveRow est en lecture seule. Cela signifie que vous ne pouvez pas modifier les valeurs de ses propriétés. Cette restriction garantit la cohérence des données et évite les effets secondaires inattendus. Les données sont extraites de la base de données et toute modification doit être effectuée de manière explicite et contrôlée.

foreach – Itération sur toutes les lignes

La manière la plus simple d'exécuter une requête et de récupérer des lignes est d'itérer avec la boucle foreach. Elle exécute automatiquement la requête SQL.

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

get ($key): ?ActiveRow

Exécute une requête SQL et renvoie une ligne par sa clé primaire ou null si elle n'existe pas.

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

fetch(): ?ActiveRow

Renvoie une ligne et fait passer le pointeur interne à la ligne suivante. S'il n'y a plus de lignes, il 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 la forme d'un tableau associatif. Le premier argument spécifie le nom de la colonne à utiliser comme clé du tableau, et le second argument spécifie 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 ligne entière sera utilisée comme valeur, représentée sous la forme d'un objet ActiveRow:

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

Si null est fourni comme clé, le tableau sera indexé numériquement à partir de zéro :

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

fetchPairs (Closure $callback)array

Vous pouvez également passer un callback en paramètre. Le callback sera appliqué à chaque ligne pour renvoyer soit une valeur unique, soit une paire clé-valeur.

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

// Le callback peut également renvoyer un tableau contenant une paire clé-valeur :
$titles = $explorer->table('book')
	->fetchPairs(fn($row) => [$row->title, $row->author->name]);
// ['Premier livre' => 'Jan Novak', ...]

fetchAll(): array

Renvoie toutes les lignes sous la forme d'un tableau associatif d'objets ActiveRow, dont 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ètres renvoie le nombre de lignes de l'objet Selection:

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

Remarque : count() avec un paramètre exécute la fonction d'agrégation COUNT dans la base de données, comme décrit ci-dessous.

ActiveRow::toArray(): array

Convertit l'objet ActiveRow en un tableau associatif dont les clés sont les noms de 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) Retourne la somme des valeurs d'une colonne
aggregation($function) Permet toute fonction d'agrégation, telle que AVG() ou GROUP_CONCAT()

count (string $expr): int

Exécute une requête SQL avec la fonction COUNT et renvoie le résultat. Cette méthode est utilisée pour déterminer le nombre de lignes correspondant à une certaine condition :

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

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

min (string $expr) and max(string $expr)

Les méthodes min() et max() renvoient les valeurs minimales et maximales de la colonne ou de l'expression spécifiée :

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

sum (string $expr): int

Renvoie la somme des valeurs de la colonne ou de l'expression spécifié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 l'exécution de n'importe quelle fonction d'agrégation.

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

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

Si nous devons agréger des résultats qui résultent eux-mêmes d'une agrégation et d'un regroupement (par exemple, SUM(value) sur des lignes regroupées), nous spécifions la fonction d'agrégation à appliquer à ces résultats intermédiaires en tant que deuxième argument :

// Calcule le prix total des produits en stock pour chaque catégorie, puis fait la somme de 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 nous regroupons les résultats par category_id. Nous utilisons ensuite aggregation('SUM(category_total)', 'SUM') pour additionner ces sous-totaux. Le deuxième argument 'SUM' spécifie la fonction d'agrégation à appliquer aux résultats intermédiaires.

Insérer, mettre à jour et supprimer

Nette Database Explorer simplifie l'insertion, la mise à jour et la suppression de données. Toutes les méthodes mentionnées envoient un message Nette\Database\DriverException en cas d'erreur.

Selection::insert (iterable $data)static

Insère de nouveaux enregistrements dans une table.

Insertion d'un seul enregistrement:

Le nouvel enregistrement est transmis sous la forme d'un tableau associatif ou d'un objet itérable (tel que ArrayHash utilisé dans les formulaires), dont les clés correspondent aux noms des colonnes de la table.

Si la table possède une clé primaire définie, la méthode renvoie un objet ActiveRow, qui est rechargé à partir de la base de données afin de refléter toute modification apportée au niveau de la base de données (par exemple, déclencheurs, valeurs de colonne par défaut ou calculs d'incrémentation automatique). Cela garantit la cohérence des données et l'objet contient toujours les données actuelles de la base de données. Si une clé primaire n'est pas explicitement définie, la méthode renvoie les données d'entrée sous la forme d'un tableau.

$row = $explorer->table('users')->insert([
	'name' => 'John Doe',
	'email' => 'john.doe@example.com',
]);
// $row est une instance d'ActiveRow contenant les données complètes de la ligne insérée,
// y compris l'identifiant généré automatiquement et toute modification apportée par les déclencheurs.
echo $row->id;          // Affiche l'identifiant de l'utilisateur nouvellement inséré
echo $row->created_at;  // Affiche l'heure de création si elle a été définie par un déclencheur.

Insertion de plusieurs enregistrements à la fois:

La méthode insert() vous permet d'insérer plusieurs enregistrements à l'aide d'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 sera égal à 2

Vous pouvez également passer en paramètre un objet Selection contenant 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:

Les valeurs peuvent être des fichiers, des objets DateTime ou des lettres SQL :

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

Selection::update (iterable $data)int

Met à jour les lignes d'une table en fonction d'un filtre spécifié. Renvoie le nombre de lignes effectivement modifiées.

Les colonnes à mettre à jour sont transmises sous la forme d'un tableau associatif ou d'un objet itérable (tel que ArrayHash utilisé dans les formulaires), dont les clés correspondent aux noms des colonnes du tableau :

$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 la valeur de la colonne "points" de 1
		'coins-=' => 1,   // diminue la valeur de la colonne "pièces" de 1
	]);
// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10

Selection::delete(): int

Supprime des lignes d'un tableau en fonction d'un filtre spécifié. Renvoie le nombre de lignes supprimées.

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

Lorsque vous appelez update() ou delete(), veillez à utiliser where() pour spécifier les lignes à mettre à jour ou à supprimer. Si where() n'est pas utilisé, l'opération sera effectuée sur l'ensemble du tableau !

ActiveRow::update (iterable $data)bool

Met à jour les données d'une ligne de la base de données représentée par l'objet ActiveRow. Il accepte en paramètre des données itérables, dont les clés sont des noms de colonnes. Pour modifier des valeurs numériques, vous pouvez utiliser les opérateurs += et -=:

Une fois la mise à jour effectuée, l'objet ActiveRow est automatiquement rechargé à partir de la base de données afin de refléter toutes les modifications apportées au niveau de la base de données (par exemple, les déclencheurs). La méthode renvoie true uniquement si une modification réelle 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 ligne spécifique de la base de données. Pour les mises à jour en masse de plusieurs lignes, utilisez la méthode Selection::update().

ActiveRow::delete()

Supprime une 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 ne supprime qu'une ligne spécifique dans la base de données. Pour la suppression en bloc 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 par des clés étrangères. Nette Database Explorer offre un moyen révolutionnaire de travailler avec ces relations – sans écrire de requêtes JOIN ni nécessiter de configuration ou de génération d'entités.

Pour la démonstration, nous utiliserons la base de données exemple(disponible sur GitHub). La base de données comprend les tables suivantes :

  • author – auteurs et traducteurs (colonnes id, name, web, born)
  • book – livres (colonnes id, author_id, translator_id, title, sequel_id)
  • tag – tags (colonnes id, name)
  • book_tag – tableau de liens entre les livres et les tags (colonnes book_id, tag_id)

Structure de la base de données

Dans cet exemple de base de données de livres, nous trouvons plusieurs types de relations (simplifiées 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.
  • Many-to-many (M:N) – Un livre peut avoir plusieurs étiquettes, et une étiquette peut être attribuée à plusieurs livres.

Dans ces relations, il y a toujours une table parent et une table enfant. Par exemple, dans la relation entre les auteurs et les livres, la table author est le parent et la table book est l'enfant – vous pouvez considérer qu'un livre “appartient” toujours à un auteur. Cela se reflète également dans la structure de la base de données : la table enfant book contient la clé étrangère author_id, qui fait référence à la table parent author.

Si nous voulons afficher les livres avec le nom de leurs auteurs, nous avons deux possibilités. Soit nous récupérons les données à l'aide d'une seule requête SQL avec un JOIN :

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

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

SELECT * FROM book;
SELECT * FROM author WHERE id IN (1, 2, 3); -- IDs of authors retrieved from books

La deuxième approche est, étonnamment, plus efficace. Les données ne sont récupérées qu'une seule fois et peuvent être mieux utilisées dans le cache. C'est exactement comme cela que fonctionne Nette Database Explorer – il s'occupe de tout sous le capot et vous fournit une API propre :

$books = $explorer->table('book');
foreach ($books as $book) {
	echo 'title: ' . $book->title;
	echo 'written by: ' . $book->author->name; // $book->author est un enregistrement de la table 'author'.
	echo 'translated by: ' . $book->translator?->name;
}

Accès à la table des parents

L'accès à la table parent est simple. Il s'agit de relations telles que un livre a un auteur ou un livre peut avoir un traducteur. L'enregistrement lié est accessible via la propriété d'objet ActiveRow – le nom de la propriété correspond au nom de la colonne de la clé étrangère sans le suffixe id:

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

Lorsqu'il accède à la propriété $book->author, Explorer recherche une colonne de la table book qui contient la chaîne author (c'est-à-dire author_id). En fonction de la valeur de cette colonne, il extrait l'enregistrement correspondant de la table author et le renvoie sous la forme d'un objet ActiveRow. De même, $book->translator utilise la colonne translator_id. Comme la colonne translator_id peut contenir null, l'opérateur ?-> est utilisé.

Une autre approche est fournie par la méthode ref(), qui accepte deux arguments – le nom de la table cible et la colonne de liaison – et renvoie une instance ActiveRow ou null:

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

La méthode ref() est utile si l'accès basé sur les propriétés ne peut pas être utilisé, par exemple lorsque la table contient une colonne portant le même nom que la propriété (author). Dans les autres cas, il est recommandé d'utiliser l'accès par propriété pour une meilleure lisibilité.

Explorer optimise automatiquement les requêtes de base de données. Lorsqu'il parcourt les livres et accède à leurs enregistrements connexes (auteurs, traducteurs), Explorer ne génère pas de requête pour chaque livre individuellement. Au lieu de cela, il n'exécute qu'une requête SELECT pour chaque type de relation, ce qui réduit considérablement la charge de la base de données. En voici un exemple :

$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 optimisées de la base de données :

SELECT * FROM `book`;
SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- IDs from 'author_id' column in selected books
SELECT * FROM `author` WHERE (`id` IN (2, 3));    -- IDs from 'translator_id' column in selected books

La logique d'identification de la colonne de liaison est définie par l'implémentation des conventions. Nous recommandons d'utiliser DiscoveredConventions, qui analyse les clés étrangères et vous permet de travailler en toute transparence avec les relations de table existantes.

Accès à la table enfant

L'accès à la table des enfants se fait dans le sens inverse. 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 un objet Selection contenant les enregistrements correspondants. Voici un exemple :

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

// Sort tous les livres écrits par l'auteur
foreach ($author->related('book.author_id') as $book) {
	echo "Wrote: $book->title";
}

// Sort tous les livres traduits par l'auteur
foreach ($author->related('book.translator_id') as $book) {
	echo "Translated: $book->title";
}

La méthode related() accepte la description de la relation en tant qu'argument unique utilisant la notation par points ou en tant que deux arguments distincts :

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

Explorer peut détecter automatiquement la colonne de liaison correcte en se basant sur le nom de la table parente. Dans ce cas, il établit un lien via la colonne book.author_id car le nom de la table source est author:

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

S'il existe plusieurs connexions possibles, Explorer lèvera une exception AmbiguousReferenceKeyException.

Bien entendu, nous pouvons également utiliser la méthode related() lorsque nous parcourons plusieurs enregistrements dans une boucle, et Explorer optimisera automatiquement les requêtes dans ce cas également :

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

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

SELECT * FROM `author`;
SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- IDs of the selected authors

Relation de plusieurs à plusieurs

Pour une relation de plusieurs à plusieurs (M:N), une table de jonction (dans notre cas, book_tag) est nécessaire. Cette table contient deux colonnes de clés étrangères (book_id, tag_id). Chaque colonne fait référence à la clé primaire de l'une des tables connectées. Pour extraire des données connexes, nous récupérons 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 étiquettes attribuées au livre
foreach ($book->related('book_tag') as $bookTag) {
	echo $bookTag->tag->name; // recherche le nom de l'étiquette dans la table des liens
}

$tag = $explorer->table('tag')->get(1);
// Direction opposée: affiche les titres des livres avec cette balise
foreach ($tag->related('book_tag') as $bookTag) {
	echo $bookTag->book->title; // recherche le titre du livre
}

Explorer optimise à nouveau les requêtes SQL pour les rendre plus efficaces :

SELECT * FROM `book`;
SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...));  -- IDs of the selected books
SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...));                 -- IDs of the tags found in book_tag

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

La notation point (parent_table.column) est utilisée pour les relations 1:N vues du point de vue de la table mère :

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

// Recherche les livres dont le nom de l'auteur commence par "Jon".
$books->where('author.name LIKE ?', 'Jon%');

// Trie les livres par nom d'auteur en ordre 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 points est utilisée pour les relations 1:N du point de vue de la table parente :

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

// Recherche les auteurs qui ont écrit un livre dont le titre contient "PHP".
$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 deux points (:book.title), la colonne de la clé étrangère n'est pas explicitement spécifiée. Explorer détecte automatiquement la colonne correcte en se basant sur le nom de la table parente. Dans ce cas, il effectue la jointure via la colonne book.author_id car le nom de la table source est author. S'il existe plusieurs connexions possibles, Explorer lève l'exception AmbiguousReferenceKeyException.

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

// Recherche les auteurs qui ont traduit un livre dont le titre contient "PHP".
$authors->where(':book(translator).title LIKE ?', '%PHP%');

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

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

Extension des conditions pour JOIN

La méthode joinWhere() ajoute des conditions supplémentaires aux jointures de tables en SQL après le mot-clé ON.

Par exemple, supposons que nous voulions trouver des livres traduits par un traducteur spécifique :

// 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 joinWhere(), vous pouvez utiliser les mêmes constructions que dans la méthode where() – opérateurs, caractères génériques, tableaux de valeurs ou expressions SQL.

Pour les requêtes plus complexes avec plusieurs JOIN, des alias de table peuvent être définis :

$tags = $explorer->table('tag')
	->joinWhere(':book_tag.book.author', 'book_author.born &lt; ?', 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 la méthode where() ajoute des conditions à la clause WHERE, tandis que la méthode joinWhere() étend les conditions de la clause ON lors des jointures de tables.

Création manuelle de l'explorateur

Si vous n'utilisez pas le conteneur Nette DI, vous pouvez créer manuellement une instance de Nette\Database\Explorer:

use Nette\Database;

// $storage implémente Nette\Caching\Storage, par exemple:
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir');
// connexion à la base de données
$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password');
// gère la réflexion sur la structure de la base de données
$structure = new Database\Structure($connection, $storage);
// définit les règles de mappage des noms de tables, des colonnes et des clés étrangères
$conventions = new Database\Conventions\DiscoveredConventions($structure);
$explorer = new Database\Explorer($connection, $structure, $conventions, $storage);
version: 4.0