Passiamo ora all’illustrazione dei metodi di accesso e modifica dei dati dei database. Per estrarre dati dal database si utilizza il metodo query dell’oggetto database su cui essa deve essere eseguita. Se invece si vuole modificare i dati, occorre utilizzare il metodo execute.
Questi metodi possono essere chiamati su una specifica istanza della classe database e, in tal caso, useranno la connessione collegata a tale istanza, oppure possono essere chiamati in modo statico sulla classe database stessa. In questo caso le chiamate agiranno tramite un’istanza di default dedicata alla sessione e quindi useranno tutte la medesima connessione. Di solito si preferisce questa seconda modalità di esecuzione dei comandi SQL.
Entrambi i metodi richiedono l’inserimento del testo del comando da eseguire. In base a come viene fornita questa stringa, si ottengono due tipi diversi di query o comandi: le query strutturate e le query libere. Ora vedremo i dettagli dei due tipi di query, tenendo conto che le medesime considerazioni valgono per i comandi (execute).
Query strutturate #
Una query strutturata viene inserita nel codice tramite un particolare editor SQL. Per iniziare l’inserimento di una query strutturata, è necessario scrivere la parte iniziale del comando, o anche solo “” (stringa vuota) come parametro del metodo query. A questo punto, cambiando riga nell’editor di codice, la chiamata al metodo viene trasformata in un testo multirighe che verrà editato tramite un editor di codice SQL che si sovrappone a quello JavaScript.
Nell’esempio di codice seguente vediamo un esempio di una query strutturata che recupera il nome di un prodotto dato l’id passato come parametro.
App.Session.prototype.queryExample = function (id)
{
var rs = yield App.NDB.query(app," \
select \
P.ProductName \
from \
Products P \
where \
P.ProductID = id \
");
return rs.rows[0].ProductName;
};
Cliccando all’esterno dell’editor SQL esso viene chiuso e il testo della query o del comando viene formattato nuovamente come testo multirighe.
La modalità più semplice per inserire una query strutturata è quella di comporre per prima la from list, cioè prima di tutto indicare le tabelle ed i join tra di esse. A questo punto è possibile comporre la select list, le clausole where e infine la order by. Non è necessario invece comporre un eventuale group by perché verrà proposta in autonomia dall’editor.
L’inserimento del codice SQL viene facilitato tramite un sistema di autocompletamento che si basa su un parser in tempo reale della query in fase di inserimento. Per massimizzare le prestazioni di questo sistema, si consiglia di mantenere la query sintatticamente corretta.
Si può notare che all’interno della query è possibile referenziare tutte le variabili JavaScript in contesto. Esse verranno sostituite al momento di esecuzione della query utilizzando il formato più appropriato in base al tipo di dati in esse contenuto.
Nelle query strutturate è infine possibile inserire funzioni di database. Se si utilizzano le funzioni proposte dal sistema di autocompletamento, si otterranno query portabili in tutti i tipi di database supportati. Se invece si utilizzano funzioni specifiche, esse potrebbero non essere disponibili in tutti i contesti.
Nell’esempio seguente vediamo un esempio di concatenamento fra stringhe dove viene usato l’operatore || (doppio pipe). Tale espressione verrà tradotta a runtime nell’operatore utilizzato dallo specifico tipo di database su cui la query viene eseguita.
App.Session.prototype.employeeExample = function (id)
{
var rs = yield App.NDB.query(app," \
select \
E.FirstName || ' ' || E.LastName as FullName \
from \
Employees E \
where \
E.EmployeeID = id \
");
return rs.rows[0].FullName;
};
Riassumendo, i vantaggi delle query strutturate sono i seguenti:
- Formattazione automatica del testo multirighe.
- Autocompletamento del codice SQL.
- Traduzione automatica per i vari tipi di database supportati.
- Gestione delle variabili in contesto
- Gestione della sicurezza delle query (per evitare SQL Injection).
L’elenco delle funzioni di database utilizzabili in modo portabile tra i vari tipi di database è disponibile nella libreria Database presente nella sezione corrispondente del progetto.
Se un determinato comando o query deve essere eseguito sia in modalità cloud che offline, si consiglia di utilizzare solo query strutturate e solo funzioni portabili in modo che possano essere eseguite senza problemi in tutti i contesti di utilizzo.
Query libere #
Una query libera consiste in un testo SQL che viene passato al metodo query tramite una variabile di tipo stringa. Un esempio di query libera è il seguente:
App.Session.prototype.employeeExample2 = function (id)
{
var sql = "select E.FirstName + ' ' + E.LastName as \”FullName\”, \
from Employees E where E.EmployeeID = " + App.Utils.sqlEscape(app, id);
//
var rs = yield App.NDB.query(app, sql);
return rs.rows[0].FullName;
};
Come è possibile vedere, la costruzione della stringa SQL è completamente a carico del programmatore che ne garantisce la correttezza e la sicurezza in tutti i contesti d’uso. Il framework si limiterà a mandare in esecuzione il comando così com’è, senza eseguire alcuna verifica in tal senso.
In questo modo è possibile eseguire un codice SQL completamente personalizzato, prendendosi tuttavia in carico una responsabilità importante. Per aiutare la gestione dei parametri esterni, si noti l’utilizzo del metodo di utilità sqlEscape che, in base al tipo di dati, prepara il parametro ad essere incluso in modo sicuro nella stringa.
Recupero dei risultati #
I metodi query ed execute sono entrambi metodi asincroni che vengono automaticamente sequenzializzati tramite yield. In questo caso, il valore restituito dal metodo query è un oggetto di classe DataMap che contiene i risultati. Il valore restituito dal metodo execute è invece un oggetto che restituisce il numero di righe modificate e il prossimo valore della sequence o del campo auto_increment, se presente in un comando di inserimento.
L’istanza di DataMap è un oggetto molto complesso che può essere utilizzato per collegare efficacemente i risultati estratti con l’interfaccia utente. Per maggiori informazioni si consiglia la lettura del capitolo relativo: Datamap.
Per gli scopi del presente capitolo, è sufficiente notare che i risultati estratti dal database vengono restituiti nell’array rows di questo oggetto. Ogni item dell’array è a sua volta un oggetto che contiene le proprietà e i valori estratti. Per questa ragione, l’esempio precedente restituisce il nome e cognome dell’impiegato tramite l’espressione rs.rows[0].FullName.
Gestione della connessione #
Sia le query che i comandi richiedono una connessione con il database per poter essere eseguite. La gestione della connessione è a carico del framework ed è legata alla specifica istanza di database su cui si sta operando. Il framework si comporta come segue:
- La prima volta che viene usata un’istanza di database (o l’istanza di default) per eseguire una query o un comando, essa richiede al framework una connessione al database. Tale connessione viene estratta da un pool di connessioni interno.
- L’istanza di database rimane in possesso della connessione finchè ci sono transazioni aperte o finché vengono inviate ulteriori richieste al database. Dopo un determinato tempo di inattività, la connessione viene rilasciata al pool di connessioni.
- Il pool di connessioni non chiude immediatamente la connessione ma si riserva di riutilizzarla anche per altre sessioni prima di chiuderla definitivamente in caso di inutilizzo prolungato.
Transazioni #
Normalmente sia le query che i comandi vengono eseguiti in modalità autocommit, cioè in una singola transazione separata. In alcuni casi è invece importante utilizzare le transazioni in modo esplicito tramite i metodi della classe database: beginTransaction, commitTransaction e rollbackTransaction.
Il primo caso riguarda il coordinamento di più comandi o query che devono essere eseguiti in modo atomico, cioè o tutti o nessuno, per garantire l’integrità dei dati del database.
Il secondo caso riguarda l’esecuzione di un grande numero di comandi di modifica ai dati o di query. In questo caso l’uso di una singola transazione aumenta notevolmente la velocità dello script. Questo incremento di performance deriva dal fatto che al momento del commit di una transazione, il log del database deve essere scritto in modo definitivo sul dispositivo di memorizzazione, e questo può richiedere anche diversi millisecondi per ogni transazione, anche se composta da un unico comando SQL.
Nell’esempio seguente vediamo come usare beginTransaction e commitTransaction per isolare e rendere più veloce l’aggiornamento del prezzo di una lista di prodotti.
App.Session.prototype.productExample2 = function (productsToIncrease, increasePerc)
{
yield App.NDB.beginTransaction(app);
for (let i = 0; i < productsToIncrease.length; i++) {
yield App.NDB.execute(app," \
update Products set \
UnitPrice = UnitPrice * increasePerc \
where \
ProductID = productsToIncrease[i] \
");
}
yield App.NDB.commitTransaction(app);
};
Gestione delle eccezioni #
Se la query o il comando producono un errore, viene lanciata un’eccezione che può essere catturata con un normale blocco try / catch JavaScript. Il testo dell’errore viene passato al blocco catch.
Se si verifica un’eccezione in un comando SQL mentre una transazione è aperta, essa viene marcata come da abortire e al momento del commit viene comunque inviato al database un comando di rollback. Questo comportamento viene utilizzato per default su alcuni tipi di database e viene generalizzato da Instant Developer Cloud per rendere il codice portabile in tutti i tipi di contesto di esecuzione.
A tal fine si consiglia di utilizzare sempre comandi SQL che non possono causare errori a livello di database. Ad esempio, prima di effettuare un inserimento su una tabella che si riferisce ad altre tabelle, si deve verificare che tali riferimenti siano validi.
Trattamento delle date #
Come abbiamo indicato in precedenza, i valori di tipo date, time e datetime vengono memorizzati nel database nel timezone di default +0. Quando vengono estratti i dati dal database, essi diventano degli oggetti Date JavaScript che possono essere utilizzati all’interno dei metodi della classe app.locale per convertire o formattare tali valori nel timezone della sessione.
Il seguente esempio scrive nella console la data di creazione di un ordine nel formato previsto dalla sessione in corso.
App.Session.prototype.orderExample = function (id)
{
var rs = yield App.NDB.query(app," \
select \
O.OrderDate \
from \
Orders O \
where \
O.OrderID = id \
");
//
var m = app.locale.moment(rs.rows[0].OrderDate);
console.log(m.format("lll"));
};
Per maggiori informazioni sull’utilizzo della classe moment, fare riferimento alla relativa documentazione online.