INTRODUZIONE
Sviluppare un’applicazione basata su un database locale richiede la giusta attenzione nel configurare, progettare e gestire la base dati. In questo articolo vedremo come tutto questo può essere fatto con l’aiuto di un ORM per semplificare e ridurre il codice e il lavoro a carico del programmatore. Prima di iniziare è essenziale dare una definizione e capire cos’è un ORM.
ORM: DEFINIZIONE E CONCETTI BASE
In informatica l’Object-Relational Mapping (ORM) è una tecnica di programmazione che favorisce l’integrazione di sistemi software aderenti al paradigma della programmazione orientata agli oggetti con sistemi RDBMS.
Un prodotto ORM fornisce, mediante un’interfaccia orientata agli oggetti, tutti i servizi inerenti alla persistenza dei dati, astraendo nel contempo le caratteristiche implementative dello specifico RDBMS utilizzato. (fonte Wikipedia)
VANTAGGI
- Astrazione rispetto alla base dati. Le operazioni più comuni come quelle di CRUD (Create, Read, Update, Delete) verranno gestite attraverso gli oggetti del linguaggio di programmazione e non codice SQL.
Per operazioni che risultano complesse o richiedono prestazioni più elevate sarà comunque possibile utilizzare l’ORM per eseguirle in maniera nativa. - Portabilità. È possibile cambiare RDBMS (MySQL, SQLite,PostgreSQL,ecc…) senza cambiare l’implementazione della persistenza ma solo cambiando la configurazione dell’ORM.
A precisazione di quanto detto va ricordato che il cambio di RDBMS non è da considerarsi così “indolore” come proposto ma va sempre analizzato con cura. Un ORM, ad esempio, consente l’esecuzione di query in sql nativo che quindi possono comprometterne la portabilità.
- Riduzione del codice sorgente. Eseguendo al posto nostro l’interfacciamento con la base dati è chiaro che il programmatore viene alleggerito di una parte del carico di lavoro. Questa può risultare più o meno ampia a seconda dell’ORM utilizzato o a seconda di quanto si voglia far gestire l’interfacciamento al framework.
Questi ovviamente sono solo alcuni dei vantaggi offerti, i più importanti. Per correttezza va precisato che a vantaggi corrispondo svantaggi. Non sono dei veri e propri “danni collaterali” ma val la pena di menzionare quelli principali.
SVANTAGGI
- Controllo dei comandi eseguiti. In caso di oggetti complessi serve accuratamente progettare tutte le relazioni per non trovarsi nella situazione in cui, ad esempio, un semplice caricamento produca un elevato numero di query.
- Prestazioni. Come è ovvio intuire l’aggiunta del layer influisce sulle prestazioni di interrogazione e salvataggio in modo proporzionale agli oggetti in gioco.
Es: se lo use case prevede di caricare 10000 righe dal database si dovrà tener conto del normale tempo di caricamento generato dall’istruzione select più il tempo di creazione degli oggetti e avvaloramento delle relative proprietà
Quanto detto sopra non deve comunque spaventare o abbandonare l’idea di utilizzare un ORM in quanto vengono sempre offerte soluzioni per gestire al meglio qualsiasi situazione.
ANDROID: SCEGLIERE L’ORM ADATTO
Il primo passo importante è sicuramente quello di capire quale ORM è più adatto all’applicazione che stiamo realizzando. Pur svolgendo più o meno gli stessi compiti ogni ORM è specializzato meglio o peggio rispetto ad un altro in determinate funzioni.
Esistono ORM ottimizzati per le letture, per le scritture, active record o basati su DAO oppure bilanciati per gestire il maggior numero di situazioni possibili. Non è intenzione di questo articolo analizzare in profondità con bencmark approfonditi tutte le combinazioni per determinare quale sia migliore o peggiore.
Per chi avesse intenzione di approfondire, un’interessante applicazione che offre termini di comparazione completi è possibile trovarla a questo indirizzo. Anche senza installarla è possibile, scorrendo in fondo alla pagina, trovare tutte le tabelle e grafici dei test eseguiti. Riporto come esempio la tebella riassuntiva per avere una visione generale delle prestazioni.
Library | write(s) | read(s) | update(s) | delete(s) | write(c) | read(c) | update(c) | delete(c) | write(b) | read(b) | update(b) | delete(b) |
---|---|---|---|---|---|---|---|---|---|---|---|---|
ORMLite | 151 | 666 | 122 | 105 | 445 | 3836 | 857 | 811 | 1563 | 3426 | 724 | 728 |
SugarORM | 245 | 842 | 252 | 152 | 1402 | 4129 | 1467 | 1003 | 2204 | 4397 | 1702 | 1197 |
Freezer | 248 | 5430 | 240 | 4797 | 1337 | 78982 | 2221 | 22104 | 3255 | 134942 | 1887 | 29515 |
DBFlow | 97 | 757 | 459 | 186 | 360 | 3534 | 3124 | 1044 | 1129 | 4653 | 5204 | 1268 |
Requery | 87 | 1501 | 147 | 129 | 461 | 8057 | 861 | 802 | 1368 | 8002 | 886 | 763 |
Realm | 151 | 29 | 1079 | 723 | 698 | 688 | 19666 | 9180 | 1522 | 210 | 21129 | 10006 |
GreenDAO | 81 | 1238 | 117 | 97 | 357 | 5552 | 455 | 274 | 598 | 5905 | 504 | 315 |
ActiveAndroid | 3123 | 930 | 2293 | 2423 | 14671 | 4165 | 15958 | 13023 | 17213 | 4653 | 19303 | 14642 |
Sprinkles | 5766 | 1050 | 6364 | 605 | 25978 | 4334 | 65579 | 2428 | 27774 | 4526 | 37705 | 2519 |
Room | 131 | 699 | 170 | 109 | 562 | 3201 | 717 | 403 | 1330 | 3532 | 790 | 507 |
SQLite | 50 | 436 | 63 | 80 | 386 | 2155 | 192 | 284 | 1146 | 2313 | 213 | 318 |
I valori indicati rappresentano una media della durata delle operazioni in millisecondi. I caratteri racchiusi nelle parentesi indicano rispettivamente i test su casi semplici, complessi e bilanciati. Ovviamente questi sono dati di massima che non tengono conto di molte variabili che potrebbero influenzare in meglio o in peggio i risultati ma riescono a dare un’idea abbastanza chiara.
L’ORM che prenderemo in considerazione e andremo ad approfondire sarà Active Android.
PERCHÈ ACTIVE ANDROID
Di tutti gli ORM elencati ho deciso di prendere in cosiderazione Active Android per alcuni motivi.
Sicuramente non è il più veloce ( lettura e/o scrittura ) ma offre un buon compromesso che lo rende un’ottima soluzione per la maggior parte delle applicazioni.
Non è da sottovalutare poi la grande community nata attorno a questo progetto. Per qualsiasi problema viene fornita spesso una soluzione riducendo al minimo le situazioni in cui l’utente viene lasciato solo nel risolvere particolari problematiche.
APPLICAZIONE DI ESEMPIO
Tutto quanto verrà spiegato in seguito è possibile trovarlo e seguirlo passo passo nell’app di esempio da me realizzata a questo indirizzo.
CONFIGURAZIONE
Jar come libreria IN ANDROID STUDIO
- Scarichiamo il jar dell’ultima versione stabile da questo indirizzo.
- Copiamo il file nella directory lib del modulo che ci interessa ( generalmente il modulo ‘app’).
- Se nel build.gradle del modulo abbiamo già l’importazione dei file jar della directory lib (come indicato qui sotto) non dobbiamo fare altro e saltiamo al punto 5.
compile fileTree(include: ['*.jar'], dir: 'libs')
- Cliccliamo con il tasto destro del mouse sopra il jar copiato e scegliamo la voce ‘Add as Library…’.
- Se ci viene rischiesto da Android Studio eseguire la sincronizzazione del progetto.
GRADLE
- Aggiungere il repository nel build.gradle principale o del modulo
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
- Nel file build.gralde del modulo aggiungere la libreria
compile 'com.michaelpardo:activeandroid:XXX'
Dove andremo a sostituire XXX con la versione desiderata, 3.1.0-SNAPSHOT la più recente in questo momento.
- Se ci viene rischiesto da Android Studio eseguire la sincronizzazione del progetto.
Al termine di questi semplici passi siamo pronti per utilizzare AA* nella nostra applicazione.
*Utilizzaremo ora l’abbreviazione AA per indicare Active Android.
DEFINIZIONE DEL DATABASE
All’interno del file Manifest.xml dobbiamo definire i seguenti elementi:
AA_DB_NAME: rappresenta il nome del database
AA_DB_VERSION: versione corrente del database
Ecco un esempio:
<manifest ...> <application android:name="com.activeandroid.app.Application" ...> ... <meta-data android:name="AA_DB_NAME" android:value="demodb" /> <meta-data android:name="AA_DB_VERSION" android:value="1" /> </application> </manifest>
È importante notare che nel nome dell’applicazione deve essere indicato quello della Application class di AA.
Nel caso in cui disponessimo già di una custom Application class è sufficiente estendere quella di AA in questo modo:
public class MyApplication extends com.activeandroid.app.Application { ... }
Oppure se stiamo già estendendo dalla classe Application di una libreria è possibile utilizzare un’utility di AA:
public class MyApplication extends SomeLibraryApplication { @Override public void onCreate() { super.onCreate(); ActiveAndroid.initialize(this); } }
In entrambi gli ultimi 2 casi è importante ricordarsi di indicare correttamente il nome dell’applicazione che non sarà più quello di AA ma quello custom (nell’esempio <nome package completo>.MyApplication).
Per quanto riguarda la definizione delle tabelle, che vedramo più avanti come configurare, AA eseguirà uno scan automatico di tutte le classi che estendono com.activeandroid.Model
.
VELOCIZZARE IL L’AVVIO DELL’APPLICAZIONE
Possiamo velocizzare la fase di avvio della nostra applicazione in 2 modi. Il primo consiste nell’evitare che AA esegua lo scan di tutte le classi presenti per trovare quelle che estendono Model
e dichiararle nel file Manifest.xml con il tag AA_MODELS. Ecco un esempio:
<manifest ...> <application android:name="com.activeandroid.app.Application" ...> ... <meta-data android:name="AA_DB_NAME" android:value="demodb" /> <meta-data android:name="AA_DB_VERSION" android:value="1" /> <meta-data android:name="AA_MODELS" android:value="it.app.db.model.Articolo, it.app.db.model.Categoria, ecc..." /> </application> </manifest>
Nel secondo caso invece possiamo fare in modo che nel file Manifest.xml non compaia alcun riferimento ad AA ma andremo a dichiarare tutto nella nostra custom Application in questo modo:
@EApplication public class MyApplication extends Application { public void onCreate() { super.onCreate(); initActiveAndroid(); } private void initActiveAndroid() { Configuration dbConfiguration = new Configuration.Builder(this) .setDatabaseName("demodb") .setDatabaseVersion(1) .addModelClasses(Articolo.class, Categoria.class) .create(); ActiveAndroid.initialize(dbConfiguration); } }
MODELLARE IL DATABASE
Dopo aver visto come configurare nome e versione del database affrontiamo in dettaglio la creazione delle classi per la definizione delle tabelle. Come già accennato precedentemente ogni classe dovrà estenderecom.activeandroid.Model
per essere gestita da AA.
Vediamo subito con un esempio come 2 classi possono descrivere le rispettive tabelle:
@Table(name = "categorie") public class Categoria extends Model { @Column(name = "code") private String codice; @Column(name = "name") private String nome; ... } @Table(name = "articoli") public class Articolo extends Model { @Column(name = "name") private String nome; @Column(name = "description") private String descrizione; ... }
Come possiamo notare è tutto molto semplice e intuitivo. In questo caso abbiamo definito 2 tabelle (categorie e articoli) con rispettivamente 2 colonne, code-name e name-description. Il DDL risultate sarà quindi il seguente:
CREATE TABLE categorie (Id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT, name TEXT); CREATE TABLE articoli (Id INTEGER PRIMARY KEY AUTOINCREMENT, description TEXT, name TEXT);
RELAZIONI
Le relazioni dirette fra gli oggetti avvengono mediante la creazione della foreign key su database.
@Table(name = "articoli") public class Articolo extends Model { @Column(name = "name", index = true) private String nome; @Column(name = "description") private String descrizione; @Column(name = "categoria_id", onDelete = Column.ForeignKeyAction.CASCADE) private Categoria categoria; ... }
mentre quelle che riguardano più oggetti sono realizzate tramite un metodo di helper senza modificare il database.
@Table(name = "categorie") public class Categoria extends Model { @Column(name = "code") private String codice; @Column(name = "name") private String nome; public List getArticoli() { return getMany(Articolo.class, "categoria_id"); } ... }
Il DDL a questo punto risulterà il seguente:
CREATE TABLE categorie (Id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT, name TEXT); CREATE TABLE articoli (Id INTEGER PRIMARY KEY AUTOINCREMENT, categoria_id INTEGER REFERENCES categorie(Id) ON DELETE CASCADE ON UPDATE NO ACTION, description TEXT, name TEXT)
INDICI
Gli indici vengono configurati attraverso l’annotation @Column
aggiungendo index=true
per la singola colonna o con indexGroups={}
per indici su colonne multiple.
@Table(name = "articoli") public class Articolo extends Model { @Column(name = "name", index = true) private String nome; @Column(name = "description") private String descrizione; ... } @Table(name = "categorie") public class Categoria extends Model { @Column(name = "code", indexGroups = {"category_idx"}) private String codice; @Column(name = "name", indexGroups = {"category_idx"}) private String nome; ... }
Ecco il DDL relativo agli indici dell’esempio
CREATE INDEX index_articoli_name on articoli(name); CREATE INDEX index_categorie_category_idx on categorie(code, name);
TIPI DATI CUSTOM
AA gestisce di default molti tipi di dati ma a livello database il tutto si riduce al caricamento e salvataggio di tipi dati primitivi. Per gestire questa casistica, AA si avvale di Type Serializer, classi che si occupano di implementare la trasformazione di tipi dato complessi in tipi dato primitivi che AA può utilizzare per la persistenza.
Non intendo divulgarmi troppo sul funzionamento di queste classi perchè il wiki di AA ci fornisce un’ottima spiegazione a questo indirizzo. Nell’esempio possiamo notare come un tipo dato java.util.Date
venga gestito come un tipo dato java.lang.Long
utilizzando la sua rappresentazione in millisecondi per il salvataggio e viceversa per il caricamento.
Vediamo ora come viene effettuata la configurazione dei Type Serializer nella nostra applicazione. Possiamo registrarli in 2 diverse maniere, nel file Manifest.xml o nell’Application class in questo modo:
<meta-data android:name="AA_SERIALIZERS" android:value="com.activeandroid.serializer.BigDecimalSerializer" />
Configuration dbConfiguration = new Configuration.Builder(this) .setDatabaseName("demodb") .setDatabaseVersion(1) .addModelClasses(Articolo.class, Categoria.class) .addTypeSerializer(BigDecimalSerializer.class) .create(); ActiveAndroid.initialize(dbConfiguration);
CRUD
AA è un ORM Active Record, questo stà a significare che il CRUD verrà gestito dal modello stesso e non delegeto ad esempio ad un DAO o manager. Dopo aver dato una panoramica generale su tutto quello che riguarda la configurazione di AA vediamo come gestire la persistenza dei dati per quanto riguarda le operazioni il CRUD (Create, Read, Update, Delete).
// Creazione nuova categoria Categoria categoria = new Categoria(); categoria.setCodice("C1"); categoria.setNome("Categoria 1"); Long idCategoria = categoria.save(); // La save restituisce l'id del modello salvato // Caricamento categoria salvata Categoria categoriaEdit = Model.load(Categoria.class,idCategoria); // Modifica categoriaEdit.setNome("Nuovo nome Categoria 1"); Long idCategoriaEdit = categoriaEdit.save(); // Cancellazione con id Model.delete(Categoria.class,idCategoriaEdit); // oppure con l'oggetto Categoria categoriaDelete = Model.load(Categoria.class,idCategoriaEdit); categoriaDelete.delete(); // oppure tramite query new Delete().from(Categoria.class).where("Id = ?", idCategoriaEdit).execute();
QUERY
Per l’esecuzione di query più o meno complesse AA mette a disposizione alcune classi che aiutano l’utente nella loro costruzione. Personalmente non trovo che portino un aiuto che comporti riduzione di codice o semplificazione di costruzione ma vale la pena comunque riportare qualche esempio.
new Select().from(Categoria.class).count(); new Select().from(Articolo.class).as("art") .innerJoin(Categoria.class).as("cat").on("art.categoria_id = cat.id") .where("cat.codice = ?","01") .and("art.nome like ?", "scatola%") .orderBy("art.nome") .execute();
Nel primo caso abbiamo un esempio di query che esegue un count delle categorie presenti e restituisce un singolo valore di tipo int
che ne rappresenta il numero.
Il secondo caso, più strutturato, restituisce una lista di articoli ordinati per nome che rispettano le condizioni del blocco where.
È possibile notare in questo ultimo esempio quanto affermato prima. Il codice è addirittura aumentato rispetto alla scrittura di SQL standard. Il beneficio comunque stà tutto nell’ORM che invece di restituire un cursore pieno di array di valori ci restituisce una lista di oggetti pronti per l’uso.
BUILK INSERT
Un tema molto caldo nella gestione di un database in una applicazione è quello delle builk insert. La necessità cioè di dover inserire una grossa quantità di dati e di doverlo fare nel minor tempo possibile. Il caso più comune è quello di dover importare dati da una fonte esterna (es. un servizio REST) o un file scaricato nel proprio database.
Perchè utilizzare gli oggetti non è la soluzione corretta? La risposta è molto semplice. Ogni salvataggio del modello effettuato da AA è transazionale. Questo significa che prima e dopo il salvataggio di ogni singolo oggetto viene aperta e chiusa una transazione e il costo dell’operazione è altissimo.
Nella figura qui accanto possiamo verificare le performace sull’esecuzione di insert eseguite con una transazione per ogni salvataggio (Transazioni multiple) e con una singla transazione. Il risultato ci mostra inequivocabilmente quanto la transazione singola incrementi le performance. Nell’inserimento di 50000 articoli si passa da 83 secondi a ben 12 secondi ottenendo un incremento delle performance dell’85%. Ecco in che modo sono stati implementati rispettivamente i blocchi di insert.
// Transazioni multiple for (Articolo articolo : articoli) { articolo.save(); } // Transazione singola ActiveAndroid.beginTransaction(); try { for (Articolo articolo : articoli) { articolo.save(); } ActiveAndroid.setTransactionSuccessful(); } finally { ActiveAndroid.endTransaction(); }
MIGRATION
Dedichiamo un piccolo paragrafo anche per dare un spiegazione di come aggiornare il nostro database al cambio della versione. Il primo passo è quello di incrementare il numero dell’attributo AA_DB_VERSION
(per chi usa il file Manifest.xml) o il valore della configurazione nella classe Application.
A questo punto per i nuovi modelli, se dichiarati nel Manifest.xml, AA li caricherà automaticamente, nel caso invece della definizione nella classe Application dovremo aggiungerli alla lista di quelli presenti. Tutte i nuovi modelli aggiunti verranno presi in carico da AA che genererà le relative tabelle senza alcun intervento da parte nostra.
Per quanto riguarda la variazione di colonne già esistenti sarà sufficiente creare un file chiamato <numero versione>.sql
che contiene tutte le istruzione per eseguire l’update e inserirlo nella directory assets/migrations
. In questo modo all’avvio dell’applicazione AA applicherà tutti i file con nome maggiore delle vecchia versione e minore-uguale alla nuova versione.
Ecco un esempio pratico per capire meglio il funzionamento. Poniamo il caso di passare dalla versione 1 alla versione 3. Nella directory di migrazione creaiamo i seguenti file:
2.sql (conterrà tutte le modifiche necessarie per allineare il database dalla versione 1 alla versione 2)
ALTER TABLE ARTICOLI ADD COLUMN note TEXT;
3.sql (conterrà tutte le modifiche necessarie per allineare il database dalla versione 2 alla versione 3)
ALTER TABLE ARTICOLO ADD COLUMN numeroDecimali INTEGER;
AA applicherà automaticamente tutte le istruzioni presenti.
PRE POPULATED DATABASE
Il supporto a database pre-populated è gestito da AA seguendo poche semplici regole. Innanzitutto dobbiamo inserire una copia del database nella directory /app/src/main/assets/
facendo attenzione che il nome corrisponda a quello configurato nel file Manifest.xml e nella Application class.
Nel caso del nostro esempio il nome del database configurato è demodb quindi il risultato sarà il seguente:
/app/src/main/assets/demodb
In questo modo, all’avvio dell’applicazione, se non è presente nessun database verrà copiato quello nella directory assets nella directory /data/data/<package+nomeapp>/databases
.
La seconda regola molto importante è quella di assicurarsi che la tabella android_metadata
sia presente inizialmente nel database o creandola all’avvio dell’applicazione con le query
CREATE TABLE "android_metadata" ("locale" TEXT DEFAULT 'en_US'); INSERT INTO "android_metadata" VALUES ('en_US');
L’ultimo controllo riguarda il nome della colonna utilizzata come Primary Key. AA utilizza come default il nome “Id
“. Nel caso in cui questa condizione non fosse rispettata nel database possiamo specificare il nome della colonna attraverso la proprietà id
dell’annotation Table
. Ecco l’esempio in cui il database utilizzi la colonna mioId
come primary key:
@Table(name = "articoli", id = "mioId") public class Articolo extends Model { ... }
MULTI DATABASE
La gestione di più database non è supportata ufficialmente da AA ma esistono molti workaround proposti dalla comunità e quello che ho deciso di adottare è il seguente.
La lista dei database presenti può essere gestita in vari modi come:
- salvataggio dei nomi nelle
shared preference
dell’applicazione (es in una stringa con separatori) - una tabella nel database principale dell’app
- caricamento della lista dei nomi dei files della directory
/data/data/<package+nomeapp>/databases
Scelto il metodo che più ci piace, tramite un Factory, dal nome del database ricaviamo la classe che lo descrive. Infine tramite quest’ultima possiamo selezionare il database corrente con una classe di utility.
IMPLEMENTAZIONE
Quanto appena spiegato più risultare poco chiaro ma possiamo basarci sull’applicazione di esempio per capire meglio il funzionamento.
L’interfaccia DatabaseStructure definisce il database (nome e modelli) e le sue implementazioni sono FilmDatabaseStructure e LibreriaDatabaseStructure.
DatabaseStructureFactory ricava la struttura del database attraverso il suo nome con il metodo getStructure
Infine il singleton DatabaseUtils inizializza il database definito dall’implementazione dell’interfaccia fornita.
LIMITAZIONI
Con questo, e qualsiasi altro, workaround è importante ricordare AA avrà solamente un database attivo, l’ultimo inizializzato con ActiveAndroid.initialize() e quindi non sarà possibile accedere a dati relativi ad altri cataloghi.
CONCLUSIONI
In questo articolo è stato analizzato in dettaglio l’utilizzo di un ORM come Active Android per la gestione di un database SQLite in una applicazine Android. Come già accennato non è l’unico disponibile ma senza dubbio è quello che personalmente più raccomando a chi per la prima volta intende utilizzarne uno o a chi vuole essere sicuro di poter contare su una grossa community che dia una risposta per qualunque dubbio o problema si possa incontrare.
La sua grande flessibilità lo rende utilizzabile sulla quasi totalità delle applicazioni senza penalizzare troppo le prestazioni.
E’ bene ricordare che tutte le operazioni sui dati è opportuno eseguirle sempre in background e, a differenza di altri suoi competitor, AA non impedisce in alcun modo (ad esempio sollevando una eccezione) di non farlo.
Nonostante questa scelta AA fornisce valide soluzioni per tutte le situazioni che un programmatore potrebbe trovarsi ad affrontare durante lo sviluppo.
Considerato quanto spiegato e analizzato in questo articolo, riuscireste ancora a farne a meno?
RIFERIMENTI
Potere trovare tutti punti analizzati nell’app di esempio da me realizzata sul mio account di GitHub.
Active Android: progetto GitHub
Active Android: wiki