Pagina personale di:
Carlo Vecchio
appunti di C#, R, SQL Server, ASP.NET, algoritmi, numeri
Vai ai contenuti

C# - Lavorare con i database Access ACCDB

C#

Note introduttive

  • Il formato *.accdb per il database Access è stato introdotto da Microsoft con la versione del 2007. Il formato precedente è *.mdb.
  • Il formato *.accdb ha alcune caratteristiche migliorative rispetto al formato precedente; per un elenco esaustivo si cerchi in Internet.
  • Per lavorare con C# con questo tipo di database, si utilizza ancora OLEDB, versione 12.
  • Per distribuire applicazioni con *.accdb potrebbe essere necessario installare "AccessRuntime.exe" che si scarica dal sito Microsoft. Al momento in cui scrivo (giugno 2018) esso si trova al link: https://www.microsoft.com/it-it/download/details.aspx?id=50040.
  • Con la versione 2016 di Access è stato introdotto un altro tipo di dati: il "numero grande". Esso occupa uno spazio di memoria di 8 byte; è quindi equivalente al tipo di dati 'long' del .NET. Le prove fatte con questo tipo di dati, non hanno però dato esito positivo, in quanto Access modifica il formato del file *.accdb e l'interfaccia OLEDB dà un errore in fase di apertura della connessione. Al momento quindi non utilizzerò questo tipo di dato.

Classe di interfaccia
  • Per costruire una applicazione C# con un database Access (di tipo *.accdb), può essere utile una semplice classe di interfaccia.
  • Essa gestisce solo la connessione (apertura, chiusura e stringa di connessione) e la transazione.
  • La classe, così come tutti gli esempi successivi, necessitano dei namespace seguenti:

  using System.Data;
   using System.Data.OleDb;

  • Ecco la classe che utilizzo nei paragrafi successivi.

   public class ConnessioneAccess
   {
       private static bool InTransazione = false;
       public static string StringaConnessione = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=Test.accdb";
       public static OleDbConnection Connessione = new OleDbConnection(StringaConnessione);

       public static bool ApriConnessione()
       {
           try
           {
               if (Connessione.State != ConnectionState.Open)
               {
                   Connessione.ConnectionString = ConnessioneAccess.StringaConnessione;
                   Connessione.Open();
               }

               return true;
           }
           catch (Exception ex)
           {
               return false;
           }
       }

       public static bool ChiudiConnessione()
       {
           try
           {
               if (Connessione.State != System.Data.ConnectionState.Closed)
               {
                   Connessione.Close();
               }
               return true;
           }
           catch
           {
               return false;
           }
       }

       public static OleDbTransaction Transazione = null;

       public static void TransazioneBegin()
       {
           Transazione = Connessione.BeginTransaction();
           InTransazione = true;
       }

       public static void TransazioneCommit()
       {
           Transazione.Commit();
           InTransazione = false;
       }

       public static void TransazioneRollback()
       {
           if (InTransazione)
           {
               Transazione.Rollback();
               InTransazione = false;
           }
       }
   }

Database di test
  • Il database è 'Test.accdb'. Non essendo indicata una directory, esso deve essere nella cartella dove si trova il file eseguibile, che generalmente è la directory 'bin\debug' della solution.
  • Per gli esemi successivi, ho definito in 'Test.accdb' la tabella 'TAB_TEST', con i seguenti campi:

   Nome campo             Tipo dati                                Variabile c#
   ID                     Numerazione automatica                   int
   TestoVario             Testo breve, lunghezza 255               string
   NumeroIntero1          Numerico, byte                           byte
   NumeroIntero2          Numerico, intero                         short
   NumeroIntero3          Numerico, intero lungo                   int
   NumPrecisioneSingola   Numerico, precisione singola             float
   NumPrecisioneDoppia    Numerico, precisione doppia              double
   NumDecimale            Numerico, decimale prec. 18, scala 4     decimal
   ValBooleano            Sì/No                                    boolean
   DataOra                Data/ora                                 DateTime

  • 'ID' è un numero generato automaticamente da Access: è un intero lungo (4 byte).
  • 'TestoVario' è stato impostato alla lunghezza massima consentita di 255 caratteri.
  • 'NumeroIntero1', 'NumeroIntero2' e 'NumeroIntero3' sono tre campi numerici rispettivamente di 1 byte, 2 byte e 4 byte.
  • 'NumPrecisioneSingola' e 'NumPrecisioneDoppia' sono i due tipi di dato per gestire numeri a virgola mobile.
  • 'NumDecimale' è il formato per gestire numeri a virgola fissa, impostato con precisione 18 cifre e scala 4 (cioè 18 cifre totali di cui 4 decimali). È il formato adatto per gestire le valute.
  • 'ValBooleano' è il campo che gestisce valori booleani, cioè TRUE o FALSE.
  • 'DataOra' è il campo per gestire date e orari.
  • Nella terza colonna della tabella precedente, c'è il tipo di dato da utilizzare in C#.

Query di esecuzione
  • Per eseguire una query di esecuzione (cioè un INSERT, un UPDATE o un DELETE), si utilizzi il codice seguente.

   // Apertura connessione
   if (!ConnessioneAccess.ApriConnessione())
       throw new Exception("Errore nell'apertura della connessione.");

   // Query da eseguire
   StringBuilder sb = new StringBuilder();
   sb.AppendLine(" INSERT INTO TAB_TEST ");
   sb.AppendLine("   (TestoVario, NumIntero1, NumIntero2, NumIntero3, ");
   sb.AppendLine("    NumPrecisioneSingola, NumPrecisioneDoppia, NumDecimale, ");
   sb.AppendLine("    ValBooleano, DataOra) ");
   sb.AppendLine(" VALUES ");
   sb.AppendLine(" ( ");
   sb.AppendLine("'Testo qualsiasi', ");
   sb.AppendLine("123, ");
   sb.AppendLine("12345, ");
   sb.AppendLine("123456789, ");
   sb.AppendLine("123.456, ");
   sb.AppendLine("123456789.123456789, ");
   sb.AppendLine("1234.5678, ");
   sb.AppendLine("TRUE, ");
   sb.AppendLine("#2018-12-25 11:22:33# ");
   sb.AppendLine(" ) ");

   using (OleDbCommand cmd = new OleDbCommand(sb.ToString(), ConnessioneAccess.Connessione))
   {
       int numRighe = cmd.ExecuteNonQuery();
   }

   // Chiusura connessione
   if (!ConnessioneAccess.ChiudiConnessione())
         throw new Exception("Errore nella chiusura della connessione.");

  • Si osservi che:
    • i campi di testo vanno inseriti racchiusi da apici;
    • il separatore decimale è sempre il punto;
    • i campi booleani vanno inseriti con 'TRUE' o 'FALSE';
    • le date / ore vanno inserite racchiuse con '#' e nel formato indicato nell'esempio precedente.

Query di esecuzione in transazione
  • Per mantenere la coerenza nei dati è necessario utilizzare le transazioni.
  • Nell'esempio seguente, una query di esecuzione è eseguita all'interno di una transazione. È facile estendere il codice racchiudendo più query all'interno della transazione.

   // Apertura connessione
   if (!ConnessioneAccess.ApriConnessione())
       throw new Exception("Errore nell'apertura della connessione.");

   // Apertura transazione
   ConnessioneAccess.TransazioneBegin();

   // Query da eseguire
   StringBuilder sb = new StringBuilder();
   sb.AppendLine(" UPDATE TAB_TEST ");
   sb.AppendLine(" SET TestoVario = 'Nuovo testo' ");
   sb.AppendLine(" WHERE Id = 2 ");

   using (OleDbCommand cmd = new OleDbCommand(sb.ToString(), ConnessioneAccess.Connessione, ConnessioneAccess.Transazione))
   {
       int numRighe = cmd.ExecuteNonQuery();
   }

   // Altre query da eseguire in transazione
   // ...

   // Chiusura transazione
   ConnessioneAccess.TransazioneCommit();

   // Chiusura connessione
   if (!ConnessioneAccess.ChiudiConnessione())
       throw new Exception("Errore nella chiusura della connessione.");

  • Si noti all'interno dello 'using', il riferimento all'oggetto 'Transazione'. Esso è un parametro opzionale, ma se non viene impostato si ha un errore in fase di esecuzione.

Query di selezione scalare
  • Una query di selezione scalare è utile quando è necessario estrarre un solo valore tramite SQL.
  • Essa restituisce il primo campo del primo record della SELECT impostata.
  • È utilizzata spesso quando si deve eseguire una SQL di COUNT(*), o MAX() o simili.

   // Apertura connessione
   if (!ConnessioneAccess.ApriConnessione())
       throw new Exception("Errore nell'apertura della connessione.");

   // Query da eseguire
   StringBuilder sb = new StringBuilder();
   sb.AppendLine(" SELECT COUNT(*) ");
   sb.AppendLine(" FROM TAB_TEST ");

   using (OleDbCommand cmd = new OleDbCommand(sb.ToString(), ConnessioneAccess.Connessione))
   {
       int num = (int)cmd.ExecuteScalar();
   }

   // Chiusura connessione
   if (!ConnessioneAccess.ChiudiConnessione())
       throw new Exception("Errore nella chiusura della connessione.");

Query di selezione
  • Ecco un esempio di una query di selezione.

   // Apertura connessione
   if (!ConnessioneAccess.ApriConnessione())
       throw new Exception("Errore nell'apertura della connessione.");

   // Query da eseguire
   StringBuilder sb = new StringBuilder();
   sb.AppendLine(" SELECT TestoVario, NumIntero1, NumIntero2, NumIntero3, ");
   sb.AppendLine("    NumPrecisioneSingola, NumPrecisioneDoppia, NumDecimale, ");
   sb.AppendLine("    ValBooleano, DataOra ");
   sb.AppendLine(" FROM TAB_TEST ");

   using (OleDbCommand cmd = new OleDbCommand(sb.ToString(), ConnessioneAccess.Connessione))
   {
       using (OleDbDataReader dr = cmd.ExecuteReader())
       {
           while (dr.Read())
           {
               string str = dr.GetString(0);
               byte bte = dr.GetByte(1);
               short shr = dr.GetInt16(2);
               int num = dr.GetInt32(3);
               float sng = dr.GetFloat(4);
               double dbl = dr.GetDouble(5);
               decimal dec = dr.GetDecimal(6);
               bool bln = dr.GetBoolean(7);
               DateTime dtt = dr.GetDateTime(8);
           }
       }
   }

   // Chiusura connessione
   if (!ConnessioneAccess.ChiudiConnessione())
       throw new Exception("Errore nella chiusura della connessione.");

  • Si osservi che una volta ottenuto il DataReader, l'accesso ai campi avviene indicando l'ordinale nella GetXxx(). L'ordinale parte sempre da 0. Nel caso si cambi la sintassi dell'SQL, sarà necessario modificare gli ordinali.
  • In realtà esiste anche una sintassi alternativa che permette di indicare il nome del campo (o l'alias dell'SQL) invece dell'ordinale. La sintassi è dr["nome_campo"]. Questa sintassi, si veda la documentazione, restituisce un tipo 'object' il che comporta una successiva conversione e questo potrebbe comportare un tempo di esecuzione leggermente superiore, a fronte naturalmente di una sintassi più leggibile e manutenibile.

Altre note
  • Se un campo ha valore null, la sintassi GetXxx() dà un errore in fase di esecuzione. Per ovviare a questo si può:
    • Definire tabelle con campi 'non nullabili'; naturalmente ogni SQL di INSERT dovrà avere indicati tutti i campi.
    • Controllare i valori null con il codice seguente:

   if (dr.IsDBNull(8))
   {                       
   }

  • Nel frammento di codice precedente, si verifica per esempio se il 9° campo dell'SQL ha valore nullo.

Elaborazione di righe
  • In certe situazioni è utile elaborare molte righe di una o più tabelle e - in base a certe condizioni - eseguire degli aggiornamenti alle righe di una di queste tabelle.
  • Per esempio, sempre con riferimento alla tabella degli esempi precedenti, si vuole scorrere tutti i record della tabella. Per quei record, il cui valore del campo 'NumIntero1' è diverso da 1, lo si vuole impostare a 1.
  • L'esempio è solo didattico, in quanto è sufficiente una SQL di tipo UPDATE per eseguire il compito. Ma in altri contesti ci si trova di fronte a situazioni dove non c'è una semplice tabella da scorrere, ma una SQL complessa con molte tabelle in JOIN. E in altre situazioni le condizioni con le quali decidere se effettuare l'UPDATE, possono essere molto più complesse.
  • È importante che la tabella sulla quale devono essere eseguiti gli UPDATE, abbia un campo che sia chiave univoca al fine di identificare la riga; la tabella che utilizziamo nell'esempio ha infatti il campo ID.
  • Ecco infine il codice di esempio.

   // Apertura connessione
   if (!ConnessioneAccess.ApriConnessione())
       throw new Exception("Errore nell'apertura della connessione.");

   // Query da eseguire
   StringBuilder sb = new StringBuilder();
   sb.AppendLine(" SELECT Id, NumIntero1 ");
   sb.AppendLine(" FROM TAB_TEST ");

   using (OleDbCommand cmd = new OleDbCommand(sb.ToString(), ConnessioneAccess.Connessione))
   {
       using (OleDbDataReader dr = cmd.ExecuteReader())
       {
           while (dr.Read())
           {
               int id = dr.GetInt32(0);
               byte bte = dr.GetByte(1);
               if (bte != 1)
               {
                   StringBuilder sb1 = new StringBuilder();
                   sb1.AppendLine(" UPDATE TAB_TEST ");
                   sb1.AppendLine(" SET NumIntero1 = 1 ");
                   sb1.AppendLine(" WHERE ID = " + id.ToString());

                   using (OleDbCommand cmd1 = new OleDbCommand(sb1.ToString(), ConnessioneAccess.Connessione, ConnessioneAccess.Transazione))
                   {
                       int numRighe = cmd1.ExecuteNonQuery();
                   }
               }
           }
       }
   }

   // Chiusura connessione
   if (!ConnessioneAccess.ChiudiConnessione())
       throw new Exception("Errore nella chiusura della connessione.");



© 2020 Carlo Vecchio
Torna ai contenuti