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().