Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 78 di 97
  • livello avanzato
Indice lezioni

Room: ORM su SQLite

Room si propone come libreria ufficiale per lo sviluppo di app Android che sfruttino la persistanza dei dati su DB SQLite, con un approccio ORM.
Room si propone come libreria ufficiale per lo sviluppo di app Android che sfruttino la persistanza dei dati su DB SQLite, con un approccio ORM.
Link copiato negli appunti

La libreria Room, introdotta con Android Architecture Components,
rappresenta una soluzione "ufficiale"
dedicata alla persistenza su Sqlite, caratterizzata da un approccio O/RM. Tecnicamente, si tratta di un
abstraction layer, uno strato software che permette di sfruttare tutte le potenzialità
del database senza la preoccupazione di affrontare ogni aspetto nei dettagli.

Room può essere utilizzato da solo in un'applicazione o integrato con ViewModel
e LiveData
per supportare l'interfaccia utente nella massima efficienza. In questa lezione, ne esploriamo
i principi fondamentali mettendo infine tutto in pratica con un esempio.

Integrare Room in un progetto

Per avere a disposizione Room, è necessario agire sulla configurazione del progetto Android,
accertandosi che:

    • il file build.gradle del progetto includa i repository di Google, mediante il metodo google():
      allprojects {
      repositories {
      jcenter()
      google()
      }
      }
    • nel file build.gradle siano incluse, a livello di modulo, le dipendenze necessarie:

dependencies{
...
...
implementation "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
}

Gerarchia delle componenti

In un'applicazione Room-based, le componenti da implementare possono essere classificate in tre tipologie, come illustrato dall'immagine seguente:

Figura 1. Componenti di Room (click per ingrandire)

Componenti di Room

In particolare:

  • Room Database: rappresenta l'astrazione del database su cui vogliamo lavorare.
    Verrà creata una classe di questo tipo che costituirà il centro di
    accesso ai dati: qualsiasi operazione da svolgere passerà di qui;
  • Entity: ogni classe di questo tipo rappresenta una tabella del
    database. All'interno delle classi Entity predisporremo tante variabili d'istanza quanti
    sono i campi previsti dallo schema della tabella, più altri eventuali membri che si renderanno
    necessari;
  • DAO (Data Access Object): un DAO viene utilizzato per incamerare
    il codice che agirà sui dati, e conterrà i veri e propri comandi per le
    operazioni CRUD. Tipicamente esistono più DAO in un'applicazione, ma non è necessario
    ve ne siano tanti quante le Entity. In genere, ogni DAO raccoglie tutte le interazioni che
    riguardano un sottosistema del software: se stiamo modellando, ad esempio, l'accesso
    ai dati di una biblioteca, potremo avere un DAO per gestire i dati utente, uno per il catalogo
    libri, uno per i prestiti e così via.

Come vedremo, la configurazione che attribuiremo ai suddetti elementi sarà molto
semplice e dovremo specificare solo ciò che Room non è in grado di "intuire" da solo (gli
aspetti personalizzati, per intenderci). Anche di SQL ne useremo ben poco. Infatti, i metodi
disponibili nei DAO lavorano direttamente su oggetti Java e riescono a capire l'operazione
da svolgere interpretando le annotazioni applicate: query e comandi espliciti
vengono scritti solo quando si renda necessario personalizzare il lavoro.

L'esempio

L'esempio che realizziamo utilizza Room per gestire un'applicazione in cui potremo
salvare delle note testuali. Ogni nota sarà composta dal semplice testo e da un numero intero
che svolgerà il ruolo di chiave primaria. Tale valore sarà autogenerato di volta in volta
dal sistema. Ogni oggetto inserito potrà essere modificato e cancellato e, come azioni generali,
supporteremo la possibilità di scegliere di caricare le note inserite (da mostrare in una ListView) o
cancellarle tutte.

Figura 2. L'app di esempio (click per ingrandire)

L'app di esempio

Il database

La classe che rappresenta il database prende il nome di DbManager:

@Database(entities = {Note.class}, version = 1)
public abstract class DbManager extends RoomDatabase {
private static DbManager INSTANCE;
public abstract NoteDao noteModel();
public static DbManager getInMemoryDatabase(Context context) {
if (INSTANCE == null) {
INSTANCE =
Room.inMemoryDatabaseBuilder(context.getApplicationContext(), DbManager.class)
.allowMainThreadQueries()
.build();
}
return INSTANCE;
}
public static DbManager getDatabase(Context context) {
if (INSTANCE == null) {
INSTANCE =
Room.databaseBuilder(context.getApplicationContext(), DbManager.class, "note_db")
.allowMainThreadQueries()
.build();
}
return INSTANCE;
}
public static void destroyInstance() {
INSTANCE = null;
}
}

Di seguito elenchiamo i tratti salienti che contradistinguono la classe appena vista (così come tutte le altre classi analoghe delle applicazioni basate su Room):

  • è annotata con @Database, che introduce l'elenco delle Entity che
    dovrà gestire (in questo caso solo Note) e la versione del database;
  • è astratta ed estende RoomDatabase;
  • contiene metodi astratti senza argomenti ognuno dei quali restituisce un DAO. In questo caso,
    ve ne è solo uno, noteModel(). Non sarà necessario fornire un'implementazione;
  • per facilitarne la gestione, la classe contiene un riferimento statico a sè stessa, denominato INSTANCE. Il database può essere gestito in maniera persistente (con salvataggio
    su disco) o in memory (ideale per realizzare cache e strutture dati di appoggio). Si sceglie
    l'una o l'altra a seconda che venga inizializzata con un DatabaseBuilder o un InMemoryDatabaseBuilder:
    noi, a scopo di esercizio, abbiamo predisposto metodi per entrambi i casi.

Ogni azione CRUD inizierà sempre richiedendo a questa classe un riferimento ad un DAO.

Classe Entity

L'unica Entity di cui abbiamo bisogno coincide con la classe Note, la rappresentazione
Java di una tabella a due colonne:

@Entity
public class Note {
@PrimaryKey(autoGenerate = true)
@NonNull
public long id;
public String text;
@Override
public String toString() {
return String.format("%s (id = %d)", text, id);
}
}

Ciò che rende questa classe un'Entity è l'annotazione @Entity. L'annotazione @PrimaryKey segnala che la
variabile d'istanza id è la chiave primaria ed è autogenerata (autoGenerate = true).

Il DAO

Con gli elementi definiti finora, abbiamo creato il livello di accesso al database
e gli oggetti che simboleggiano i singoli record da inserire in una tabella. Ciò
che manca è lo strato intermedio, il DAO. Creiamo a tale scopo l'interfaccia NoteDAO:

@Dao
public interface NoteDao {
@Insert(onConflict = REPLACE)
void insertNote(Note note);
@Query("SELECT * FROM Note")
List<Note> loadAllNotes();
@Query("DELETE FROM Note")
void deleteAll();
@Delete
void deleteNote(Note n);
}

In linea di massima, per creare un DAO con Room è necessario:

  • applicare l'annotazione @Dao;
  • creare una interface che definisca tutti i metodi di accesso ai dati. Sarà poi
    Room a fornire loro un'implementazione.

Per connotare i metodi, sono state applicate altrettante annotazioni. Alcune di
esse contengono codice SQL, altre sono auto-esaustive: un caso o l'altro si renderà
necessario a seconda di quanto dovremo personalizzare il comando.

Con @Insert, @Update e @Delete potremo, rispettivamente, inserire, modificare o
cancellare un oggetto Note: questo sarà direttamente tradotto nel relativo comando sul
database. Per effettuare interrogazioni o eseguire comandi di modifica personalizzati
passiamo direttamente il comando SQL. All'annotazione @Insert, è stata specificata la proprietà onConflict = REPLACE, che impone la sostituzione dei dati nel caso in cui la nota da inserire abbia una chiave primaria già presente nella tabella: in questo modo utilizziamo @Insert per effettuare sia inserimento che
modifica.

Implementiamo l'esempio

Per seguire una via semplificata, tutta la logica dell'esempio sarà inserita nell'Activity.
Ecco i punti cruciali dell'integrazione con Room.

Tra le variabili d'istanza, ne fissiamo due per nostra comodità: una conserva il riferimento al database
attuale (lo richiameremo tramite il metodo getDatabaseManager()) ed un'altra il riferimento
all'eventuale nota che stiamo modificando (in tutti gli altri casi, rimarrà null):

private DbManager db;
private Note currentNote;
private DbManager getDatabaseManager()
{
if (db==null)
db=DbManager.getInMemoryDatabase(this);
return db;
}

A scopo di esempio, abbiamo creato un database in memoria, ma si consideri che al termine dell'applicazione
non resterà traccia dei dati inseriti. Nel metodo onCreate() recuperiamo riferimenti ad elementi dell'interfaccia
utente ed impostiamo ArrayAdapter e ListView. Nell'OnItemClickListener a quest'ultima legato,
recuperiamo il riferimento all'oggetto Note selezionato e lo usiamo per compilare l'EditText:

public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Note n = adapter.getItem(position);
MainActivity.this.txt_note.setText(n.text);
currentNote = n;
}

Si noti che abbiamo impostato la variabile currentNote per sancire l'inizio della fase
di modifica: se al click del pulsante Salva questa verrà trovata nulla, si procederà ad un nuovo
inserimento. Segue il metodo save():

public void save(View view) {
if (txt_note.getText().length()==0) {
Toast.makeText(this, "Inserire del testo", Toast.LENGTH_SHORT).show();
// eventuale fase di modifica viene annullata
abortUpdate();
return;
}
if (currentNote==null)
currentNote = new Note();
currentNote.text = txt_note.getText().toString();
// inserimento o modifica? Dipende se la nota esiste già nella tabella
getDatabaseManager().noteModel().insertNote(currentNote);
abortUpdate();
// aggiorna ListView
updateList();
}

Il metodo abortUpdate() è stato predisposto al fine di avere un luogo unico
dove concentrare le operazioni per interrompere eventuali modifiche in corso (ad esempio, se iniziamo a modificare una nota e nel frattempo attiviamo un pulsante di
cancellazione):

private void abortUpdate()
{
currentNote = null;
txt_note.getText().clear();
}

Anche per la cancellazione recupereremo il riferimento alla nota da eliminare e ne
ordineremo la rimozione mediante il metodo deleteNote() offerto dal model:

public void deleteNote(View view) {
// annulliamo eventuali modifiche in corso
abortUpdate();
// cerchiamo la nota relativa alla riga selezionata
int position = list.getPositionForView(view);
Note n = adapter.getItem(position);
// cancelliamo la nota
getDatabaseManager().noteModel().deleteNote(n);
updateList();
}

Per correttezza, sarà infine importante invocare la chiusura del database in fase di
distruzione dell'Activity:

@Override
protected void onDestroy() {
DbManager.destroyInstance();
super.onDestroy();
}

Conclusioni

Con questa lezione, possiamo dire di aver appreso la gestione dei dati con Room. Ci sono però altre interessanti tematiche
da affrontare relativamente a questa libreria, quali la possibilità di gestire operazioni asincrone, l'uso di Converter per la conversione di dati e soprattutto
l'integrazione con ViewModel e LiveData per realizzare applicazioni moderne, efficienti ma al contempo robuste.

Ti consigliamo anche