Accesso SQL

Nette Database offre due approcci: puoi scrivere query SQL da solo (accesso SQL), oppure puoi farle generare automaticamente (vedi Explorer). L'accesso SQL ti dà il pieno controllo sulle query, garantendo al contempo la loro costruzione sicura.

I dettagli sulla connessione e la configurazione del database si trovano nel capitolo Connessione e configurazione.

Query di base

Per interrogare il database, si usa il metodo query(). Questo restituisce un oggetto ResultSet, che rappresenta il risultato della query. In caso di fallimento, il metodo lancia un'eccezione. Possiamo scorrere il risultato della query usando un ciclo foreach, oppure usare una delle funzioni ausiliarie.

$result = $database->query('SELECT * FROM users');

foreach ($result as $row) {
	echo $row->id;
	echo $row->name;
}

Per inserire in modo sicuro i valori nelle query SQL, usiamo query parametrizzate. Nette Database le rende estremamente semplici: basta aggiungere una virgola e il valore dopo la query SQL:

$database->query('SELECT * FROM users WHERE name = ?', $name);

Con più parametri, hai due opzioni di scrittura. Puoi “intervallare” la query SQL con i parametri:

$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age);

Oppure scrivere prima l'intera query SQL e poi aggiungere tutti i parametri:

$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age);

Protezione contro SQL injection

Perché è importante usare query parametrizzate? Perché ti proteggono da un attacco chiamato SQL injection, in cui un attaccante potrebbe inserire i propri comandi SQL e quindi ottenere o danneggiare i dati nel database.

Non inserire mai variabili direttamente nella query SQL! Usa sempre query parametrizzate, che ti proteggono da SQL injection.

// ❌ CODICE PERICOLOSO - vulnerabile a SQL injection
$database->query("SELECT * FROM users WHERE name = '$name'");

// ✅ Query parametrizzata sicura
$database->query('SELECT * FROM users WHERE name = ?', $name);

Familiarizza con i possibili rischi per la sicurezza.

Tecniche di query

Condizioni WHERE

Puoi scrivere le condizioni WHERE come un array associativo, dove le chiavi sono i nomi delle colonne e i valori sono i dati per il confronto. Nette Database seleziona automaticamente l'operatore SQL più appropriato in base al tipo di valore.

$database->query('SELECT * FROM users WHERE', [
	'name' => 'John',
	'active' => true,
]);
// WHERE `name` = 'John' AND `active` = 1

Nella chiave puoi anche specificare esplicitamente l'operatore per il confronto:

$database->query('SELECT * FROM users WHERE', [
	'age >' => 25,          // usa l'operatore >
	'name LIKE' => '%John%', // usa l'operatore LIKE
	'email NOT LIKE' => '%example.com%', // usa l'operatore NOT LIKE
]);
// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%'

Nette gestisce automaticamente casi speciali come valori null o array.

$database->query('SELECT * FROM products WHERE', [
	'name' => 'Laptop',         // usa l'operatore =
	'category_id' => [1, 2, 3], // usa IN
	'description' => null,      // usa IS NULL
]);
// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL

Per le condizioni negative, usa l'operatore NOT:

$database->query('SELECT * FROM products WHERE', [
	'name NOT' => 'Laptop',         // usa l'operatore <>
	'category_id NOT' => [1, 2, 3], // usa NOT IN
	'description NOT' => null,      // usa IS NOT NULL
	'id' => [],                     // viene omesso
]);
// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL

Per unire le condizioni si usa l'operatore AND. Questo può essere cambiato usando il segnaposto ?or.

Regole ORDER BY

L'ordinamento ORDER BY può essere scritto usando un array. Nelle chiavi indichiamo le colonne e il valore sarà un booleano che determina se ordinare in modo ascendente:

$database->query('SELECT id FROM author ORDER BY', [
	'id' => true, // ascendente
	'name' => false, // discendente
]);
// SELECT id FROM author ORDER BY `id`, `name` DESC

Inserimento dati (INSERT)

Per inserire record si usa l'istruzione SQL INSERT.

$values = [
	'name' => 'John Doe',
	'email' => 'john@example.com',
];
$database->query('INSERT INTO users ?', $values);
$userId = $database->getInsertId();

Il metodo getInsertId() restituisce l'ID dell'ultima riga inserita. Per alcuni database (ad es. PostgreSQL), è necessario specificare come parametro il nome della sequenza da cui generare l'ID tramite $database->getInsertId($sequenceId).

Come parametri possiamo passare anche #valori speciali come file, oggetti DateTime o tipi enum.

Inserimento di più record contemporaneamente:

$database->query('INSERT INTO users ?', [
	['name' => 'User 1', 'email' => 'user1@mail.com'],
	['name' => 'User 2', 'email' => 'user2@mail.com'],
]);

L'INSERT multiplo è molto più veloce perché viene eseguita una singola query al database, invece di molte query individuali.

Avviso di sicurezza: Non usare mai dati non validati come $values. Familiarizza con i possibili rischi.

Aggiornamento dati (UPDATE)

Per aggiornare i record si usa l'istruzione SQL UPDATE.

// Aggiornamento di un singolo record
$values = [
	'name' => 'John Smith',
];
$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1);

Il numero di righe interessate viene restituito da $result->getRowCount().

Per UPDATE possiamo usare gli operatori += e -=:

$database->query('UPDATE users SET ? WHERE id = ?', [
	'login_count+=' => 1, // incrementa login_count
], 1);

Esempio di inserimento o modifica di un record, se esiste già. Usiamo la tecnica ON DUPLICATE KEY UPDATE:

$values = [
	'name' => $name,
	'year' => $year,
];
$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?',
	$values + ['id' => $id],
	$values,
);
// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978)
//   ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978

Nota che Nette Database riconosce in quale contesto dell'istruzione SQL viene inserito il parametro con l'array e costruisce il codice SQL di conseguenza. Quindi dal primo array ha costruito (id, name, year) VALUES (123, 'Jim', 1978), mentre il secondo lo ha convertito nella forma name = 'Jim', year = 1978. Ne parliamo più dettagliatamente nella sezione Hint per la costruzione di SQL.

Cancellazione dati (DELETE)

Per cancellare i record si usa l'istruzione SQL DELETE. Esempio con ottenimento del numero di righe cancellate:

$count = $database->query('DELETE FROM users WHERE id = ?', 1)
	->getRowCount();

Hint per la costruzione di SQL

Un hint è un segnaposto speciale nella query SQL che indica come il valore del parametro deve essere riscritto nell'espressione SQL:

Hint Descrizione Utilizzato automaticamente
?name usa per inserire il nome della tabella o della colonna
?values genera (key, ...) VALUES (value, ...) INSERT ... ?, REPLACE ... ?
?set genera l'assegnazione key = value, ... SET ?, KEY UPDATE ?
?and unisce le condizioni nell'array con l'operatore AND WHERE ?, HAVING ?
?or unisce le condizioni nell'array con l'operatore OR
?order genera la clausola ORDER BY ORDER BY ?, GROUP BY ?

Per l'inserimento dinamico di nomi di tabelle e colonne nella query, si usa il segnaposto ?name. Nette Database si occupa della corretta gestione degli identificatori secondo le convenzioni del database specifico (ad es. racchiudendoli tra backtick in MySQL).

$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table);
// SELECT `name` FROM `users` WHERE id = 1 (in MySQL)

Avviso: usa il simbolo ?name solo per nomi di tabelle e colonne provenienti da input validati, altrimenti ti esponi a un rischio per la sicurezza.

Gli altri hint di solito non devono essere specificati, poiché Nette utilizza un'intelligente rilevazione automatica durante la composizione della query SQL (vedi la terza colonna della tabella). Ma puoi usarlo, ad esempio, in una situazione in cui vuoi unire le condizioni usando OR invece di AND:

$database->query('SELECT * FROM users WHERE ?or', [
	'name' => 'John',
	'email' => 'john@example.com',
]);
// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com'

Valori speciali

Oltre ai comuni tipi scalari (string, int, bool), puoi passare valori speciali come parametri:

  • file: fopen('image.gif', 'r') inserisce il contenuto binario del file
  • data e ora: gli oggetti DateTime vengono convertiti nel formato del database
  • tipi enum: le istanze enum vengono convertite nel loro valore
  • letterali SQL: creati con Connection::literal('NOW()') vengono inseriti direttamente nella query
$database->query('INSERT INTO articles ?', [
	'title' => 'My Article',
	'published_at' => new DateTime,
	'content' => fopen('image.png', 'r'),
	'state' => Status::Draft,
]);

Per i database che non hanno supporto nativo per il tipo di dati datetime (come SQLite e Oracle), DateTime viene convertito nel valore specificato nella configurazione del database tramite la voce formatDateTime (il valore predefinito è U – timestamp unix).

Letterali SQL

In alcuni casi, è necessario specificare direttamente il codice SQL come valore, che però non deve essere interpretato come stringa ed escapato. A questo servono gli oggetti della classe Nette\Database\SqlLiteral. Li crea il metodo Connection::literal().

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	'year >' => $database::literal('YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR())

O alternativamente:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	$database::literal('year > YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR())

I letterali SQL possono contenere parametri:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	$database::literal('year > ? AND year < ?', $min, $max),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017)

Grazie a ciò possiamo creare combinazioni interessanti:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	$database::literal('?or', [
		'active' => true,
		'role' => $role,
	]),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin')

Recupero dati

Scorciatoie per query SELECT

Per semplificare il recupero dei dati, Connection offre diverse scorciatoie che combinano la chiamata query() con il successivo fetch*(). Questi metodi accettano gli stessi parametri di query(), ovvero la query SQL e parametri opzionali. Una descrizione completa dei metodi fetch*() si trova sotto.

fetch($sql, ...$params): ?Row Esegue la query e restituisce la prima riga come oggetto Row
fetchAll($sql, ...$params): array Esegue la query e restituisce tutte le righe come array di oggetti Row
fetchPairs($sql, ...$params): array Esegue la query e restituisce un array associativo, dove la prima colonna rappresenta la chiave e la seconda il valore
fetchField($sql, ...$params): mixed Esegue la query e restituisce il valore del primo campo della prima riga
fetchList($sql, ...$params): ?array Esegue la query e restituisce la prima riga come array indicizzato

Esempio:

// fetchField() - restituisce il valore della prima cella
$count = $database->query('SELECT COUNT(*) FROM articles')
	->fetchField();

foreach – iterazione sulle righe

Dopo l'esecuzione della query, viene restituito un oggetto ResultSet, che consente di scorrere i risultati in diversi modi. Il modo più semplice per eseguire una query e ottenere le righe è iterando in un ciclo foreach. Questo metodo è il più efficiente in termini di memoria, poiché restituisce i dati gradualmente e non li memorizza tutti in memoria contemporaneamente.

$result = $database->query('SELECT * FROM users');

foreach ($result as $row) {
	echo $row->id;
	echo $row->name;
	// ...
}

ResultSet può essere iterato solo una volta. Se è necessario iterare ripetutamente, è necessario prima caricare i dati in un array, ad esempio utilizzando il metodo fetchAll().

fetch(): ?Row

Restituisce una riga come oggetto Row. Se non ci sono più righe, restituisce null. Sposta il puntatore interno alla riga successiva.

$result = $database->query('SELECT * FROM users');
$row = $result->fetch(); // carica la prima riga
if ($row) {
	echo $row->name;
}

fetchAll(): array

Restituisce tutte le righe rimanenti dal ResultSet come un array di oggetti Row.

$result = $database->query('SELECT * FROM users');
$rows = $result->fetchAll(); // carica tutte le righe
foreach ($rows as $row) {
	echo $row->name;
}

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

Restituisce i risultati come un array associativo. Il primo argomento specifica il nome della colonna da utilizzare come chiave nell'array, il secondo argomento specifica il nome della colonna da utilizzare come valore:

$result = $database->query('SELECT id, name FROM users');
$names = $result->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]

Se specifichiamo solo il primo parametro, il valore sarà l'intera riga, ovvero un oggetto Row:

$rows = $result->fetchPairs('id');
// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...]

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

$names = $result->fetchPairs(null, 'name');
// [0 => 'John Doe', 1 => 'Jane Doe', ...]

fetchPairs (Closure $callback)array

In alternativa, puoi specificare come parametro un callback, che per ogni riga restituirà o il valore stesso, o una coppia chiave-valore.

$result = $database->query('SELECT * FROM users');
$items = $result->fetchPairs(fn($row) => "$row->id - $row->name");
// ['1 - John', '2 - Jane', ...]

// Il callback può anche restituire un array con una coppia chiave & valore:
$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]);
// ['John' => 46, 'Jane' => 21, ...]

fetchField(): mixed

Restituisce il valore del primo campo della riga corrente. Se non ci sono più righe, restituisce null. Sposta il puntatore interno alla riga successiva.

$result = $database->query('SELECT name FROM users');
$name = $result->fetchField(); // carica il nome dalla prima riga

fetchList(): ?array

Restituisce una riga come array indicizzato. Se non ci sono più righe, restituisce null. Sposta il puntatore interno alla riga successiva.

$result = $database->query('SELECT name, email FROM users');
$row = $result->fetchList(); // ['John', 'john@example.com']

getRowCount(): ?int

Restituisce il numero di righe interessate dall'ultima query UPDATE o DELETE. Per SELECT, è il numero di righe restituite, ma questo potrebbe non essere noto – in tal caso il metodo restituirà null.

getColumnCount(): ?int

Restituisce il numero di colonne nel ResultSet.

Informazioni sulle query

A scopo di debugging, possiamo ottenere informazioni sull'ultima query eseguita:

echo $database->getLastQueryString();   // stampa la query SQL

$result = $database->query('SELECT * FROM articles');
echo $result->getQueryString();    // stampa la query SQL
echo $result->getTime();           // stampa il tempo di esecuzione in secondi

Per visualizzare il risultato come tabella HTML, si può usare:

$result = $database->query('SELECT * FROM articles');
$result->dump();

ResultSet offre informazioni sui tipi di colonna:

$result = $database->query('SELECT * FROM articles');
$types = $result->getColumnTypes();

foreach ($types as $column => $type) {
	echo "$column è di tipo $type->type"; // ad es. 'id è di tipo int'
}

Logging delle query

Possiamo implementare il nostro logging delle query personalizzato. L'evento onQuery è un array di callback che vengono chiamati dopo ogni query eseguita:

$database->onQuery[] = function ($database, $result) use ($logger) {
	$logger->info('Query: ' . $result->getQueryString());
	$logger->info('Time: ' . $result->getTime());

	if ($result->getRowCount() > 1000) {
		$logger->warning('Large result set: ' . $result->getRowCount() . ' rows');
	}
};
versione: 4.0