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() o 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 (colonneid,name,web,born)book– libri (colonneid,author_id,translator_id,title,sequel_id)tag– tag (colonneid,name)book_tag– tabella di collegamento tra libri e tag (colonnebook_id,tag_id)
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.