Database Explorer

Explorer offre un modo intuitivo ed efficiente di lavorare con il database. Gestisce automaticamente le relazioni tra le tabelle e ottimizza le query, così potete concentrarvi sulla logica della vostra applicazione. Funziona subito, senza configurazione. Se avete bisogno del pieno controllo sulle query SQL, potete usare l'approccio SQL.

  • Lavorare con i dati è naturale e facile da capire
  • Genera query SQL ottimizzate che caricano solo i dati necessari
  • Permette un accesso semplice ai dati collegati senza dover scrivere query JOIN
  • Funziona subito, senza alcuna configurazione né generazione di entità

Il lavoro con Explorer comincia chiamando il metodo table() sull'oggetto Nette\Database\Explorer (i dettagli su come impostare la connessione al database li trovate nel capitolo Connessione e configurazione):

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

Il metodo restituisce un oggetto Selection, che rappresenta una query SQL. A questo oggetto si possono concatenare altri metodi per filtrare e ordinare i risultati. La query viene composta ed eseguita solo nel momento in cui si richiedono i dati, per esempio iterando con foreach. Ogni riga è rappresentata da un oggetto ActiveRow:

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

Explorer semplifica enormemente il lavoro con le relazioni tra le tabelle. L'esempio seguente mostra con quanta facilità possiamo mostrare dati provenienti da tabelle collegate (libri e i loro autori). Notate che non serve scrivere alcuna query JOIN, ci pensa Nette a generarle:

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

foreach ($books as $book) {
	echo 'Libro: ' . $book->title;
	echo 'Autore: ' . $book->author->name; // crea un JOIN alla tabella 'author'
}

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

Explorer tiene inoltre traccia di quali colonne vengono usate nel codice e carica dal database solo quelle, risparmiando altre prestazioni. Questo comportamento è del tutto automatico e adattivo. Se in seguito modificate il codice per usare altre colonne, Explorer adatta automaticamente le query. Non dovete configurare nulla né pensare a quali colonne serviranno: lasciate fare a Nette.

Filtraggio e ordinamento

La classe Selection offre i metodi per filtrare e ordinare la selezione dei dati.

where($condition, ...$params) Aggiunge una condizione WHERE. Più condizioni si uniscono con AND
whereOr(array $conditions) Aggiunge un gruppo di condizioni WHERE unite con OR
wherePrimary($value) Aggiunge una condizione WHERE sulla chiave primaria
order($columns, ...$params) Imposta l'ordinamento con ORDER BY
select($columns, ...$params) Indica quali colonne caricare
limit($limit, $offset = null) Limita il numero di righe (LIMIT) e imposta eventualmente OFFSET
page($page, $itemsPerPage, &$numOfPages = 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 si possono concatenare (la cosiddetta interfaccia fluent): $table->where(...)->order(...)->limit(...).

In questi metodi potete usare anche le notazioni speciali per accedere ai dati delle tabelle collegate.

Escaping e identificatori

I metodi eseguono automaticamente l'escaping dei parametri e quotano gli identificatori (nomi di tabelle e colonne), il che previene la SQL injection. Perché tutto funzioni correttamente bisogna rispettare alcune regole:

  • Scrivete le parole chiave, i nomi di funzioni, procedure ecc. in maiuscolo.
  • Scrivete i nomi di colonne e tabelle in minuscolo.
  • Passate sempre le stringhe tramite parametri.
where('name = ' . $name);         // FALLA CRITICA: SQL injection
where('name LIKE "%search%"');    // SBAGLIATO: complica la quotatura automatica
where('name LIKE ?', '%search%'); // CORRETTO: valore passato come parametro

where('name like ?', $name);     // SBAGLIATO: genera: `name` `like` ?
where('name LIKE ?', $name);     // CORRETTO: genera: `name` LIKE ?
where('LOWER(name) = ?', $value);// CORRETTO: LOWER(`name`) = ?

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

Filtra i risultati con le condizioni WHERE. La sua forza sta nel gestire in modo intelligente i vari tipi di valore e nello scegliere automaticamente gli operatori SQL adatti.

Uso di 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'

Grazie al riconoscimento automatico dell'operatore adatto non dovete occuparvi dei vari casi particolari, li risolve Nette per voi:

$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)
// potete usare anche il segnaposto ? senza operatore:
$table->where('id ?', 1);        // WHERE `id` = 1

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

$table->where('id', []);         // WHERE `id` IS NULL AND FALSE -- non trova nulla
$table->where('id NOT', []);     // WHERE `id` IS NULL OR TRUE -- trova tutto
$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- trova tutto
// $table->where('NOT id ?', $ids); // ATTENZIONE: questa sintassi non è supportata

Come parametro potete passare anche il risultato di un'altra query alla tabella, creando così una sottoquery:

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

Le condizioni si possono passare anche come array, i cui elementi vengono uniti con AND:

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

Nell'array potete usare le coppie chiave ⇒ valore e Nette sceglierà di nuovo automaticamente gli operatori corretti:

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

Nell'array potete combinare espressioni SQL con segnaposto e più parametri. Il che è adatto alle condizioni complesse con operatori definiti con precisione:

// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5)
$table->where([
	'age > ?' => 18,
	'ROUND(score, ?) > ?' => [2, 75.5], // i due parametri si passano come array
]);

Più chiamate a where() uniscono automaticamente le condizioni con AND.

whereOr (array $parameters)static

Analogamente a where() aggiunge delle condizioni, ma le unisce con OR:

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

Anche qui si possono usare espressioni più complesse:

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

wherePrimary (mixed $key)static

Aggiunge una condizione sulla chiave primaria della tabella:

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

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

Se la tabella ha una chiave primaria composta (per esempio foo_id, bar_id), passatela come array:

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

Determina l'ordine in cui vengono restituite le righe. Potete ordinare per una o più colonne, in ordine crescente o decrescente, oppure secondo un'espressione personalizzata:

$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

Indica le colonne da restituire dal database. Per impostazione predefinita Nette Database Explorer restituisce solo le colonne effettivamente usate nel codice. Usate il metodo select() quando avete bisogno di ottenere espressioni specifiche:

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

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

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

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

Limita il numero di righe restituite (LIMIT) e permette eventualmente di impostare uno scostamento:

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

Per la paginazione è più adatto il metodo page().

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

Facilita la paginazione dei risultati. Accetta il numero di pagina (a partire da 1) e il numero di elementi per pagina. Come opzione potete passare il riferimento a una variabile in cui verrà salvato il numero totale di pagine:

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

group (string $columns, …$parameters)static

Raggruppa le righe secondo le colonne indicate (GROUP BY). Si usa di solito insieme alle 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). Si può usare insieme al metodo group() e alle funzioni di aggregazione:

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

Leggere i dati

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

foreach ($table as $key => $row) Itera su 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 sposta 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. Questo significa che non potete cambiare i valori delle sue proprietà. Questa limitazione garantisce la coerenza dei dati ed evita effetti collaterali imprevisti. I dati vengono caricati dal database e ogni modifica va fatta in modo esplicito e controllato.

foreach – iterazione su tutte le righe

Il modo più semplice di eseguire una query e ottenere le righe è iterare con un ciclo foreach. Esegue automaticamente la query SQL.

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

get ($key): ?ActiveRow

Esegue la query SQL e restituisce la riga in base alla chiave primaria, oppure null se non esiste.

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

fetch(): ?ActiveRow

Restituisce la riga corrente e sposta il puntatore interno a quella successiva. Se non esistono altre 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 indica il nome della colonna da usare come chiave dell'array, il secondo il nome della colonna da usare come valore:

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

Se viene indicato solo il primo parametro, il valore sarà l'intera riga, cioè l'oggetto ActiveRow:

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

In caso di chiavi duplicate viene usato il valore dell'ultima riga. Usando null come chiave, l'array sarà indicizzato numericamente a partire da zero (in tal caso non si verificano collisioni):

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

fetchPairs (Closure $callback)array

In alternativa potete passare come parametro un callback che per ogni riga restituirà o un singolo valore, oppure una coppia chiave-valore.

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

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

fetchAll(): array

Restituisce tutte le righe come array associativo di oggetti ActiveRow, dove le chiavi sono i valori delle chiavi primarie.

$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); // alternativa

Attenzione: count() con un parametro esegue la funzione di aggregazione COUNT nel database, vedi più sotto.

ActiveRow::toArray(): array

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

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

Aggregazione

La classe Selection offre metodi per eseguire facilmente le 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) Permette una funzione di aggregazione qualsiasi, per esempio AVG()GROUP_CONCAT()

count (string $expr): int

Esegue una query SQL con la funzione COUNT e restituisce il risultato. Il metodo serve a scoprire quante righe soddisfano una certa condizione:

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

Attenzione: count() senza parametri restituisce solo il numero di righe nell'oggetto Selection.

min (string $expr) e max(string $expr)

I metodi min() e max() restituiscono il valore minimo e massimo della colonna o dell'espressione indicata:

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

sum (string $expr): mixed

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

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

Permette di eseguire una funzione di aggregazione qualsiasi.

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

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

Se dobbiamo aggregare risultati che sono già frutto di una funzione di aggregazione e di un raggruppamento (per esempio SUM(value) su righe raggruppate), indichiamo come secondo argomento la funzione di aggregazione da applicare a questi risultati intermedi:

// calcola il prezzo totale dei prodotti in magazzino per le singole categorie e poi 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 prima il prezzo totale dei prodotti di ogni categoria (SUM(price * stock) AS category_total) e raggruppiamo i risultati per category_id. Poi usiamo aggregation('SUM(category_total)', 'SUM') per sommare questi totali intermedi category_total. Il secondo argomento 'SUM' indica che ai risultati intermedi va applicata la funzione SUM.

Insert, Update e Delete

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

Selection::insert (iterable $data)

Inserisce nuovi record nella tabella.

Inserimento di un singolo record:

Passate il nuovo record come array associativo o come oggetto iterabile (per esempio 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 le eventuali modifiche fatte a livello di database (trigger, valori predefiniti delle colonne, calcolo delle colonne auto-increment). Questo garantisce la coerenza dei dati e l'oggetto contiene sempre i dati attuali dal database. Se la tabella non ha una chiave primaria, non esiste una riga identificabile e il metodo restituisce null.

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

Inserimento di più record in una volta:

Il metodo insert() permette di inserire più record con un'unica query SQL. In tal 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

Come parametro si può passare anche 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:

Come valori possiamo passare anche 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 binario del file
	'uuid' => $explorer::literal('UUID()'), // chiama la funzione UUID()
]);

Selection::update (iterable $data)int

Aggiorna le righe della tabella secondo il filtro indicato. Restituisce il numero di righe effettivamente modificate.

Passate le colonne da modificare come array associativo o come oggetto iterabile (per esempio ArrayHash, usato nei form), dove le chiavi corrispondono ai nomi delle colonne della tabella:

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

Per modificare i valori numerici potete usare gli operatori += e -=:

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

Selection::delete(): int

Cancella le righe dalla tabella secondo il filtro indicato. Restituisce il numero di righe cancellate.

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

Quando chiamate update() o delete(), non dimenticate di indicare con where() le righe da modificare o cancellare. Se non usate where(), l'operazione verrà eseguita sull'intera tabella!

ActiveRow::update (iterable $data)bool

Aggiorna i dati nella riga del database rappresentata dall'oggetto ActiveRow. Accetta un iterabile con i dati da aggiornare (le chiavi sono i nomi delle colonne). Per modificare i valori numerici potete usare gli operatori += e -=:

Dopo l'aggiornamento l'ActiveRow viene automaticamente ricaricato dal database per riflettere le eventuali modifiche fatte a livello di database (per esempio dai trigger). Il metodo restituisce true solo se è avvenuto un cambiamento reale dei dati.

$article = $explorer->table('article')->get(1);
$article->update([
	'views += 1',  // incrementa il numero di visualizzazioni
]);
echo $article->views; // stampa il numero attuale di visualizzazioni

Questo metodo aggiorna una sola riga concreta del database. Per l'aggiornamento massivo di più righe usate il metodo Selection::update().

ActiveRow::delete(): int

Cancella dal database la riga rappresentata dall'oggetto ActiveRow. Restituisce il numero di righe cancellate, che dovrebbe essere 1.

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

Questo metodo cancella una sola riga concreta del database. Per la cancellazione massiva di più righe usate il metodo Selection::delete().

Relazioni tra le tabelle

Nei database relazionali i dati sono divisi in più tabelle e collegati tra loro con le chiavi esterne. Nette Database Explorer offre un modo rivoluzionario di lavorare con queste relazioni: senza scrivere query JOIN e senza dover configurare o generare nulla.

Per mostrare il lavoro con le relazioni useremo come esempio un database di libri (lo trovate su GitHub). Nel database abbiamo le tabelle:

  • author – scrittori 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 usata negli esempi

Nel nostro database di libri di esempio troviamo diversi tipi di relazione (anche se il modello è semplificato rispetto alla realtà):

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

In queste relazioni c'è sempre una tabella genitore e una tabella figlia. Per esempio nella relazione tra autori e libri la tabella author è quella genitore e book quella figlia: potete immaginarvelo come se il libro “appartenesse” sempre a un autore. Questo si riflette anche nella struttura del database: la tabella figlia book contiene la chiave esterna author_id, che punta alla tabella genitore author.

Se dobbiamo elencare i libri con i nomi dei loro autori, abbiamo due possibilità. O otteniamo i dati con un'unica query SQL usando JOIN:

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

Oppure otteniamo i dati in due passaggi (prima i libri, poi i loro autori) e li mettiamo insieme in PHP:

SELECT * FROM book;
SELECT * FROM author WHERE id IN (1, 2, 3);  -- ID degli autori dei libri selezionati

Il secondo approccio è in realtà più efficiente, anche se può sorprendere. I dati vengono caricati una sola volta e si possono sfruttare meglio nella cache. Ed è proprio così che funziona Nette Database Explorer: fa tutto sotto il cofano e vi offre un'API elegante:

$books = $explorer->table('book');
foreach ($books as $book) {
	echo 'titolo: ' . $book->title;
	echo 'scritto da: ' . $book->author->name; // $book->author è un record della tabella 'author'
	echo 'tradotto da: ' . $book->translator?->name;
}

Accesso alla tabella genitore

L'accesso alla tabella genitore è semplice. Sono relazioni del tipo un libro ha un autore oppure un libro può avere un traduttore. Il record collegato si ottiene tramite una proprietà dell'oggetto ActiveRow, il cui nome corrisponde al nome della colonna con la chiave esterna senza il suffisso _id:

$book = $explorer->table('book')->get(1);
echo $book->author->name;      // trova l'autore in base alla colonna author_id
echo $book->translator?->name; // trova il traduttore in base alla colonna translator_id

Quando si accede alla proprietà $book->author, Explorer cerca nella tabella book una colonna il cui nome contenga la stringa author (cioè author_id). In base al valore di questa colonna carica il record corrispondente dalla tabella author e lo restituisce come ActiveRow. Analogamente $book->translator usa la colonna translator_id. Poiché la colonna translator_id può contenere null, nel codice usiamo l'operatore nullsafe ?->.

Un approccio alternativo lo offre il metodo ref(), che accetta due argomenti (il nome della tabella di destinazione e il nome della colonna di collegamento) e restituisce un'istanza di ActiveRow oppure null:

echo $book->ref('author', 'author_id')->name;      // relazione con l'autore
echo $book->ref('author', 'translator_id')->name;  // relazione con il traduttore

Il metodo ref() torna utile quando non si può usare l'accesso tramite proprietà, per esempio perché la tabella contiene una colonna con lo stesso nome (cioè author). Negli altri casi si consiglia l'accesso tramite proprietà, per una migliore leggibilità.

Explorer ottimizza automaticamente le query al database. Quando percorriamo i libri in un ciclo e accediamo ai loro record collegati (autori, traduttori), Explorer non genera una query per ogni libro. Esegue invece solo una query SELECT per ogni tipo di relazione, riducendo in modo significativo il carico sul database. Per esempio:

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

Questo codice esegue solo queste tre velocissime query al database:

SELECT * FROM `book`;
SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- ID dalla colonna author_id dei libri selezionati
SELECT * FROM `author` WHERE (`id` IN (2, 3));    -- ID dalla colonna translator_id dei libri selezionati

La logica con cui viene individuata la colonna di collegamento è determinata dall'implementazione di Conventions. Consigliamo di usare DiscoveredConventions, che analizza le chiavi esterne e permette di lavorare facilmente con le relazioni esistenti tra le tabelle.

Accesso alla tabella figlia

L'accesso alla tabella figlia funziona nella direzione opposta. Ora ci chiediamo quali libri ha scritto questo autore oppure quali libri ha tradotto questo traduttore. Per questo tipo di query usiamo il metodo related(), che restituisce una Selection con i record collegati. Vediamo un esempio:

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

// stampa tutti i libri dell'autore
foreach ($author->related('book.author_id') as $book) {
	echo "Ha scritto: $book->title";
}

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

Il metodo related() accetta la descrizione del collegamento come un unico argomento con la notazione a punto, oppure come due argomenti separati:

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

Explorer sa individuare automaticamente la colonna di collegamento corretta in base al nome della tabella genitore. In questo caso collega tramite la colonna book.author_id, perché il nome della tabella di partenza è author:

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

Se esistessero più collegamenti possibili, Explorer lancia una AmbiguousReferenceKeyException.

Il metodo related() lo possiamo naturalmente usare anche iterando su più record in un ciclo, e anche in questo caso Explorer ottimizza automaticamente le query:

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

Questo codice genera solo due velocissime query SQL:

SELECT * FROM `author`;
SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- ID degli autori selezionati

Relazione molti a molti

Per la relazione molti a molti (M:N) serve una tabella di collegamento (nel nostro caso book_tag), che contiene due colonne con chiavi esterne (book_id, tag_id). Ognuna di queste colonne punta alla chiave primaria di una delle tabelle collegate. Per ottenere i dati collegati prendiamo prima i record dalla tabella di collegamento con related('book_tag') e da lì proseguiamo verso i dati di destinazione:

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

$tag = $explorer->table('tag')->get(1);
// oppure al contrario: stampa i nomi dei libri contrassegnati con questo tag
foreach ($tag->related('book_tag') as $bookTag) {
	echo $bookTag->book->title; // stampa il titolo del libro
}

Explorer ottimizza di nuovo le query SQL in una forma efficiente:

SELECT * FROM `book`;
SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...));  -- ID dei libri selezionati
SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...));                 -- ID dei tag trovati in book_tag

Interrogare attraverso le tabelle collegate

Nei metodi where(), select(), order() e group() potete usare notazioni speciali per accedere alle colonne di altre tabelle. Explorer crea automaticamente i JOIN necessari.

La notazione a punto (tabella_genitore.colonna) si usa per le relazioni 1:N dal punto di vista della tabella figlia:

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

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

La notazione con i due punti (:tabella_figlia.colonna) si usa per le relazioni 1:N dal punto di vista della tabella genitore:

$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 di ogni autore
$authors->select('*, COUNT(:book.id) AS book_count')
	->group('author.id');

Nell'esempio sopra con la notazione a due punti (:book.title) non è indicata la colonna con la chiave esterna. Explorer individua automaticamente la colonna corretta in base al nome della tabella genitore. In questo caso collega tramite la colonna book.author_id, perché il nome della tabella di partenza è author. Se esistessero più collegamenti possibili, Explorer lancia una AmbiguousReferenceKeyException.

La colonna di collegamento si può indicare esplicitamente tra parentesi:

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

Le notazioni si possono concatenare per accedere ai dati attraverso più tabelle:

// trova gli autori dei libri contrassegnati con il tag 'PHP'
$authors->where(':book:book_tag.tag.name', 'PHP')
	->group('author.id');

Estendere le condizioni per JOIN

Il metodo joinWhere() estende le condizioni indicate quando si collegano le tabelle in SQL dopo la parola chiave ON.

Diciamo che vogliamo trovare i libri tradotti da un certo traduttore:

// trova i libri tradotti dal traduttore di nome 'David'
$books = $explorer->table('book')
	->joinWhere('translator', 'translator.name', 'David');
// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David')

Nella condizione di joinWhere() potete usare le stesse costruzioni del metodo where(): operatori, segnaposto, array di valori o espressioni SQL.

Per query più complesse con più JOIN potete definire degli alias per le tabelle:

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

Notate che mentre il metodo where() aggiunge condizioni alla clausola WHERE, il metodo joinWhere() estende le condizioni nella clausola ON quando si collegano le tabelle.

versione: 4.x