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 < 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() o 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 (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
)
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 autore o un 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 autore
o quali 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
Interrogazione attraverso tabelle correlate
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 < ?', 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);