back to top

Android e SQLite – Guida Sviluppo App Android

Nella lezione precedente abbiamo focalizzato la nostra attenzione sullo storage di tipo interno, per esempio quello operato su un file di testo nel quale è possibile andare a leggere e scrivere. Questo metodo risulta adatto per immagazzinare un quantitativo limitato di informazioni, mentre se si ha a che fare con insiemi di dati di dimensione considerevole, la scrittura e lettura su file non risulta la scelta migliore.

Questo tipo di problema si accentua soprattutto qualora vi sia la necessità di effettuare ricerche all’interno dei dati salvati. Pensiamo infatti alla ricerca di una certa parola in un file di testo contenente un insieme di dati molto grande, con la parola cercata collocata nella parte finale del file. Ovviamente la ricerca richiederebbe diverso tempo in quanto sarebbe necessario scorrere l’intero file!

In queste situazioni è più corretto utilizzare un database che migliori nettamente le prestazioni per quanto riguarda le operazioni che comunemente possono essere eseguite su un insieme di dati, come per esempio la ricerca, l’aggiornamento di un dato o la cancellazione degli stessi.

Se per esempio volessimo memorizzare gli indirizzi di tutti i clienti di un’azienda, l’approccio migliore sarebbe sicuramente quello di utilizzare un database, soprattutto perché scegliendo questa soluzione sarà molto più semplice, successivamente, effettuare interrogazioni sui dati. Inoltre l’utilizzo di un database ci consentirà di assicurare l’integrità dei dati stessi, specificando le relazioni tra differenti insiemi di dati. Si tratta, insomma, di un approccio più solido e professionale.

Al fine di far fronte a tale esigenza, Android utilizza SQLite, un leggerissimo (ma potente) database open-source che si basa sulla sintassi SQL. Se il lettore ha intenzione di sviluppare un’applicazione che utilizzi un database è opportuno che si documenti a fondo sul linguaggio SQL in generale ed in particolare sulle metodologie da adottare per ottimizzare le tabelle e creare basi di dati performanti. Infatti un database non ottimizzato che possiede dei gravi errori concettuali può vanificare il vantaggio legato al suo utilizzo, in quanto le prestazioni (calcolate sul tempo di esecuzione), per le varie operazioni base, calano vertiginosamente.

Dato che la progettazione di database ottimizzati e privi di errori concettuali esula dallo scopo di questa guida, in questa lezione forniremo al lettore solo le linee guide per utilizzare un database SQLite all’interno di un’applicazione (per approfondimenti sul linguaggio SQL rimandiamo all’apposita sezione su questo sito).

Prima di entrare nello specifico dell’utilizzo di SQLite all’interno di applicazioni Android però andiamo ad analizzare, molto velocemente, le caratteristiche di questo sistema.

SQLite è fondamentalmente una libreria che implementa un motore di database transazionale autonomo. A differenza di altri database, SQLite non si basa su un processo separato ma accede direttamente ai propri file contenenti i dati. Le caratteristiche peculiari di SQLite sono le seguenti:

  • Non richiede un server o sistema separato per funzionare
  • Non necessita di configurazioni
  • Un database completo viene gestito da un singolo file
  • Costituisce un DBMS piccolo e leggero
  • Non dipende da applicazioni esterne
  • E’ un database transazionale che rispetta le proprietà ACID (Atomicità, Coerenza, Isolamento e Durabilità)
  • Supporta la maggior parte delle caratteristiche dello standard SQL92
  • E’ scritto in ANSI-C e fornisce semplici ed intuitive API
  • E’ utilizzabile sia su UNIX che su Windows

I comandi SQLite sono quelli standard SQL suddivisi in:

  • DDL (Data Definition Language): CREATE, ALTER, DROP
  • DML (Data Manipulation Language): INSERT, UPDATE
  • DQL (Data Query Language): SELECT

Definizione di una classe di servizio

Dopo questa rapida, e per nulla esaustiva, introduzione ad SQLite andiamo a vedere come utilizzare questo DB all’interno delle nostre applicazioni Android.

Iniziamo con il dire che ogni database creato è accessibile da qualsiasi classe appartenente all’applicazione, ma non è visibile all’esterno dell’applicazione stessa. Questa caratteristica di SQLite oltre a garantire la sicurezza dei dati evita che vi possano essere conflitti tra applicazioni che lavorano sulla medesima base di dati.

Un approccio consigliato per la gestione di database è quello di creare una classe di servizio che contenga tutto il codice necessario per la gestione dei dati, in modo che tale gestione sia trasparente all’interno del codice della nostra applicazione.

Quindi avviamo Eclipse e creiamo un nuovo progetto denominato "TestDatabase" e aggiungiamo al package un nuovo file denominato GestioneDB.java. Si tratta del file in cui andremo a definire il codice di gestione del nostro database:

Nel nostro esempio definiremo un database denominato "TestDB" contenente una sola tabella denominata Clienti, avente tre colonne:

  • id
  • nome
  • indirizzo

Andiamo dunque a modificare il file GestioneDB.java nel modo seguente:

package com.example.testdatabase;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class GestioneDB {

  /*
  Definisco una serie di costanti
  */
  static final String KEY_RIGAID = "_id";
  static final String KEY_NOME = "nome";
  static final String KEY_INDIRIZZO = "indirizzo";
  static final String TAG = "GestioneDB";
  static final String DATABASE_NOME = "TestDB";
  static final String DATABASE_TABELLA = "clienti";
  static final int DATABASE_VERSIONE = 1;

  /*
  Creo una costante contenente la query per la creazione del database
  */
  static final String DATABASE_CREAZIONE = "create table clienti (_id integer primary key autoincrement, "
  + "nome text not null, indirizzo text not null);";
  
  final Context context;
  DatabaseHelper DBHelper;
  SQLiteDatabase db;

  /*
  Costruttore
  */
  public GestioneDB(Context ctx)
  {
    this.context = ctx;
    DBHelper = new DatabaseHelper(context);
  }

  /*
  Estendo la classe SQLiteOpenHelper che si occupa
  della gestione delle connessioni e della creazione del DB
  */
  private static class DatabaseHelper extends SQLiteOpenHelper
  {    
    DatabaseHelper(Context context)
    {
      // invoco il costruttore della classe base
      super(context, DATABASE_NOME, null, DATABASE_VERSIONE);
    }
    @Override
    public void onCreate(SQLiteDatabase db)
    {
      try {  
        db.execSQL(DATABASE_CREAZIONE);
      } 
      catch (SQLException e) {
        e.printStackTrace();
      }
    }
  }

  /*
  Apro la connessione al DB
  */
  public GestioneDB open() throws SQLException
  {
    // ottengo accesso al DB anche in scrittura
    db = DBHelper.getWritableDatabase();
    return this;
  }

  /*
  Chiudo la connessione al DB
  */
  public void close()
  {
    // chiudo la connessione al DB
    DBHelper.close();
  }

  /*
  Estraggo elenco di tutti i clienti
  */
  public Cursor ottieniTuttiClienti()
  {
    // applico il metodo query senza applicare nessuna clausola WHERE
    return db.query(DATABASE_TABELLA, new String[] {KEY_RIGAID, KEY_NOME, KEY_INDIRIZZO}, null, null, null, null, null);
  }

  /*
  Estraggo un sigolo cliente specificandone l'ID
  */
  public Cursor ottieniCliente(long rigaId) throws SQLException
  {
    // applico il metodo query filtrando per ID
    Cursor mCursore = db.query(true, DATABASE_TABELLA, new String[] {KEY_RIGAID, KEY_NOME, KEY_INDIRIZZO}, KEY_RIGAID + "=" + rigaId, null, null, null, null, null);
    if (mCursore != null) {
      mCursore.moveToFirst();
    }
    return mCursore;
  }

  /*
  Inserimento di un nuovo cliente nella tabella
  */
  public long inserisciCliente(String nome, String indirizzo)
  {
    // creo una mappa di valori
    ContentValues initialValues = new ContentValues();
    initialValues.put(KEY_NOME, nome);
    initialValues.put(KEY_INDIRIZZO, indirizzo);
    // applico il metodo insert
    return db.insert(DATABASE_TABELLA, null, initialValues);
  }

  /*
  Cancellazione di un nuovo cliente nella tabella
  */
  public boolean cancellaCliente(long rigaId)
  {
    // applico il metodo delete
    return db.delete(DATABASE_TABELLA, KEY_RIGAID + "=" + rigaId, null) > 0;
  }

  /*
  Aggiorno dati di un cliente
  */
  public boolean aggiornaCliente(long rigaId, String name, String indirizzo)
  {
    // creo una mappa di valori
    ContentValues args = new ContentValues();
    args.put(KEY_NOME, name);
    args.put(KEY_INDIRIZZO, indirizzo);
    // applico il metodo update
    return db.update(DATABASE_TABELLA, args, KEY_RIGAID + "=" + rigaId, null) > 0;
  }  
}

Andiamo ad analizzare questo codice. Nella prima parte abbiamo definito alcune costanti relative ai campi della tabella che andremo a gestire e al relativo database. In particolare la costante DATABASE_CREAZIONE contiene l’istruzione SQL per la creazione della tabella clienti all’interno del database.

All’interno della classe GestioneDB abbiamo poi creato una classe privata (DatabaseHelper) che estende la classe SQLiteOpenHelper, cioè la classe che in Android consente di gestire la creazione di database, la gestione delle connessioni ed altri aspetti come il versionig dello stesso. In tale classe abbiamo effettuato l’override del metodo onCreate al fine di poter creare dinamicamente le tabelle del nostro DB.

Si noti che il metodo onCreate viene invocato nel momento in cui non viene trovato, nello spazio riservato all’applicazione, il database indicato. Questo metodo, pertanto, viene invocato solo una volta quando, cioè, è necessario creare il database. Contestualmente utilizziamo il metodo execSQL() passandogli la query SQL impostata nell’apposita costante al fine della creazione delle tabelle.

Successivamente abbiamo implementato tutti i metodi canonici per l’apertura e chiusura del database, per la selezione dei record e per l’inserimento, la modifica e cancellazione dei dati dalla nostra tabella clienti.

Per quanto riguarda il metodo di apertura abbiamo utilizzato getWriteableDatabase() che restituisce un riferimento al database che consente anche la modifica dei dati (se avessimo voluto un accesso in sola lettura avremmo dovuto usare getReadableDatabase()).

Per quanto riguarda gli altri metodi della nostra classe, come potete vedere, abbiamo utilizzato i metodi nativi implementati nelle API di Android, cioè:

  • query() – consente di effettuare interrogazioni al database;
  • insert() – consente di inserire un nuovo record in una tabella;
  • update() – consente di aggiornare un record già presente nella tabella;
  • delete() – consente di cancellare un record da una tabella;

Vediamo di seguito la sintassi di questi quattro metodi:

query()

query(tableName, tableColumns, whereClause, whereArgs, groupBy, having, orderBy)
  • tableName – nome della tabella su cui operare;
  • tableColumns – (facoltativo) elenco delle colonne da includere nel risultato della query;
  • whereClause – (facoltativo) clausola WHERE (se si utilizza null vengono estratti tutti i record);
  • whereArgs – (facoltativo) argomenti che sostituiscono gli eventuali ‘?’ nella clausola WHERE;
  • groupBy – (facoltativo) serve a raggruppare i risultati in base al/ai campo/i specificato/i;
  • having – (facoltativo) è utilizzato per applicare un vincolo sui dati risultanti dall’operazione di raggruppamento;
  • orderBy – (facoltativo) serve ad indicare il criterio di ordinamento dei risultati.

insert()

insert(tableName, nullColumnHack, values)
  • tableName – nome della tabella su cui operare;
  • nullColumnHack – (facoltativo) è un valore che di solito va impostato a null, in quanto deve essere usato solo nel caso in cui si voglia inserire un record con valori tutti nulli;
  • values – mappa dei valori da inserire (nel formato chiave/valore).

update()

update(tableName, values, whereClause, whereArgs)
  • tableName – nome della tabella su cui operare;
  • values – mappa dei valori da aggiornare (nel formato chiave/valore).
  • whereClause – (facoltativo) clausola WHERE (se si utilizza null vengono aggiornate tutti i record);
  • whereArgs – (facoltativo) argomenti che sostituiscono gli eventuali ‘?’ nella clausola WHERE;

delete()

delete(tableName, whereClause, whereArgs)
  • tableName – nome della tabella su cui operare;
  • whereClause – (facoltativo) clausola WHERE (se si utilizza null vengono elimiati tutti i record);
  • whereArgs – (facoltativo) argomenti che sostituiscono gli eventuali ‘?’ nella clausola WHERE;

whereClause e whereArgs

Diversi metodi fanno ricorso a questi due attributi. In realtà, molto spesso, viene utilizzato solo il primo scrivendo per intero la clausola WHERE in questo modo:

db.delete("mia_tabella", "nome = 'Mario' AND cognome = 'Rossi'", null)

mentre whereArgs viene settato su null.

Tuttavia è anche possibile scrivere la clausola WHERE in modo astratto facendo ricorso a delle variabili rappresentate dal simbolo ‘?’, in questo modo:

db.delete("mia_tabella", "nome = ? AND cognome = ?", new String[] { "Mario","Rossi" })

così facendo, il valore di ‘?’ sarà specificato in whereArgs.

execSQL() e rawQuery()

Come avrete notato, i quattro metodi esposti poco sopra non prevedono un uso esplicito di query SQL. Per chi invece preferisse "scrivere" le query è necessario ricorrere ad altri due metodi cioè execSQL() (esegue una query) o rawQuery() (effettua un’interrogazione). Il primo può essere utilizzato in sostituzione dei insert(), update() e delete(), il secondo al posto di query().

Pubblicità

In questa guida...