Esploratore di database

Con Nette Database è possibile lavorare in due modi: lasciare che le query SQL vengano generate automaticamente (approccio Explorer) o scriverle personalmente(accesso diretto). L'Explorer semplifica notevolmente l'accesso ai dati. Gestisce le relazioni tra le tabelle e consente di concentrarsi sulla logica dell'applicazione.

  • Lavorare con i dati è naturale e facile da capire
  • Genera query SQL ottimizzate che recuperano solo i dati necessari
  • Fornisce un facile accesso a dati correlati senza la necessità di scrivere query JOIN
  • Funziona immediatamente senza alcuna configurazione o generazione di entità

Il lavoro con l'Explorer inizia richiamando il metodo table() sull'oggetto Nette\Database\Explorer (per informazioni sulla creazione delle connessioni e sulla configurazione, vedere la pagina dedicata):

$books = $explorer->table('book'); // 'book' è il nome della tabella

Il metodo restituisce un oggetto Selection, che rappresenta una query SQL. È possibile concatenare altri metodi a questo oggetto per filtrare e ordinare i risultati. La query viene assemblata ed eseguita solo quando i dati vengono richiesti, ad esempio iterando con foreach. Ogni riga è rappresentata da un oggetto ActiveRow:

foreach ($books as $book) {
	echo $book->title;        // produce la colonna "titolo
	echo $book->author_id;    // produce la colonna "author_id
}

Explorer semplifica notevolmente il lavoro con le relazioni tra tabelle. L'esempio seguente mostra la facilità con cui è possibile ottenere dati da tabelle correlate (libri e relativi autori). Si noti che non è necessario scrivere query JOIN: Nette le genera per noi:

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

foreach ($books as $book) {
	echo 'Book: ' . $book->title;
	echo 'Author: ' . $book->author->name; // crea una JOIN con la tabella "autore".
}

Nette Database Explorer ottimizza le query per ottenere la massima efficienza. L'esempio precedente esegue solo due query SELECT, indipendentemente dal fatto che vengano elaborati 10 o 10.000 libri.

Inoltre, Explorer tiene traccia delle colonne utilizzate nel codice e recupera solo quelle dal database, risparmiando ulteriori prestazioni. Questo comportamento è completamente automatico e adattivo. Se in seguito si modifica il codice per utilizzare altre colonne, Explorer adatta automaticamente le query. Non è necessario configurare nulla o pensare a quali colonne saranno necessarie: questo compito spetta a Nette.

Filtraggio e ordinamento

La classe Selection fornisce metodi per filtrare e ordinare i dati.

where($condition, ...$params) Aggiunge una condizione WHERE. Più condizioni sono combinate con AND
whereOr(array $conditions) Aggiunge un gruppo di condizioni WHERE combinate usando OR
wherePrimary($value) Aggiunge una condizione WHERE basata sulla chiave primaria
order($columns, ...$params) Imposta l'ordinamento con ORDER BY
select($columns, ...$params) Specifica quali colonne recuperare
limit($limit, $offset = null) Limita il numero di righe (LIMIT) e imposta facoltativamente OFFSET
page($page, $itemsPerPage, &$total = null) Imposta la paginazione
group($columns, ...$params) Raggruppa le righe (GROUP BY)
having($condition, ...$params) Aggiunge una condizione HAVING per filtrare le righe raggruppate

I metodi possono essere concatenati (la cosiddetta interfaccia fluente): $table->where(...)->order(...)->limit(...).

Questi metodi consentono anche di utilizzare notazioni speciali per accedere ai dati di tabelle correlate.

Escaping e identificatori

I metodi eseguono automaticamente l'escape dei parametri e degli identificatori di quote (nomi di tabelle e colonne), impedendo l'iniezione di SQL. Per garantire un funzionamento corretto, è necessario seguire alcune regole:

  • Scrivere parole chiave, nomi di funzioni, procedure, ecc. in maiuscolo.
  • Scrivere i nomi delle colonne e delle tabelle in minuscolo.
  • Passare sempre stringhe utilizzando parametri.
where('name = ' . $name);         // **DISASTRO**: vulnerabile all'iniezione SQL
where('name LIKE "%search%"');    // **VERSO**: complica il quoting automatico
where('name LIKE ?', '%search%'); // **CORRETTO**: valore passato come parametro

where('name like ?', $name);     // **WRONG**: genera: `nome` `come` ?
where('name LIKE ?', $name);     // **CORRETTO**: genera: `nome` LIKE ?
where('LOWER(name) = ?', $value);// **CORRETTO**: LOWER(`nome`) = ?

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

Filtra i risultati utilizzando le condizioni WHERE. Il suo punto di forza è la gestione intelligente di vari tipi di valori e la selezione automatica degli operatori SQL.

Utilizzo di base:

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

Grazie al rilevamento automatico degli operatori adatti, non è necessario gestire casi particolari: Nette li gestisce per voi:

$table->where('id', 1);          // DOVE `id` = 1
$table->where('id', null);       // DOVE `id` È NULL
$table->where('id', [1, 2, 3]);  // DOVE `id` IN (1, 2, 3)
// Il segnaposto ? può essere utilizzato senza operatore:
$table->where('id ?', 1);        // DOVE `id` = 1

Il metodo gestisce correttamente anche le condizioni negative e gli array vuoti:

$table->where('id', []);         // DOVE `id` È NULLO E FALSO -- non trova niente
$table->where('id NOT', []);     // DOVE `id` È NULLO O VERO -- trova tutto
$table->where('NOT (id ?)', []); // DOVE NON (`id` È NULLO E FALSO) -- trova tutto.
// $table->where('NOT id ?', $ids); // ATTENZIONE: questa sintassi non è supportata

È inoltre possibile passare come parametro il risultato di un'altra query di tabella, creando una sottoquery:

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

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

Le condizioni possono essere passate anche come array, con gli elementi combinati tramite AND:

// DOVE (`prezzo_finale` < `prezzo_originale`) E (`conteggio scorte` > `min_stock`)
$table->where([
	'price_final &lt; price_original',
	'stock_count > min_stock',
]);

Nell'array si possono usare coppie chiave-valore e Nette sceglierà automaticamente gli operatori corretti:

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

Possiamo anche mescolare espressioni SQL con segnaposto e parametri multipli. Questo è utile per condizioni complesse con operatori definiti con precisione:

// DOVE (`età` > 18) E (ROUND(`punteggio`, 2) > 75,5)
$table->where([
	'age > ?' => 18,
	'ROUND(score, ?) > ?' => [2, 75.5], // due parametri vengono passati come array
]);

Chiamate multiple a where() combinano automaticamente le condizioni usando AND.

whereOr (array $parameters)static

Simile a where(), ma combina le condizioni utilizzando OR:

// DOVE (`status` = 'attivo') OPPURE (`cancellato` = 1)
$table->whereOr([
	'status' => 'active',
	'deleted' => true,
]);

È possibile utilizzare anche espressioni più complesse:

// DOVE (`prezzo` > 1000) O (`prezzo_con_tassa` > 1500)
$table->whereOr([
	'price > ?' => 1000,
	'price_with_tax > ?' => 1500,
]);

wherePrimary (mixed $key)static

Aggiunge una condizione per la chiave primaria della tabella:

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

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

Se la tabella ha una chiave primaria composta (ad esempio, foo_id, bar_id), la si passa come array:

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

// DOVE (`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

Specifica l'ordine in cui vengono restituite le righe. È possibile ordinare in base a una o più colonne, in ordine crescente o decrescente, oppure in base a un'espressione personalizzata:

$table->order('created');                   // ORDINE PER `creato`
$table->order('created DESC');              // ORDINE PER `creato` DESC
$table->order('priority DESC, created');    // ORDINATO PER `priorità' DESC, `creato'
$table->order('status = ? DESC', 'active'); // ORDINATO PER `status` = 'attivo' DESC

select (string $columns, …$parameters)static

Specifica le colonne da restituire dal database. Per impostazione predefinita, Nette Database Explorer restituisce solo le colonne effettivamente utilizzate nel codice. Utilizzate il metodo select() quando dovete recuperare espressioni specifiche:

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

Gli alias definiti con AS sono accessibili come proprietà dell'oggetto ActiveRow:

foreach ($table as $row) {
	echo $row->formatted_date;   // accedere all'alias
}

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

Limita il numero di righe restituite (LIMIT) e imposta facoltativamente un offset:

$table->limit(10);        // LIMIT 10 (restituisce le prime 10 righe)
$table->limit(10, 20);    // LIMITE 10 OFFSET 20

Per la paginazione, è più appropriato utilizzare il metodo page().

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

Semplifica la paginazione dei risultati. Accetta il numero di pagina (a partire da 1) e il numero di elementi per pagina. Opzionalmente, è possibile passare un riferimento a una variabile in cui verrà memorizzato il numero totale di pagine:

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

group (string $columns, …$parameters)static

Raggruppa le righe in base alle colonne specificate (GROUP BY). Si usa in genere in combinazione con le funzioni di aggregazione:

// Conta il numero di prodotti in ogni categoria
$table->select('category_id, COUNT(*) AS count')
	->group('category_id');

having (string $having, …$parameters)static

Imposta una condizione per filtrare le righe raggruppate (HAVING). Può essere utilizzata in combinazione con il metodo group() e le funzioni di aggregazione:

// Trova le categorie con più di 100 prodotti
$table->select('category_id, COUNT(*) AS count')
	->group('category_id')
	->having('count > ?', 100);

Dati di lettura

Per leggere i dati dal database sono disponibili diversi metodi utili:

foreach ($table as $key => $row) Itera tutte le righe, $key è il valore della chiave primaria, $row è un oggetto ActiveRow
$row = $table->get($key) Restituisce una singola riga in base alla chiave primaria
$row = $table->fetch() Restituisce la riga corrente e fa avanzare il puntatore a quella successiva
$array = $table->fetchPairs() Crea un array associativo dai risultati
$array = $table->fetchAll() Restituisce tutte le righe come array
count($table) Restituisce il numero di righe nell'oggetto Selection

L'oggetto ActiveRow è di sola lettura. Ciò significa che non è possibile modificare i valori delle sue proprietà. Questa restrizione garantisce la coerenza dei dati e previene effetti collaterali imprevisti. I dati vengono prelevati dal database e qualsiasi modifica deve essere effettuata in modo esplicito e controllato.

foreach – Iterazione di tutte le righe

Il modo più semplice per eseguire una query e recuperare le righe è l'iterazione con il ciclo foreach. Il ciclo esegue automaticamente la query SQL.

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

get ($key): ?ActiveRow

Esegue una query SQL e restituisce una riga in base alla sua chiave primaria o null se non esiste.

$book = $explorer->table('book')->get(123); // restituisce ActiveRow con ID 123 o null
if ($book) {
	echo $book->title;
}

fetch(): ?ActiveRow

Restituisce una riga e fa avanzare il puntatore interno a quella successiva. Se non ci sono più righe, restituisce null.

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

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

Restituisce i risultati come array associativo. Il primo argomento specifica il nome della colonna da usare come chiave dell'array e il secondo argomento specifica il nome della colonna da usare come valore:

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

Se viene fornito solo il primo parametro, l'intera riga sarà utilizzata come valore, rappresentato come oggetto ActiveRow:

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

Se null è fornito come chiave, l'array sarà indicizzato numericamente a partire da zero:

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

fetchPairs (Closure $callback)array

In alternativa, è possibile passare un callback come parametro. Il callback verrà applicato a ogni riga per restituire un singolo valore o una coppia chiave-valore.

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

// Il callback può anche restituire un array contenente una coppia chiave-valore:
$titles = $explorer->table('book')
	->fetchPairs(fn($row) => [$row->title, $row->author->name]);
// ['Primo libro' => 'Jan Novak', ...]

fetchAll(): array

Restituisce tutte le righe come array associativo di oggetti ActiveRow, dove le chiavi sono i valori della chiave primaria.

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

count(): int

Il metodo count() senza parametri restituisce il numero di righe nell'oggetto Selection:

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

Nota: count() con un parametro esegue la funzione di aggregazione COUNT nel database, come descritto di seguito.

ActiveRow::toArray(): array

Converte l'oggetto ActiveRow in un array associativo in cui le chiavi sono nomi di colonne e i valori sono i dati corrispondenti.

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

Aggregazione

La classe Selection fornisce metodi per eseguire facilmente funzioni di aggregazione (COUNT, SUM, MIN, MAX, AVG, ecc.).

count($expr) Conta il numero di righe
min($expr) Restituisce il valore minimo di una colonna
max($expr) Restituisce il valore massimo di una colonna
sum($expr) Restituisce la somma dei valori di una colonna
aggregation($function) Consente qualsiasi funzione di aggregazione, come ad esempio AVG()GROUP_CONCAT()

count (string $expr): int

Esegue una query SQL con la funzione COUNT e restituisce il risultato. Questo metodo viene utilizzato per determinare quante righe corrispondono a una determinata condizione:

$count = $table->count('*');                 // SELEZIONARE COUNT(*) DA `tabella'
$count = $table->count('DISTINCT column');   // SELEZIONARE COUNT(DISTINCT `colonna') DA `tabella`

Nota: count() senza un parametro restituisce semplicemente il numero di righe nell'oggetto Selection.

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

I metodi min() e max() restituiscono i valori minimi e massimi della colonna o dell'espressione specificata:

// SELEZIONA MAX(`prezzo`) DA `prodotti` DOVE `attivo` = 1
$maxPrice = $products->where('active', true)
	->max('price');

sum (string $expr): int

Restituisce la somma dei valori della colonna o dell'espressione specificata:

// SELEZIONA SOMMA(`prezzo` * `voci_in_magazzino`) DA `prodotti` DOVE `attivo` = 1
$totalPrice = $products->where('active', true)
	->sum('price * items_in_stock');

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

Consente l'esecuzione di qualsiasi funzione di aggregazione.

// Calcola il prezzo medio dei prodotti di una categoria
$avgPrice = $products->where('category_id', 1)
	->aggregation('AVG(price)');

// Combina i tag dei prodotti in un'unica stringa
$tags = $products->where('id', 1)
	->aggregation('GROUP_CONCAT(tag.name) AS tags')
	->fetch()
	->tags;

Se è necessario aggregare risultati che a loro volta derivano da un'aggregazione e da un raggruppamento (ad esempio, SUM(value) su righe raggruppate), si specifica la funzione di aggregazione da applicare a questi risultati intermedi come secondo argomento:

// Calcola il prezzo totale dei prodotti in magazzino per ogni categoria, quindi somma questi prezzi
$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total')
	->group('category_id')
	->aggregation('SUM(category_total)', 'SUM');

In questo esempio, calcoliamo innanzitutto il prezzo totale dei prodotti di ogni categoria (SUM(price * stock) AS category_total) e raggruppiamo i risultati per category_id. Quindi utilizziamo aggregation('SUM(category_total)', 'SUM') per sommare questi totali parziali. Il secondo argomento 'SUM' specifica la funzione di aggregazione da applicare ai risultati intermedi.

Inserimento, aggiornamento e cancellazione

Nette Database Explorer semplifica l'inserimento, l'aggiornamento e la cancellazione dei dati. Tutti i metodi menzionati lanciano un Nette\Database\DriverException in caso di errore.

Selection::insert (iterable $data)static

Inserisce nuovi record in una tabella.

Inserimento di un singolo record:

Il nuovo record viene passato come array associativo o oggetto iterabile (come ArrayHash usato nei form), dove le chiavi corrispondono ai nomi delle colonne della tabella.

Se la tabella ha una chiave primaria definita, il metodo restituisce un oggetto ActiveRow, che viene ricaricato dal database per riflettere eventuali modifiche apportate a livello di database (ad esempio, trigger, valori predefiniti delle colonne o calcoli di autoincremento). Ciò garantisce la coerenza dei dati e l'oggetto contiene sempre i dati correnti del database. Se non è stata definita esplicitamente una chiave primaria, il metodo restituisce i dati di input come array.

$row = $explorer->table('users')->insert([
	'name' => 'John Doe',
	'email' => 'john.doe@example.com',
]);
// $row è un'istanza di ActiveRow contenente i dati completi della riga inserita,
// compreso l'ID autogenerato e le modifiche apportate dai trigger.
echo $row->id;          // Fornisce l'ID dell'utente appena inserito
echo $row->created_at;  // Fornisce l'ora di creazione, se impostata da un trigger.

Inserimento di più record contemporaneamente:

Il metodo insert() consente di inserire più record con una singola query SQL. In questo caso, restituisce il numero di righe inserite.

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

È anche possibile passare come parametro un oggetto Selection con una selezione di dati.

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

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

Inserimento di valori speciali:

I valori possono includere file, oggetti DateTime o letterali SQL:

$explorer->table('users')->insert([
	'name' => 'John',
	'created_at' => new DateTime,           // converte nel formato del database
	'avatar' => fopen('image.jpg', 'rb'),   // inserisce il contenuto del file binario
	'uuid' => $explorer::literal('UUID()'), // chiama la funzione UUID()
]);

Selection::update (iterable $data)int

Aggiorna le righe di una tabella in base a un filtro specificato. Restituisce il numero di righe effettivamente modificate.

Le colonne da aggiornare sono passate come array associativo o oggetto iterabile (come ArrayHash usato nei moduli), dove le chiavi corrispondono ai nomi delle colonne della tabella:

$affected = $explorer->table('users')
	->where('id', 10)
	->update([
		'name' => 'John Smith',
		'year' => 1994,
	]);
// AGGIORNARE `utenti` SET `nome` = 'John Smith', `anno` = 1994 DOVE `id` = 10

Per modificare i valori numerici, si possono usare gli operatori += e -=:

$explorer->table('users')
	->where('id', 10)
	->update([
		'points+=' => 1,  // aumenta il valore della colonna "punti" di 1
		'coins-=' => 1,   // diminuisce il valore della colonna "monete" di 1
	]);
// UPDATE `utenti` SET `punti` = `punti` + 1, `monete` = `monete` - 1 WHERE `id` = 10

Selection::delete(): int

Elimina le righe da una tabella in base a un filtro specificato. Restituisce il numero di righe eliminate.

$count = $explorer->table('users')
	->where('id', 10)
	->delete();
// CANCELLARE DA `utenti` DOVE `id` = 10

Quando si chiama update() o delete(), assicurarsi di usare where() per specificare le righe da aggiornare o eliminare. Se where() non viene utilizzato, l'operazione verrà eseguita sull'intera tabella!

ActiveRow::update (iterable $data)bool

Aggiorna i dati di una riga del database rappresentata dall'oggetto ActiveRow. Accetta come parametro dati iterabili, dove le chiavi sono nomi di colonne. Per modificare i valori numerici, si possono usare gli operatori += e -=:

Dopo l'aggiornamento, ActiveRow viene ricaricato automaticamente dal database per riflettere le modifiche apportate a livello di database (ad esempio, i trigger). Il metodo restituisce true solo se si è verificata una modifica reale dei dati.

$article = $explorer->table('article')->get(1);
$article->update([
	'views += 1',  // incrementa il conteggio delle visualizzazioni
]);
echo $article->views; // Emette il conteggio delle visualizzazioni correnti

Questo metodo aggiorna solo una riga specifica del database. Per gli aggiornamenti in blocco di più righe, utilizzare il metodo Selection::update().

ActiveRow::delete()

Elimina una riga dal database rappresentata dall'oggetto ActiveRow.

$book = $explorer->table('book')->get(1);
$book->delete(); // Elimina il libro con ID 1

Questo metodo elimina solo una riga specifica nel database. Per l'eliminazione in blocco di più righe, utilizzare il metodo Selection::delete().

Relazioni tra tabelle

Nei database relazionali, i dati sono suddivisi in più tabelle e collegati tramite chiavi esterne. Nette Database Explorer offre un modo rivoluzionario di lavorare con queste relazioni, senza scrivere query JOIN o richiedere alcuna configurazione o generazione di entità.

Per la dimostrazione, utilizzeremo il database di esempio(disponibile su GitHub). Il database comprende le seguenti tabelle:

  • author – autori e traduttori (colonne id, name, web, born)
  • book – libri (colonne id, author_id, translator_id, title, sequel_id)
  • tag – tag (colonne id, name)
  • book_tag – tabella di collegamento tra libri e tag (colonne book_id, tag_id)

Struttura del database

In questo esempio di database di libri, troviamo diversi tipi di relazioni (semplificate rispetto alla realtà):

  • Uno-a-molti (1:N) – Ogni libro ha un autore; un autore può scrivere molti libri.
  • Zero-a-molti (0:N) – Un libro può avere un traduttore; un traduttore può tradurre molti libri.
  • Zero-a-uno (0:1) – Un libro può avere un seguito.
  • Molti a molti (M:N) – Un libro può avere diversi tag, e un tag può essere assegnato a molti libri.

In queste relazioni, c'è sempre una tabella padre e una tabella figlio. Ad esempio, nella relazione tra autori e libri, la tabella author è il genitore, mentre la tabella book è il figlio: si può pensare che un libro “appartenga” sempre a un autore. Questo si riflette anche nella struttura del database: la tabella figlio book contiene la chiave esterna author_id, che fa riferimento alla tabella padre author.

Se vogliamo visualizzare i libri insieme ai nomi degli autori, abbiamo due possibilità. O recuperiamo i dati con un'unica query SQL con un JOIN:

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

Oppure recuperiamo i dati in due fasi – prima i libri, poi i loro autori – e li assembliamo in PHP:

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

Il secondo approccio è, sorprendentemente, più efficiente. I dati vengono recuperati una sola volta e possono essere utilizzati meglio nella cache. Questo è esattamente il modo in cui funziona Nette Database Explorer: gestisce tutto sotto il cofano e fornisce un'API pulita:

$books = $explorer->table('book');
foreach ($books as $book) {
	echo 'title: ' . $book->title;
	echo 'written by: ' . $book->author->name; // $book->author è un record della tabella "author" (autore)
	echo 'translated by: ' . $book->translator?->name;
}

Accesso alla tabella dei genitori

L'accesso alla tabella dei genitori è semplice. Si tratta di relazioni come un libro ha un autoreun libro può avere un traduttore. È possibile accedere al record correlato tramite la proprietà dell'oggetto ActiveRow – il nome della proprietà corrisponde al nome della colonna della chiave esterna senza il suffisso id:

$book = $explorer->table('book')->get(1);
echo $book->author->name;      // trova l'autore tramite la colonna "author_id".
echo $book->translator?->name; // trova il traduttore tramite la colonna "translator_id".

Quando si accede alla proprietà $book->author, Explorer cerca una colonna nella tabella book che contenga la stringa author (ad esempio, author_id). In base al valore di questa colonna, recupera il record corrispondente dalla tabella author e lo restituisce come oggetto ActiveRow. Allo stesso modo, $book->translator utilizza la colonna translator_id. Poiché la colonna translator_id può contenere null, viene utilizzato l'operatore ?->.

Un approccio alternativo è fornito dal metodo ref(), che accetta due argomenti – il nome della tabella di destinazione e la colonna di collegamento – e restituisce un'istanza ActiveRow o null:

echo $book->ref('author', 'author_id')->name;      // link all'autore
echo $book->ref('author', 'translator_id')->name;  // link al traduttore

Il metodo ref() è utile se non è possibile utilizzare l'accesso basato sulle proprietà, ad esempio quando la tabella contiene una colonna con lo stesso nome della proprietà (author). In altri casi, l'uso dell'accesso basato sulle proprietà è consigliato per una migliore leggibilità.

Explorer ottimizza automaticamente le query al database. Quando si iterano i libri e si accede ai loro record correlati (autori, traduttori), Explorer non genera una query per ogni singolo libro. Esegue invece solo una query SELECT per ogni tipo di relazione, riducendo in modo significativo il carico del database. Ad esempio:

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

Questo codice eseguirà solo tre query ottimizzate al database:

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 logica per identificare la colonna di collegamento è definita dall'implementazione di Conventions. Si consiglia di utilizzare DiscoveredConventions, che analizza le chiavi esterne e consente di lavorare senza problemi con le relazioni esistenti tra le tabelle.

Accesso alla tabella figlio

L'accesso alla tabella dei figli funziona in senso inverso. Ora chiediamo quali libri ha scritto questo autorequali libri ha tradotto questo traduttore. Per questo tipo di query, si usa il metodo related(), che restituisce un oggetto Selection con i record correlati. Ecco un esempio:

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

// Visualizza tutti i libri scritti dall'autore
foreach ($author->related('book.author_id') as $book) {
	echo "Wrote: $book->title";
}

// Esce tutti i libri tradotti dall'autore
foreach ($author->related('book.translator_id') as $book) {
	echo "Translated: $book->title";
}

Il metodo related() accetta la descrizione della relazione come un singolo argomento usando la notazione a punti o come due argomenti separati:

$author->related('book.translator_id');    // singolo argomento
$author->related('book', 'translator_id'); // due argomenti

Explorer può rilevare automaticamente la colonna di collegamento corretta in base al nome della tabella padre. In questo caso, il collegamento avviene tramite la colonna book.author_id perché il nome della tabella di origine è author:

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

Se esistono più collegamenti possibili, Explorer lancia un'eccezione AmbiguousReferenceKeyException.

Naturalmente, è possibile utilizzare il metodo related() anche quando si iterano più record in un ciclo, e anche in questo caso Explorer ottimizzerà automaticamente le query:

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

Questo codice genera solo due query SQL efficienti:

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

Relazione molti-a-molti

Per una relazione molti-a-molti (M:N), è necessaria una tabella di giunzione (nel nostro caso, book_tag). Questa tabella contiene due colonne di chiave esterna (book_id, tag_id). Ogni colonna fa riferimento alla chiave primaria di una delle tabelle collegate. Per recuperare i dati correlati, si recuperano prima i record dalla tabella di collegamento usando related('book_tag'), e poi si prosegue con i dati di destinazione:

$book = $explorer->table('book')->get(1);
// Visualizza i nomi dei tag assegnati al libro
foreach ($book->related('book_tag') as $bookTag) {
	echo $bookTag->tag->name; // recupera il nome del tag attraverso la tabella dei collegamenti
}

$tag = $explorer->table('tag')->get(1);
// Direzione opposta: visualizza i titoli dei libri con questo tag
foreach ($tag->related('book_tag') as $bookTag) {
	echo $bookTag->book->title; // recupera il titolo del libro
}

Explorer ottimizza nuovamente le query SQL in modo efficiente:

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

Nei metodi where(), select(), order(), e group(), si possono usare notazioni speciali per accedere alle colonne di altre tabelle. Explorer crea automaticamente le JOIN necessarie.

La notazione a punti (parent_table.column) è usata per le relazioni 1:N viste dalla prospettiva della tabella madre:

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

// Trova i libri il cui nome dell'autore inizia con "Jon".
$books->where('author.name LIKE ?', 'Jon%');

// Ordina i libri per nome dell'autore in ordine decrescente
$books->order('author.name DESC');

// Emette il titolo del libro e il nome dell'autore
$books->select('book.title, author.name');

La notezione a colonne è usata per le relazioni 1:N dal punto di vista della tabella madre:

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

// Trova gli autori che hanno scritto un libro con 'PHP' nel titolo
$authors->where(':book.title LIKE ?', '%PHP%');

// Conta il numero di libri per ogni autore
$authors->select('*, COUNT(:book.id) AS book_count')
	->group('author.id');

Nell'esempio precedente con la notazione a due punti (:book.title), la colonna della chiave esterna non è specificata esplicitamente. Explorer rileva automaticamente la colonna corretta in base al nome della tabella padre. In questo caso, si unisce attraverso la colonna book.author_id perché il nome della tabella di origine è author. Se esistono più connessioni possibili, Explorer lancia l'eccezione AmbiguousReferenceKeyException.

La colonna di collegamento può essere specificata esplicitamente tra parentesi:

// Trova gli autori che hanno tradotto un libro con il termine "PHP" nel titolo.
$authors->where(':book(translator).title LIKE ?', '%PHP%');

Le notazioni possono essere concatenate per accedere ai dati di più tabelle:

// Trova gli autori di libri etichettati con "PHP".
$authors->where(':book:book_tag.tag.name', 'PHP')
	->group('author.id');

Estensione delle condizioni di JOIN

Il metodo joinWhere() aggiunge condizioni aggiuntive alle unioni di tabelle in SQL dopo la parola chiave ON.

Ad esempio, supponiamo di voler trovare i libri tradotti da un traduttore specifico:

// Trova i libri tradotti da un traduttore chiamato 'David'
$books = $explorer->table('book')
	->joinWhere('translator', 'translator.name', 'David');
// LEFT JOIN autore traduttore ON book.translator_id = translator.id AND (translator.name = 'David')

Nella condizione joinWhere() è possibile utilizzare gli stessi costrutti del metodo where(): operatori, segnaposto, array di valori o espressioni SQL.

Per query più complesse con più JOIN, è possibile definire degli alias di tabella:

$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)

Si noti che mentre il metodo where() aggiunge condizioni alla clausola WHERE, il metodo joinWhere() estende le condizioni della clausola ON durante i join di tabelle.

Creare manualmente Explorer

Se non si usa il contenitore Nette DI, si può creare manualmente un'istanza di Nette\Database\Explorer:

use Nette\Database;

// $storage implementa Nette\Caching\Storage, ad es:
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir');
// connessione al database
$connection = new Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password');
// gestisce la riflessione della struttura del database
$structure = new Database\Structure($connection, $storage);
// definisce le regole per la mappatura dei nomi delle tabelle, delle colonne e delle chiavi esterne
$conventions = new Database\Conventions\DiscoveredConventions($structure);
$explorer = new Database\Explorer($connection, $structure, $conventions, $storage);
versione: 4.0