...

04_Persistence

by user

on
Category: Documents
8

views

Report

Comments

Transcript

04_Persistence
Mantenere dati persistenti un un DB
Docente: Gabriele Lombardi
[email protected]
© 2012 - CEFRIEL
The present original document was produced by CEFRIEL and the Teacher for the benefit and internal use of this
course, and nobody else may claim any right or paternity on it. No right to use the document for any purpose other than
the Intended purpose and no right to distribute, disclose, release, furnish or disseminate it or a part of it in any way or
form to anyone without the prior express written consent of CEFRIEL and the Teacher.
© copyright Cefriel and the Teacher-Milan-Italy-23/06/2008. All rights reserved in accordance with rule of law and
international agreements.
© 2012 - CEFRIEL
Sommario
SLIDE
CONTENUTO
SQLite
Da riga di comando, tools in Android
Lavorare sul DB
DDL e operazioni CRUD
App di esempio
Creiamo un ricettario con persistenza su DB
ContentProvider
Dati a disposizione delle app… in sicurezza
ORM ed Entities
Object Relational Mapping e nostra lib ORM
Hadi
Tutto in Java… al DB ci pensa qualcun altro
© 2012 - CEFRIEL
Cos’è SQLite

Citando http://www.sqlite.org/:
– «SQLite is a software library that implements a selfcontained, serverless, zero-configuration,
transactional SQL database engine.»

Caratteristiche:
–
–
–
–
–
–

lavora su un file singolo per ogni db;
ha un’impronta contenutissima (completo in Android);
supporta la maggior parte dello standard SQL92;
molto più veloce per molte operazioni;
è supportato nativamente in Android;
…
Particolarità:
– tipi di dato suggeriti per le colonne, non obbligatori.
© 2012 - CEFRIEL
Usiamolo da riga di comando

accediamo alla shell (dell’emulatore in esecuzione):
– adb shell

creiamo un nuovo database (che elimineremo):
– sqlite3 prova.db

creiamo una tabella:
– create table note(_id integer primary key autoincrement,
content text not null);

elenchiamo le tabelle e vediamone lo schema:
– .tables
– .schema note

inseriamo un dato di esempio:
– insert into note(content) values('una nota di esempio');

vediamo lo in HTML (provare anche cvs, tabs, …):
– .mode html
– select * from note;

dump di tutto il db come istruzioni SQL:
– .output myDump
– .dump
– .output stdout

fine sessione:
– .exit
© 2012 - CEFRIEL
In Android…

Il Framework ci offre alcuni strumenti:
–
–
–
–

gestione delle versioni del DB (per gli aggiornamenti);
wrapping in Java delle API;
astrazione dei cursori;
composizione di query e DDL con metodi comodi.
Per l’interfaccia utente:
– adattatori dedicati alla fruizione di dati da DB.

Per l’accesso ai dati:
– la possibilità di realizzare ContentProvider:
• accedere ai dati da altre applicazioni;
• offrire un nuovo tipo di content URI;
– accesso locale nella nostra app (primo esempio).
© 2012 - CEFRIEL
…cosa manca?

ORM:
– esistono dei progetti a riguardo:
–
–
–
–
http://ormlite.com/  reflection su annotazioni stile JPA
http://hadi.sourceforge.net/  come sopra ma più JPA-oso
http://greendao-orm.com/
 code generation
http://code.google.com/p/hiberdroid/  Hibernate per Android
– più altri framework generici per l’ORM:
– http://www.avaje.org/
– http://cayenne.apache.org/
– ma niente ancora di standard;
– spesso si tende a fare ORM «a manina».

Cosa vedremo noi?
– Utilizzo «piatto» del DB;
– ORM «a manina» con uno strumento nostro;
© 2012
- CEFRIEL su Android.
– hadi come esempio di
ORM
Qualcuno ci da una mano

SQLiteOpenHelper:
– classe da estendere per accedere a un DB;
– ci offre dei metodi per la «connesione» al DB;
– invoca i nostri metodi per il mantenimento:
• onCreate:
– DB mancante, prima esecuzione, creazione schema;
• onUpgrade:
– se cambia la versione vorremmo aggiornare lo schema.

Gestore del DB:
– la nostra app accederà al DB da diverse activity:
• serve un gestore centralizzato del DB;
– gestisce l’accesso al DB nascondendo i dettagli;
– offre le operazioni che ci interessa svolgere sul DB.
© 2012 - CEFRIEL
Un nostro DbManager
private class RecipesHelper extends SQLiteOpenHelper {
public static final String RECIPES_TABLE_CREATE = …;
public static final String RECIPES_TABLE_DROP = …;
…
public RecipesHelper(Context context, String name,
CursorFactory factory, int version) {
super(context, name, factory, version); }
@Override public void onCreate(SQLiteDatabase db) {
db.execSQL(INGREDIENTS_TABLE_CREATE);
db.execSQL(RECIPES_TABLE_CREATE);
db.execSQL(RECIPES_INGREDIENTS_TABLE_CREATE); }
@Override public void onUpgrade(SQLiteDatabase db,
int oldVersion, int newVersion) {
db.execSQL(RECIPES_INGREDIENTS_TABLE_DROP);
db.execSQL(RECIPES_TABLE_DROP);
db.execSQL(INGREDIENTS_TABLE_DROP);
onCreate(db); } }
© 2012 - CEFRIEL
DbManager: offrire funzionalità
private RecipesHelper helper;
private SQLiteDatabase db = null;
public DbManager(Context ctx) {
helper = new RecipesHelper(ctx,
RECIPES_DB_NAME, null, RECIPES_DB_VERSION);
}
public synchronized void open() {
if (db==null)
db = helper.getWritableDatabase();
if (!db.isReadOnly()) db.execSQL("PRAGMA foreign_keys=ON;");
}
public synchronized void close() {
if (db!=null) { db.close(); db = null; }
}
private void checkDb() {
if (db==null) throw new IllegalStateException("DB non opened.");
}
© 2012 - CEFRIEL
DbManager: offrire funzionalità
public synchronized Cursor getRecipeDescriptions() {
checkDb();
return db.query(RECIPES_TABLE,
new String[] {RECIPES_ID, RECIPES_NAME, RECIPES_DESC},
null, null, null, null, RECIPES_NAME);
Volendo limit
}
Tabella a cui
accedere
WHERE e
argomenti
GROUP BY
e HAVING
ORDER BY
Campi della
proiezione
Argomenti gestiti
in stile JDBC
Cursor c = db.rawQuery("SELECT " + RECIPES_NAME + …
" WHERE " + RECIPES_ID + "=?", new String[] {Long.toString(id)});
db.insert(INGREDIENTS_TABLE, null, ingredient2content(ingredient));
db.replace(INGREDIENTS_TABLE, null, ingredient2content(ingredient));
db.delete(RECIPES_TABLE, RECIPES_ID + "=?",
Operazioni CRUD
new String[] {Long.toString(id)});
ContentValues values = new ContentValues(); Dati come ContentValues
values.put(RECIPES_INGREDIENTS_RECIPE, recipe.get_id());
values.put(RECIPES_INGREDIENTS_INGREDIENT, ingredient.get_id());
values.put(RECIPES_INGREDIENTS_QTA, ingredient.getQta());
© 2012 - CEFRIEL
SQLiteQuery e SQLiteQueryBuilder

Dinamicità nella creazione di query?
– Query dalla struttura dinamica possono esser create:
• per concatenazione di stringhe:
– rende difficile la produzione di statement validi se parti diverse
dell’app devono «metterci il proprio» (parti differenti);

Soluzione alternativa:
– builder per la query a cui dichiarare ciò che si vuole;
– aggiunta programmatica di parti di query;

Altre soluzioni:
– framework esterni che offrono criteria-query:
– http://code.google.com/p/infinitum-framework/
– http://code.google.com/p/hiberdroid/
© 2012 - CEFRIEL
Transazioni

A volte una sequenza di più operazioni su un DB
deve avere le seguenti caratteristiche:
• Atomiche:
come fossero eseguite
istantaneamente
• Consistenti: con i vincoli imposti dal DB
• Isolate:
• Durature:

(relazioni, vincoli)
se concorrenti non
interferiscono
modifiche con successo sono
permanenti anche a crash
In Android usiamo il seguente pattern:
db.beginTransaction(); try {
… // Operazioni su DB che possono fallire.
db.setTransactionSuccessful(); // Segno la transazione come buona.
} catch (Exception e) { … }
finally {
db.endTransaction(); // Fine transazione.
}
© 2012 - CEFRIEL
Rendiamo il nostro DbManager accessibile

Come gestire qualcosa di «globale» in una app?
– Le attività vanno e vengono, scomodo usare lo stato;
– i servizi possono sopravvivere indefinitamente…
– … ma comunicare con essi non è particolarmente agile;
– attributi statici pubblici (o con getter e laziness)
possono aiutare ad affrontare il problema…
– …ma offrono in realtà un modello «sporco» di lavoro.

Il contesto… non è sono l’attività:
– il concetto di contesto è più ampio di quanto si pensi;
– l’app possiede un contesto indipendente dalle attività;
– creiamo la nostra classe applicazione:
• potremo personalizzarne il ciclo di vita;
• offriremo il DbManager mantenuto in vita.
© 2012 - CEFRIEL
Una applicazione… nostra!
public class RecipesApplication extends Application {
private DbManager dbManager = null;
public DbManager getDbManager() {
if (dbManager==null) {
dbManager = new DbManager(this);
dbManager.open();
}
return dbManager;
Attenti a non sfruttare il metodo
}
onTerminate… serve solo in emulazione
}
Specificando name nel manifest
specifichiamo anche la classe
<activity android:name=".RecipesActivity"
android:label="@string/app_name" android:launchMode="singleTop">
private RecipesApplication app;
app = ((RecipesApplication)getApplication());
app.getDbManager().getRecipeDescriptions()
© 2012 - CEFRIEL
Dichiarata e ottenuta nel
metodo onCreate…
…offre accesso ai dati!

Un ricettario sul telefono…
Vogliamo gestire un ricettario su Android…
– schema del (semplice) DB (che estenderemo):
name
recipes
desc
Una ricetta molti ingredienti
preparation
recipes_ingredients
ingredients
name
qta
Un ingrediente molte ricette
desc
CREATE TABLE ingredients(_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE
NOT NULL, desc TEXT NOT NULL);
CREATE TABLE recipes(_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT
NULL, desc TEXT NOT NULL,preparation TEXT NOT NULL);
CREATE TABLE recipes_ingredients(recipe_id INTEGER NOT NULL REFERENCES recipes ON
DELETE CASCADE, ingredient_id INTEGER NOT NULL REFERENCES ingredients,qta TEXT
,PRIMARY KEY (recipe_id, ingredient_id));
© 2012 - CEFRIEL
Progettazione dell’interfaccia

RecipesActivity:
Nome ricetta
– attività principale,
elenca le attività;

Descrizione breve
RecipeDetailsActivity:
– consultazione ed
eventuale editing di
una ricetta;

Nome ricetta
Descrizione breve
Preparazione
dettagliata
IngredientsActivity:
– gestione degli
ingredienti, creazione,
modifica.
Nome ingrediente
Descrizione breve
© 2012 - CEFRIEL
Ingrediente
Quantità
Descrizione
Casi d’uso e sequenze

Consultazione ricette:
– L’utente può leggere la lista di ricette, accedendo al
menu po’ crearne una nuova, o selezionarne una
esistente per consultarla (modalità di lettura).
– L’attività di consultazione mostra i dettagli della ricetta
in sola lettura, il menu consente di passare alla
modalità di enditing (automatica per ricette nuove).

Editing di una ricetta nuova o vecchia:
– I campi di testo divengono modificabili, il menu
mostra voci in più (salvataggio, aggiunta ingrediente).
– Se non si salva si mantiene una copia temporanea.
– Dal menu si passa da una modalità all’altra.

Editing degli ingredienti:
– Possono essere creati, eliminati, modificati da una
lista tramite un menu contestuale.
© 2012 - CEFRIEL
Anche l’aspetto ha il suo valore… 9 (patches)

Aspetto accattivante della ricetta consultata:
– campi di testo come su carta con una piega;
– aspetto normale in modalità di editing
– vogliamo una sola immagine per tutte e tre i campi.

Disegniamo una nine-patch:
– prepariamo un’immagine png della carta;
– usiamo draw9patch per definire la patch.

Utilizziamo un drawable XML (selector):
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/selector_paper">
<!-- Stato associato al disabilitato: -->
<item android:state_enabled="false"
android:drawable="@drawable/paper_img"/>
<!-- Stato di default mantenuto negli altri casi: -->
<item android:state_enabled="true"
android:drawable="@android:drawable/edit_text«/>
</selector>
© 2012 - CEFRIEL
Offriamo accesso ai dati come ContentProvider

Vogliamo rendere le nostre ricette disponibili…
–
–
–
–

…anche ad altre applicazioni (altri processi);
tramite cursori direttamente dal DB;
mantenendo controllo sui permessi di scrittura;
mantenendo indipendenza tra l’applicazione che offre
i contenuti e quella che ne usufruisce.
Soluzione: implementare un ContentProvider:
– ogni ricetta viene mappata con un uri «content://…»;
– devono essere noti (oltre l’uri) i nomi delle colonne:
– non obbligatoriamente, solo se si intende proiettare;
– spesso si utilizza una classe contratto contenente le costanti;
– un ContentResolver permette poi di accedere ai dati:
– inconsapevolmente rispetto alla sorgente li ha prodotti.
© 2012 - CEFRIEL
Un po’ di configurazione
Nome «logico» e
nome «fisico»
<provider android:name=
"com.gab.tests.android.recipes.model.RecipesContentProvider"
android:authorities="com.gab.tests.android.recipes.provider"
android:writePermission=
"com.gab.tests.android.recipes.WRITE_RECIPES"
android:label="Recipes content provider"
Il permesso in scrittura va
android:icon="@drawable/ic_launcher"/>
garantito per operazioni
diverse da «query»
private static final int RECIPES = 1;
Scegliamo il tipo
private static final String PATH = "com.gab.tests.android.recipes";
private static final String AUTHORITY = PATH + ".provider";
static final String RECIPES_CONTENT_TYPE = "vnd.android.cursor.dir/vnd." +
PATH + ".provider." + DbManager.RECIPES_TABLE;
static final Uri RECIPES_CONTENT_URI = Uri.parse(
"content://" + AUTHORITY + "/" + DbManager.RECIPES_TABLE);
private static final UriMatcher uriMatcher;
Scegliamo l’uri
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, DbManager.RECIPES_TABLE, RECIPES);
}
© 2012 - CEFRIEL
Inizializzazione, tipo, operazioni
Ci serve il riferimento
all’applicazione
private RecipesApplication app;
@Override public boolean onCreate() {
app = (RecipesApplication)getContext().getApplicationContext();
return true; }
Dobbiamo verificare gli
uri che ci arrivano
@Override public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case RECIPES: return RECIPES_CONTENT_TYPE;
default: throw new IllegalArgumentException("Unknown URI "); } }
@Override public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); Query costruita con
switch (uriMatcher.match(uri)) {
un QueryBuilder
case RECIPES:
qb.setTables(DbManager.RECIPES_TABLE);
qb.setProjectionMap(projectionMap); break;
default: throw new IllegalArgumentException("Unknown URI ");
}
Cursor c = qb.query(app.getDbManager().getDb(), projection, Osserviamo l’uri
selection, selectionArgs, null, null, sortOrder);
in caso di
c.setNotificationUri(getContext().getContentResolver(), uri);
cambiamento
return c; }
© 2012 - CEFRIEL
Una applicazione «client»

Per verificare il nostro provider dobbiamo:
– creare una applicazione separata che lo utilizzi;
– mostriamo (banalmente) in una ListView le ricette.
– Proviamo assieme a modificare il «client» per sfogliare
solamente le ricette «as they are» (senza ingredienti).
– Proviamo a offrire/mostrare anche gli ingredienti.
@Override protected Cursor doInBackground(Void... params) {
ContentResolver resolver = getContentResolver();
Uri uri = Uri.parse("content://com.gab.tests.android.recipes.provider/recipes");
return resolver.query(uri, columns, null, null, "name");
}
@Override protected void onPostExecute(Cursor result) {
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
RecipesClientActivity.this, android.R.layout.two_line_list_item,
result, columns, new int[] { 0, android.R.id.text1, android.R.id.text2 });
setListAdapter(adapter); // Aggiornamento della lista (nel main thread).
}
© 2012 - CEFRIEL
Offriamo l’attività di consultazione

Come per le call… anche la nostra app può:
– registrarsi per un uri custom  recipe:<id>
– mostrarsi come strumento di consultazione;
– venire interpellata in maniera trasparente.
Uri uri = intent.getData(); // Ottengo l'uri e lo parso.
if (uri!=null && "recipe".equals(uri.getScheme())) {
String id = uri.getSchemeSpecificPart(); // Estraggo l'id in un uri del tipo "recipe:<id>".
try { // Carico la ricetta se possibile:
recipe = app.getDbManager().getRecipe(Long.parseLong(id));
canEdit = false; // Mi metto in nodalità solo consulto.
} catch (Exception e) { throw new IllegalArgumentException(“…"); }
}
onCreate
<activity android:name=".RecipeDetailsActivity" android:screenOrientation="landscape">
<intent-filter >
Da dichiarare
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="recipe"/>
</intent-filter>
In un’altra app…
</activity>
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("recipe:" + id)));
© 2012 - CEFRIEL
Chi cerca trova

Android offre una funzionalità di ricerca:
– a volte con un tasto fisico sul dispositivo;
– noi possiamo offrire sempre un tool di avvio ricerca.

Per abilitarlo:
– lo dichiariamo nel manifest;
– ne catturiamo la richiesta come intent.
<activity android:name=".RecipesClientActivity" …>
…
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/app_name" android:hint="@string/search_hint"/>
Intent intent = getIntent();
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
// Questa stringa la useremo nella query verso il ContentResolver:
query = intent.getStringExtra(SearchManager.QUERY);
}
© 2012 - CEFRIEL
Cos’è l’ORM?

ORM  Object Relational Mapping:
– lavoriamo sempre a oggetti nella nostra app;
– qualcun altro si preoccupa di fare traduzione tra
modello di dati OO e modello di dati relazionale;
– auto-magicamente i dati divengono persistenti!
Modello a oggetti
Objectrelational
mapper
Database
relazionale
Mapping tra
oggetti e relazioni
© 2012 - CEFRIEL

Un nostro Object Relational Mapper
Solo un cenno di architettura, per dare l’idea:
public abstract class Entity {
protected abstract String getTableName();
protected abstract String[] getFieldNames();
public void createTable() throws Exception {
StringBuilder sb = new StringBuilder( "CREATE TABLE " + getTableName() +
"(_id integer primary key autoincrement");
for (String name: getFieldNames()) {
Class type = getClass().getField(name).getType();
String typeName = "text";
if (type.equals(Integer.class)) typeName = integer;
…
sb.appen("," + name + " " + typeName + "NOT NULL");
}
getDb().rawQuery(sb.toString() + ");");
}
public Entity byId(long id) {
// Query tramite concatenazione.
// Istanziazione e assegnamento valori tramite reflection.
}…
}
© 2012 - CEFRIEL
Proviamo ad utilizzare Hadi

In Hadi vengono utilizzate le annotazioni:
– porzioni di informazione «annotate» sul codice;
– proviamo anche in un nostro ORMapper!
@Table(name="Hello")
public class Book{
@Column(autoincrement=true)
public int id;
@Column(name="sn")
public String sn;
@Column(name = "")
public String name;
}
DefaultDAO dao = new DefaultDAO(this);
Book b1 = new Book();
b1.name = "Who Moved My Cheese";
b1.sn = "sn123456789";
dao.insert(b1);
b1.sn = "sn987654321";
dao.update_by_primary(b1);
String[] args = {"0"};
List<Book> books =
(List<Book>)dao.select(Book.class, false,
" id > ?", args, null, null, null, null);
b1.id = 1;
dao.delete_by_primary(b1);
© 2012 - CEFRIEL
Da qui?

Esiste anche il lato oscuro della persistenza:
– accesso diretto a file su telefono o SD;
– utilizzo di SharedPreferences;
– utilizzo di file classici con java.io.

La persistenza non è tutto nella vita:
– i dati potrebbero provenire da altre fonti:
• internet e servizi HTTP (RESTful);
• servizi di localizzazione;
• sensori di vario tipo.
– spesso si usano strumenti misti, ad esempio se volessimo
aggiungere un’immagine per ricetta salveremmo un URI,
mantenendo l’immagine su SD.

Considerare i tempi di accesso:
– quanto fatto dovrebbe venir modificato per lavorare in
differita da un thread diverso da quello principale.
© 2012 - CEFRIEL
Fly UP