Android lezione 15 – Aggiungiamo un database con SQLite

In questa lezione vedremo come salvare dei dati della nostra applicazione in un database relazionale con SQLite. Per chi non lo sapesse, SQLIte è un database relazionale, che non necessita dell’installazione di nessun server, e tutto avviene all’interno di un file (dove ogni database è contenuto in un file differente).

Ora immaginiamo di voler sviluppare una applicazione che salvi delle note composte da un titolo all’interno di un database, in questa lezione vedremo come svilupparla (mi concentreró sulla parte relativa alla gestione del database, lasciando a voi tutto ciò che riguarda l’interfaccia grafica). Per utilizzare i database avremo bisogno di una classe che estende SQLiteOpenHelper, e faccia l’override dei metodi:

  • onCreate
  • onUpdate

E che abbia un costruttore che mediante super chiami il costruttore della classe padre. Iniziamo con il vedere la struttura base della classe:

public class DatabaseHelper extends SQLiteOpenHelper {
 
	public DatabaseHelper(Context context, String name, CursorFactory factory,
			int version) {
		super(context, name, factory, version);
	}
 
	@Override
	public void onCreate(SQLiteDatabase db) {
		// TODO Auto-generated method stub
 
	}
 
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldversion, int newversion) {
		// TODO Auto-generated method stub
 
	}
}

Vediamo cosa abbiamo in questa classe:

  • Il metodo onCreate viene chiamato quando il database non esiste e necessita di essere creato. Quindi per esempio viene chiamato quando l’applicazione viene installata per la prima volta.
  • Il metodo onUpgrade viene chiamato invece quando occorre modificare la struttura ad un database esistente. Ma come fa a sapere la nostra classe quando dobbiamo aggiornare un db esistente? Questo lo vediamo spiegando il costruttore.
  • Il costruttere come vedete fra gli argomenti prende due paramentri: il nome del database e un intero chiamato version. Bene questo intero servirá per indicare alla nostra classe se occorre o meno aggiornare il database.

Cominciamo con vedere gli argomenti del costruttore:

  • Context context – Il primo è il contesto dell’applicazione.
  • String name – Il nome che avrá il nostro database. Normalmente non cambierá nel tempo.
  • CursorFactory factory – Se vogliamo usare degli oggetti cursor che estendono quello standard (in questo articolo useremo quello standard, quindi passeremo null).
  • int version – Questo invece è il numero di versione del database. Come accennavo prima serve al costruttore per capire, se deve aggiornare o meno il database (per esempio abbiamo modificato i campi di una tabella, oppure abbiamo aggiunto una tabella nuova). Se il numero di versione è maggiore a quello utilizzato precedentemente, la classe sa che deve chiamare il metodo onUpgrade non appena verrá richiesta la prossima operazione di scrittura sul database. Nel caso invece il numero di maggiore è inferiore il metodo che verrá chiamato è onDownGrade(…).

Inquesta lezione creiamo un database con dei semplici contatti di persone. I campi che inseriremo in questa tabella sono:

  • id – Intero Numero sequenziale univoco da assegnare al contatto (Anche chiave primaria)
  • Nome e cognome – Stringa (usiamo un campo unico per nome e cognome, in barba  a tutte le forme di normalizzazione! tanto è solo un semplice esempio :D)
  • Numero di telefono – Intero

Mentre le operazioni che andremo ad implementare saranno:

  • Inserimento di un nuovo contatto
  • selezione di un contatto dato l’id
  • eliminazione di un contatto

Quindi vediamo la sintassi SQL per questa tabella:

CREATE TABLE my_contacts (
 id INTEGER PRIMARY KEY AUTOINCREMENT,
 contact_name TEXT,
 number INTEGER);

Questa query andrá eseguita all’interno del metodo onCreate, che diventerá quindi: :

@Override
public void onCreate(SQLiteDatabase db) {
    String create_table = "CREATE TABLE my_contacts ( "
        + "id INTEGER PRIMARY KEY AUTOINCREMENT, "
        + "contact_name TEXT, "
        + "number TEXT);";
    db.execSQL(create_table);
}

Questa funzione non verrá comunque invocata al momento della creazione della classe, infatti il database non verrá creato fino a che non verrà chiamata per la prima volta la funzione getWritableDatabase (che come ci suggerisce il nome ci ritorna un oggetto per accedere al database in modalitá scrittura).

 

Anche se non è strettamente necessario, suggerisco per le varie tabelle definite nel database (in questo caso solo una), di creare delle classi che le rappresentano, questo accorgimento rende la vita più semplice soprattutto per progetti di grandi dimensioni. Quindi creiamo una classe Contacts.java (in questo esempio la assegno al package com.italialinux.data.model) che avrá come variabili le varie colonne del database:

 

public class Contact {
 
	private long id;
	private String contact_name;
	private String number;
 
	public Contact(long id, String contact_name, String number) {
		super();
		this.id = id;
		this.contact_name = contact_name;
		this.number = number;
	}
 
	public Contact(String contact_name, String number) {
		super();
		this.contact_name = contact_name;
		this.number = number;
	 }
 
	public long getId() {
		return id;
	}
	public void setId(long id) {
		this.id = id;
	}
	public String getContact_name() {
		return contact_name;
	}
	public void setContact_name(String contact_name) {
		this.contact_name = contact_name;
	}
	public String getNumber() {
		return number;
	}
	public void setNumber(String number) {
		this.number = number;
	} 
} 

Come potete vedere questa classe è un esatta rappresentazione della tabella che abbiamo definito per il database. La sua struttura è abbastanza semplice, contiene solo un costruttore e i metodi getters e setters per i vari campi della tabella.

A questo punto abbiamo terminato i preparativi e possiamo passare a implementari le varie operazioni per leggere e/o scrivere dati su questo database.

Partiamo dall’inserimento di un record. Aggiungiamo nella classe DatabaseHelper un nuovo metodo: insertContact, che come argomemnto prenderá un oggetto di tipo Contact:

public boolean insertContact(Contact contact){
    ...
    return false
}

Per inserire un nuovo record occorre fare tre operazioni:

  • Prima di tutto ottenere un accesso in scrittura al database, questo lo otteniamo mediante il metodo getWritableDatabase()  definito nella classe parent del nostro DatabaseHelper. Questo metodo torna un oggetto di tipo SQLiteDatabase
  • Creare un oggetto di tipo ContentValues e popolarlo con i valori della riga che andremo a inserire.
  • Chiamare la funzione insert passandogli l’oggetto ContentValues appena creato.

Vediamole in sequenza.

Per ottenere l’oggetto SQLiteDatabase scrivibile ci basterá fare all’interno del metodo insertContact la chiamata:

SQLiteDatabase db = this.getWritableDatabase();

SQLite per inserire delle righe in una tabella utilizza un oggetto di tipo ContentValues che è una sorta di HashMap dove ogni elemento è identificato da una coppia <Chiave, Valore> e la chiave é il nome della colonna corrispondente al valore che si deve inserire. Per comoditá (e anche per rendere il codice un pò più leggibile) creiamo una funzione apposita che dato un oggetto di tipo Contact rittorna un oggetto ContentValues giá popolato. La classe è molto semplice da utilizzare, e per aggiungere valori abbiamo il metodo put(chiave, valore).

Quindi definiamo (per comoditá) un nuovo metodo, in questo esempio lo chiamerò createContentValues che prende un oggetto di tipo Contact e ritorna un oggetto di tipo ContentValues già popolato:

public ContentValues createContentValues(Contact contact){
		ContentValues values = new ContentValues();
		values.put("contact_name", contact.getContact_name());
		values.put("number", contact.getNumber());
		return values;
}

Come potete notare, non ho aggiunto il campo ID, questo perchè è stato definito come autoincrement, e quindi se non lo setto, se ne occupa il sistema di assegnarli automaticamente un id.

Ora abbiamo tutto quello che ci serve per poter aggiungere un record nella nostra tabella dei contatti di seguito il metodo insertContact:

public boolean insertContact(Contact contact){
	Log.i(TAG, "Inserting Notification");
	SQLiteDatabase db = this.getWritableDatabase();
	ContentValues values = createContentValues(contact);
	long id = db.insert("my_contacts", null, values);
	if(id==-1)
		return false;
	return true;
}

Riepilogando:

  • Con getWritableDatabase otteniamo una istanza “scrivibile” dell’oggetto SQliteDatabase.
  • Chiamiamo createContentValues per ottenere un oggetto di tipo ContentValues che rappresenta una entry nella tabella.
  • chiamiamo la funzione db.insert alla quale passiamo come argomenti il nome della tabella, e l’oggetto ContentValues appena ottenuto. Questa funzione torna -1 se l ínsert non è andata a buon fine, altrimenti torna l’id della riga con la quale è stato inserito.

Passiamo ora all’eíminazione di un record, anche per questo metodo avremo ovviamente bisogno del database in modalitá scrittura, e come per l’inserimento abbiamo a disposizione un metodo delete che prende tre parametri:

  1. La tabella dalla quale vogliamo eliminare il record
  2. La condizione where,  senza WHERE davanti. Quindi se vogliamo per esempio eliminare un elemento che ha per id il valore della variabile todelete_id scriveremo il parametro nel seguente formato: “id=” + todelete_id.
  3. Infine, la lista degli argomenti per la clausola where, nel caso si decidesse di non definire tutti gli argomenti nel parametro precedente, e passarli invece come vettore di Stringhe, in questo caso le variabili concatenate nel parametro 2 vengono sostituire con un ?. Altrimenti se non utilizziamo questa modalitá questo parametro dovrebbe restare null

Vediamo i due modi con i quali si può chiamare questo metodo:

db.delete("my_contacts", "id="+id, null);

oppure:

db.delete("my_contacts", "id=?", new String[](Integer.toString(id));

Vediamo ora come fare per aggiornare un elemento che è stato precedente inserito. Come nei casi precedenti, anche per l’update abbiamo un metodo con lo stesso nome. La logica è molto simile a quella vista per l’eliminazione di un record, con l’unica differenza che in questo caso oltre alla tabella, e alla clausola where passiamo anche un oggetto di tipo ContentValues con valori che vogliamo andare a modificare del record. Il metodo completo è il seguente:

 

public boolean updateContact(long id, String number){
		SQLiteDatabase db= getWritableDatabase();
		ContentValues values = new ContentValues();
		values.put(Columns.NUMBER, number);
		int rows = db.update("my_contacts", values, Columns.id + "=?" , new String[]{String.valueOf(id)});
		Log.i(TAG, "Rows: " + rows);
		return false;
	}

Resta da vedere come effettuare delle query. Esistono di versi modi per farle (noi ne vedremo due):

  • Tramite l’esecuzione diretta del codice SQL passato come stringa
  • Tramite il metodo query, passando al metodo come argomenti le varie componenti della query.

In entrambi i casi, l’output della query è un oggetto di tipo Cursor, che conterrá l’insieme navigabile delle tuple che compongono il risultato. Prima di vedere come effettuare delle query spieghiamo il funzionamento dell’oggetto cursor, supponiamo di avere una variabile di questo tipo che contiene gia un set di Risultati:

Cursor result = getAllContacts();

Il contenuto di getAllContacts lo vedremo quando spiegheremo il Prepared Statement. Siccome result è un insieme di tuple, la classe Cursor mette a disposizione i seguenti metodi per navigarle:

  • boolean moveToFirst()  – Questo metodo posiziona result sulla prima tupla. I
  • boolean moveToLast() – Posiziona il resultset all’ultimo elemento.
  • boolean moveToNext(), boolean moveToPrevious() – posiziona alla tupla immediatamente successivo e immediatamente precedente quello corrente
  • boolean moveToPositon(int position) – Porta alla tupla che si trova alla posizione position

Una volta che abbiamo ottenuto le tuple all’interno dell’oggetto cursor, per iniziare a navigarle dobbiamo necessariamente effettuare una chiamata alla funzione moveToFirst(), altrimenti verrebbe generata un eccezione.

Nel caso si sta navigando il set dei risultati, per sapere se abbiamo superato l’ultimo elemento, possiamo usare il metodo isAfterLast() che ritorna true se ci troviamo oltre l’ultima tupla.

Per l’accesso alle colonne della tupla, l’oggetto Cursor mette a disposizione dei metodi tipati get* (i.e. getInt, getLong, getString, etc) che prendono come argomenti un intero che rappresenta l’indice numerico della colonna alla quale si riferiscono.

 

Chiaramente, noi sappiamo i nomi delle colonne, ma non il loro indice, ma anche per questo esiste un metodo (o meglio due) che dato il nome della colonna ci restituisce il suo indice:

  • getColumnIndex(String columnName) – Torna l’indice di colonna se esiste (0-based) o -1 altrimenti.
  • getColumnIndexOrThrow(String columnName) – Come sopra, ma se l’indice di colonna non esiste in questo caso genera un eccezione di tipo IllegalArgumentException.

Quindi per leggere per esempio il campo contact_name dalla tabella dei contatti potremo avere una chiamata simile alla seguente:

Integer name_column_index = result.getColumnIndex("contact_name");
String contact_name = result.getString(name_column_index);

Dopo che abbiamo visto come leggere i risultati di una query, spieghiamo come eseguirle. Come detto sopra esistono principalmente due modi, il primo che comporta l’esecuzione di una stringa contenente querty SQL, mentre il secondo comporta l’utilizzo del metodo query(…).

Per spiegare queste due modalitá farò due esempi:

  • Per l’utilizzo della raw query farò un metodo che mi seleziona la lista completa dei contatti dal database.
  • Per il secondo caso invece il metodo cerchèra un contatto dato il suo id.

Esempio 1 – Selezione di tutti i contatti 

Iniziamo col definire il metodo nel quale andremo a scrivere la nostra funzione:

public List<Notification> getAllContacts(){
 
....
 
}

La query da eseguire ovviamente è la seguente:

SELECT * FROM my_contacts

Quindi creiamo una stringa con questa query:

String stringQuery = "SELECT * FROM my_contacts";

Otteniamo una istanza in sola lettura dell’oggetto SQLiteDatabase:

SQLiteDatabase db = getReadableDatabase();

A questo punto possiamo eseguire la query:

Cursor resultset = db.rawQuery(stringQuery,null);

Se il database non è vuoto, ovviamente avremo resultset che conterrà una certa quantita di tuple di risultato. Quindi utilizzeremo un ciclo while per leggerle una ad una. In questo caso l’idea è creare un oggetto Notification per ogni tupla.

List<Contact> contacts = new ArrayList();
resultset.moveToFirst();
while(!resultset.isAfterLast()){
       Integer id = resultset.getString(resultset.getColumnIndex("id"));
       String name = resultset.getString(resultset.getColumnIndex("contact_name"));
       String number = resultset.getString(resultset.getColumnIndex("number"));
       Contact contact = new Contact(id, name, number);
       contacts.add(contact);
       resultset.moveToNext();
}
resultset.close();
return contact;

Grazie a isAfterLast siamo sicuri che il ciclo while uscirá al termine degli elementi che compongono il risultato. Inoltre è consigliato chiudere il cursor al termine del suo utilizzo. Di seguito è riportato il metodo completo:

public List<Contact> getAllContacts(){
       SQLiteDatabase db = getReadableDatabase();
       String stringQuery = "SELECT * FROM my_contacts";
       Cursor resultset = db.rawQuery(stringQuery,null);
       resultset.moveToFirst();
       List<Contact> contacts = new ArrayList();
       while(!resultset.isAfterLast()){
              Long id = resultset.getLong(resultset.getColumnIndex("id"));
              String name = resultset.getString(resultset.getColumnIndex("contact_name"));
              String number = resultset.getString(resultset.getColumnIndex(Columns.phone_number));
              Contact contact = new Contact(id, name, number);
              contacts.add(contact);
              resultset.moveToNext();
       }
       resultset.close();
       return contacts;
}

Esempio 2 – Selezionare un contatto dato il suo id

Vediamo ora la seconda modalitá per eseguire una query, in questo caso vogliamo selezionare un contatto conoscendo il suo id (chiaramente si tratta di un esempio, quindi questo ragionamento si applica anche ad altri campi come per esempio il nome).

La query che dovremo eseguire in questo caso è:

SELECT * FROM my_contacts WHERE id=XX

Dove XX è un numero che identifica una tupla. Partiamo dalla firma del metodo:

public Notification getNotificationById(int id){
       ...
}

In questo caso, dal momento che id è un identificatore univoco sappiamo che la query tornerá un numero di risultati fra 0 e 1, e quindi non abbiamo bisogno di utilizzare liste. la logica è molto simile a quella dell’esempio precedente, la differenza sta solo nel metodo da chiamare per eseguire la query.

La firma di questo metodo è:

public Cursor query (String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)

Dove:

  • table è la tabella dove effettueremo la selezione
  • columns è un array con la lista delle colonne che vogliamo selezionare, se passiamo null allora vuol dire che selezioniamo tutte le colonne.
  • selection è la clausola where, quindi in questo caso andremo a mettere id=XX. Se passiamo null vuol dire che selezioneremo tutti gli elementi. In alternativa possiamo per esempio mettere come selection id=? e specificare i valori nel parametro successivo.
  • selectionArgs è un array con i valori per il parametro selection, se sono stati sostituiti dal ?. Vengono elaborati in ordine di apparizione il primo ? interrogativo viene sostituito dal primo elemento dell’array e così via.
  • groupBy Se vogliamo aggiungere il groupBy (null indica che non lo vogliamo).
  • having come sopra, rappresenta la clausola having di sql, se non ci interessa passiamo null.
  • orderBy come sopra, rappresenta la clausola order by di sql se non ci interessa passiamo null
  • limit limita il numero di risultati. Null equivale a nessun limite

Quindi per il nostro esempio la chiamata sarebbe:

Cursor result = db.query("my_contacts", null, "id=?", new String[](Integer.toString(id), null, null, null, null);

La navigazione del cursor, avviene in maniera molto simile all’esempio 1, con la sola differenza che dal momento che ci aspettiamo un solo elemento tornato non avremo bisogno del while:

       if(cursor==null || cursor.getCount() == 0){
              return null;
       }
       cursor.moveToFirst();
       String id = cursor.getString(cursor.getColumnIndex("id"));
       String name = cursor.getString(cursor.getColumnIndex("contact_name"));
       String number = cursor.getString(cursor.getColumnIndex("number"));
       Contact contact = new Contact(id, name, number);
       return contact;

Il metodo completo sará:

public Notification getNotificationById(int id){
       SQLiteDatabase db = getReadableDatabase();
       Cursor result = db.query("my_contacts", null, "id=?", new String[](Integer.toString(id), null, null, null, null);
      if(cursor==null || cursor.getCount() == 0){
              return null;
       }
       cursor.moveToFirst();
       String id = cursor.getString(cursor.getColumnIndex("id"));
       String name = cursor.getString(cursor.getColumnIndex("contact_name"));
       String number = cursor.getString(cursor.getColumnIndex("number"));
       Contact contact = new Contact(id, name, number);
       return contact;
}

Bene con questo è tutto.

I sorgenti dell’esempio utilizzato per questa applicazione li trovate su github: https://github.com/inuyasha82/ItalialinuxExample

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.