ACCESSO AI DATI CON ADO.NET

CARICARE PIU’ TABELLE DI RISULTATI IN UN DATASET

NET CORE

Vediamo come sia possibile caricare più DataTable in un DataSet. In questo esempio ne vengono caricati due in quanto due sono le query, ma se ne possono caricare anche più. Questo è il codice:

Codice

Il codice è stato commentato.

PROTEGGERE L’APPLICAZIONE DALLA SQL INJECTION

Quando selezioniamo un corso imponendo che l’id ci venga passato, in realtà quel valore fa parte della query string o di un form e l’utente ha facoltà di modificarlo. Supponiamo che ci sia un utente malintenzionato. La realtà è che è in grado di fare quello che riporta l’immagine, e cioè una DROP TABLE sulla tabella Courses.

Drop Table Sql Injection

Vediamo un divertente fumetto.

Fumetto

Usiamo i SqliteParameter per evitare la SQL Injection.

SqliteParameter

Il problema di questa sintassi è che se abbiamo utilizzato @id allora anche il SqliteParameter deve avere id come parametro. Quando ad esempio abbiamo dieci parametri è facile incappare in errori. Per risolvere il problema utilizziamo l’interpolazione di stringhe che il C# ci mette a disposizione. Per usare l’interpolazione dobbiamo usare il $ e la @ per definire l’istruzione su più righe.

Interpolazione di stringhe

La FormattableString fa si che rimanga separata la parte della query dai suoi argomenti, cioè i due id. Questa separazione ci ritornerà utilissima quando dobbiamo usare i SqliteParameter. Vediamo come.

FormattableString

Il metodo GetArguments() di FormattableString restituisce i due id presenti nelle due query. Tuttavia, sono in un formato non compatibile con i SqliteParameter, da qui l’esigenza di inserire la @ nell’ultima istruzione del ciclo for. Una volta creati tutti i parametri il metodo AddRange li aggiunge alla collezione dei parametri del SqliteCommand.

OPERAZIONI ASINCRONE CON ASYNC E AWAIT

Sfruttare le operazioni asincrone significa ottenere le migliori performance dalla nostra applicazione. Per spiegarti le operazioni asincrone supponiamo che l’applicazione stia ricevendo tante richieste contemporanee, questo potremmo paragonarlo ad una pizzeria che una sera è strapiena di persone, diciamo più di cento. Ovviamente i gestori non possono tenere cento pizzaioli, uno per ogni persona. Possiamo paragonare i pizzaioli di una pizzeria ai Thread di ASP.NET, non se ne possono creare molti perché sono risorse preziose come i pizzaioli.

OPERAZIONI CHE NON BLOCCANO I THREAD DI ASP.NET CORE

Alcune operazioni sono bloccanti, mentre altre non richiedono l’intervento attivo del Thread o del pizzaiolo, nel caso di un Thread dopo inviata una query c’è il database che lavora, per il pizzaiolo potrebbe essere aspettare la cottura della pizza. Se in quel momento sia il Thread che il pizzaiolo dovessero attendere sarebbe un’efficienza, il pizzaiolo non rimane bloccato ma si mette a lavorare alla pizza di un altro cliente, così pure il Thread può svincolarsi da questa attesa e andare a servire la richiesta di un altro utente.

Operazioni asIncrone

Vediamo dei controesempi.

Operazioni sincrone

Vediamo come usare le operazioni asincrone nel codice.

asincrone

Per poter utilizzare OpenAsync() dobbiamo prima compiere alcune operazioni. Prima di tutto anteporre await al metodo. await designa questa istruzione come asincrona di cui si dovrà attendere il suo completamento prima di elaborare l’operazione successiva. Un Thread che arriva ad elaborare questa istruzione trovando la keyword await si renderà disponibile in modo che ASP.NET Core possa affidargli altro lavoro. Il secondo passo consiste nell’utilizzo della parola chiave async in corrispondenza della firma del metodo. Grazie a questa parola chiave possiamo usare await all’interno del metodo. Il terzo passo è quello di far restituire al metodo un Task. Un Task è una classe che serve a descrivere un’operazione asincrona, ci permette di capire quale è il suo stato, cioè se è ancora in corso, se è andata in errore oppure si è completata.

RIEPILOGO DELLA SEZIONE

ACCEDERE A UN DATABASE RELAZIONALE CON ADO.NET

ADO.NET è il nome della tecnologia di accesso ai dati che ci permette di collegarci a un database relazionale per leggere e scrivere dati grazie a query e comandi in linguaggio SQL. All’interno del database, i dati sono organizzati su tabelle, e ogni tabella è formata di righe e colonne. Ogni riga rappresenta un’entità (ad esempio un corso), mentre le colonne sono gli attributi di quell’entità (ad esempio il titolo, la descrizione, l’autore, …).

Anche se esistono tante tecnologie database, ADO.NET è molto versatile perché è composto di classi base su cui poi ogni produttore può costruire il proprio provider. Ad esempio, Microsoft ha realizzato il provider System.Data.SqlClient che serve a collegarsi a un database SQLServer, ma anche Microsoft.Data.Sqlite che invece serve a collegarsi a un database Sqlite.

Per ogni tecnologia database esiste almeno un provider, che possiamo installare nel progetto come pacchetto NuGet. Perciò, se volessimo installare il provider per Sqlite, potremmo lanciare questo comando:

  1. dotnet add packageData.Sqlite

A questo punto, nella nostra applicazione, abbiamo a disposizione varie classi per interagire con il database. Eccone alcune tra le più usate:

  • SqliteConnection ci permette di stabilire una connessione al database;
  • SqliteCommand serve ad inviare query e comandi SQL al database;
  • SqliteParameter per usare l’input dell’utente in maniera sicura nelle nostre query e comandi;
  • SqliteDataReader per leggere la tabella (o le tabelle) di risultati restituite dal database in seguito a una query SQL;
  • SqliteTransaction per eseguire query e comandi in un contesto isolato e in maniera atomica (tutto o niente), così che i dati nel database restino sempre in uno stato consistente.

Queste classi sono presenti in tutti i provider ADO.NET e quindi, se impariamo a usarle con Sqlite, allora le sapremo già usare per qualsiasi altra tecnologia database. Quel che cambia è giusto il prefisso nel nome delle classi (SqliteConnection per Sqlite, SqlConnection per SQL Server, ecc…). Tuttavia, dobbiamo sempre tenere presente che ogni tecnologia database usa un proprio “dialetto” del linguaggio SQL e perciò ci servirà comunque del tempo per imparare a usarlo al suo massimo potenziale.

CLASSI CONNESSE E DISCONNESSE

Le classi che abbiamo appena visto sono anche chiamate classi “connesse” perché sono pensate per funzionare con una connessione al database. Poi esistono anche queste altre due classi, che invece sono definite “disconnesse” perché possono funzionare senza dover stabilire alcuna connessione. Infatti, le possiamo usare liberamente anche se nella nostra applicazione non esiste ancora alcun database.

  • DataTable rappresenta una tabella dati, su righe e colonne. Grazie al suo metodo Load, è in grado di caricare dati da un SqliteDataReader (o da qualsiasi altro tipo di data reader), così che possiamo conservare i risultati nella memoria dell’applicazione anche dopo aver chiuso la connessione al database;
  • DataSet è una collezione di DataTable e può anche descrivere le relazioni esistenti tra le DataTable, come ad esempio la relazione uno-a-molti che esiste tra un corso e le sue lezioni.

Insomma, le classi “disconnesse” sono in grado di imitare le stesse strutture dati che abbiamo in un database relazionale, così che possiamo usarle nell’applicazione anche dopo aver chiuso la connessione.

LA CONNESSIONE AL DATABASE

La connessione al database andrebbe aperta solo quando necessario e chiusa il più presto possibile, proprio come abbiamo fatto nel precedente esempio. Possiamo anche inviare due o più query SQL con un unico oggetto SqliteCommand: ci basta separarle con punto e virgola. Ecco un esempio in cui recuperiamo sia i dati di un corso che l’elenco delle sue lezioni. Usiamo il ciclo do..while per creare tanti DataTable per quanti sono le tabelle di risultati restituite.

  1. //Ora usiamo un DataSet, che conterrà più DataTable
  2. var dataSet = new DataSet();
  3. using (var conn = new SqliteConnection(“Data Source=Data/MyCourse.db”))
  4. {
  5. Open();
  6. string query = “SELECT * FROM Courses WHERE Id=5; SELECT * FROM Lessons WHERE IdCourse=5”;
  7. using (var cmd = new SqliteCommand(query, conn))
  8. {
  9. using (var reader = cmd.ExecuteReader())
  10. {
  11. do
  12. {
  13. var dataTable = new DataTable();
  14. Add(dataTable);
  15. Load(reader);
  16. } while (!reader.IsClosed); //Continuiamo a ciclare finché il SqliteDataReader non viene chiuso
  17. }

PREVENIRE LA SQL INJECTION

Quando riceviamo l’input dell’utente, dovremmo sempre sanitizzarlo, per evitare che malintenzionati possano eseguire delle query e comandi SQL arbitrari. Per questo usiamo un oggetto SqliteParameter, che poi aggiungiamo al SqliteCommand.

DISTRUGGERE CORRETTAMENTE GLI OGGETTI IDISPOSABLE

Nei precedenti esempi abbiamo fatto ampio uso dei blocchi using. In questo modo ci assicuriamo che il metodo Dispose degli oggetti che implementano IDisposable venga sempre chiamato, anche se si dovesse verificare un’eccezione durante il loro utilizzo. È molto importante che tali oggetti vengano distrutti correttamente, altrimenti si potrebbero verificare sprechi di memoria o altre situazioni indesiderate.

Infatti, se non distruggessimo gli oggetti SqliteConnection, il connection pool, che è l’organo che si occupa di predisporre le connessioni, presto si saturerebbe di connessioni aperte e non ci concederebbe di aprirne altre, di fatto impedendoci di accedere al database.

METODI ASINCRONI LE PAROLE CHIAVE ASYNC E AWAIT

Molti dei metodi delle classi di ADO.NET possiedono delle varianti asincrone. Ad esempio, la SqliteConnection, oltre al metodo Open dispone di OpenAsync.

I metodi asincroni esistono per le operazioni che fanno uso di periferiche di I/O, come il disco o la rete, che sono soggetti a latenze.

Usare i metodi asincroni è ideale perché migliora le prestazioni della nostra applicazione quando ci sono parecchi utenti contemporanei. Infatti, dato che aprire una connessione al database o inviare una query sono operazioni che richiedono un’attesa, il thread può rendersi disponibile durante quell’attesa per fare altro, ad esempio per iniziare a elaborare la richiesta di un altro utente. Poi, quando l’operazione asincrona si conclude, l’esecuzione può riprendere dall’istruzione successiva.

Per poter sfruttare i metodi asincroni abbiamo dovuto apportare 3 modifiche.

  1. Usare l’operatore await prima dell’invocazione del metodo asincrono;
  2. Apporre la parola chiave async in corrispondenza della firma del nostro metodo;
  3. Far restituire al metodo un Task(oppure un Task, nel caso restituisse void).

Opzionalmente, possiamo aggiungere il suffisso “Async” al nome del metodo.

LINK AL CODICE SU GITHUB

GITHUB

Scaricare il codice della sezione10 o clonare il repository GITHUB per avere a disposizione tutte le sezioni nel tuo editor preferito.