2002-11-25 23:42:16
RMS - Record Management System (Sistema di gestione dei record) è un' API che
consente di effettuarela persistenza di dati all'interno di dispositivi MIDP.
Quest'articolo vuole essere una introduzionea questo tipo di tecnologia, cos'`
e come si usa.
Qualsiasi suggerimento e' gradito
Ogni applicazione che si rispetti, e le MIDlet non fanno eccezione, hanno bisogno di poter conservare dei dati. Purtroppo l' API MIDP 1.0 non consente di accedere a risorse quali i file system, quindi, per poter memorizzare le proprie informazioni bisogna ricorrere a qualcos'altro.
Per risolvere questo tipo di problema è stato introdotto l' RMS, cioè la gestione dei record a livello di MIDP.
L'API RMS consente la gestione di record sequenziali. E' un API in grado di fornire accesso a singoli record e di effettuare anche delle ricerche in base a filtri e a ordinamenti, ma non ha niente a che vedere con JDBC, infatti mentre JDBC ha l'obiettivo di consentire un collegamento a tutti i tipi di database relazionali, gestendo concorrenza e transazionalità, RMS ha come unico obiettivo quello di fornire un sistema di persistenza a livello di Suite Midlet.
L'RMS utilizza la memoria non volatile del dispositivo per memorizzare le informazioni. Le informazioni vengono memorizzate in una sorta di database "flat" paragonabile ad un foglio a righe numerate dove ad ogni riga è associato un identificativo unico gestito dal sistema paragonabile ad una "chiave primaria".
ID |
Array di byte |
1 | array di byte del Record 1 |
2 | array di byte del Record 2 |
... | .... |
n | array di byte del Record N |
Contrariamente a JDBC non è possibile definire tipi di campi per il record ma si ha a disposizione un array di byte su cui memorizzare qualsiasi tipo di informazione.
Questo database "flat" viene chiamato RecordStore.
Ogni Suite di MIDlet può creare zero o più record store ed ogni
record store può contenere zero o più record. Teoricamente non ci sono
limiti alle dimensioni di questi "database" tranne, ovviamente, la memoria
volatile messa a disposizione dal terminale MIDP (telefonino).
La vita dei record store è legata alla suite a cui appartiene.
Ogni volta che viene cancellata una suite tutti i record store (indipendentemente dalla
MIDlet che li ha creati) vengono perduti.
I MIDlet che fanno parte di una Suite possono accedere a tutti i recordStore creati dalle MIDlet facenti parti della Suite. Questo significa che il recordStore è un ottimo candidato per memorizzare le informazioni comuni alle applicazioni e per poter scambiare informazioni tra MIDlet. Ogni recordStore è identificato all'interno della suite da un nome univoco che può essere lungo al più 32 caratteri (16 caratteri Unicode) ed inoltre il nome è sensibile alle maiuscole/minuscole.
RecordStoreL'oggetto javax.microedition.rms.RecordStore
rappresenta il fulcro
su cui ruota tutta l'architettura RMS (In effetti è l'unica classe del package
javax.microedition.rms
). Questa classe metta a disposizione un'insieme
di metodi per poter operare sui singoli record. La classe javax.microedition.rms.RecordStore
non ha un costruttore e quindi per poter ottenere un oggetto di tipo RecordStore è necessario invocare un metodo statico della classe RecordStore.
public static RecordStore openRecordStore( String recordStoreName, boolean createIfNecessary) |
Questo metodo apre (e possibilmente crea) un record store associato con la Suite a cui appartiene la MIDlet. Il nome del recordStore da aprire viene passato come primo argomento, mentre il secondo argomento indica l'azione da intraprendere nel caso in cui il recordStore che si intende aprire non esista.
Altri metodi della classe RecordStore sono:
Classe javax.microedition.rms.RecordStore |
void closeRecordStore() |
static void deleteRecordStore(String recordStoreName) |
static String[] listRecordStores() |
int addRecord(byte data[], int offset, int numBytes) |
void deleteRecord(int recordID) |
byte[] getRecord(int recordID) |
int getRecord(int recordID, byte buffer[], int offset) |
int getRecordSize(int recordID) |
int getNextRecordID() |
int getNumRecord() |
|
int getVersion() |
|
int getSize() |
int getSizeAvailable() |
void addRecordListener(RecordListener listener) |
void removeRecordListener(RecordListener listener) |
Una volta aperto un recordStrore è possbile leggere un record attraverso il metodo
public int addRecord(byte data[], int offset, int numBytes ) |
che aggiunge una array di byte (data) in coda al recordStore (una sorta di append). Gli altri due parametri servono ad indicare il numero di byte (numBytes) che si intende copiare nel recordStore a partire della posizione indicata in offset. Dato che l'operazione tipica è quella di memorizzare una Stringa lo stralcio di codice necessario è il seguente:
|
E' necessario ricordarsi sempre che prima di poter inserire un record è necessario aprire il recordStore su cui si vuole inserire (pena il sollevamento di un'eccezione). Inoltre, dato che si vuole inserire una Stringa, il metodo getBytes()
restituisce l'array di byte di cui è formata la stringa (sembra messo a posta).
L'intero restituito dal metodo addRecord
è molto importante, quell'intero rappresenta l' ID assegnato al record appena inserito. E' molto importante conservare quel valore visto che l'unico modo per poter operare su quel record è attraveso il suo ID.
Supponiamo infatti di voler modificare il valore del record appena inserito. Il metodo per poter modificare il valore di un record è il seguente:
|
Questo metodo ha quasi gli stessi parametri del metodo addRecord in più è necessario l'ID del record su cui effettuare la modifica.
Una volta aperto un recordStore, inserito dei dati è necessario poterlo chiudere (ed eventualmente cancellare). Il metodo necessario alla chiusura di un recordStore è il metodo closeRecordStore()
, mentre il metodo per poter rimuovere definitivamente un recordStore è il metodo deleteRecordStore()
.
Mentre il metodo closeRecordStore
non è statico e quindi va invocato sul recordStore da chiudere, il metodo deleteRecordStore
lo è e va quindi indicato il nome del recordStore da cancellare. In effetti il metodo di rimozione non poteva che essere statico, altrimenti, dopo la rimozione, in che stato avremmo trovato la variabile assegnata al recordStore ?
Riepilogando vediamo uno stalcio di codice che apre un recordStore, inserisce qualche record, chiude e cancella il recordStore
|
In questo esempio è stata (ancora una volta) omessa tutta la gestione delle eccezioni. Basti ricordare che ogni metodo della classe RecordStore
può sollevare più eccezioni e che per operare su un recordStore (lettura, scrittura, modifica e cancellazione di record) il recordStore deve essere prima aperto (pena il sollevamento dell'eccezione RecordStoreNotOpenException
) .
Inoltre, nell'esempio, si è preceduto alla cancellazione fisica dell'intero recordStore. Non è assolutamente necessario farlo ogni volta altrimento che persistenza si otterrebbe ?
Passiamo ora alla lettura dei record presenti in un recordStore. E' chiaro che prima di poter leggere dei record è necessario che il recordStore sia aperto regolarmente e che ci siano record al suo interno :-)
Per poter leggere un record in una data posizione del recordStore, l' API RMS mette a disposizione il metodo
|
dove:
E' chiaro che la lunghezza del buffer che ospiterà il record deve essere abbastanza grande da contenere il record stesso, altrimenti verrà sollevata un'eccezione di tipo ArrayIndexOutOfBoundException
. Il numero restituito dal metodo addRecord()
rappresenta il numero di byte effettivamente letti dal recordStore.
Vediamo un esempio di come leggere tutti i record presenti in un recordStore utilizzando anche un piccolo trucco per evitare di cadere nell'errore di allocare un buffer non grande a sufficienza per contenere tutti i record. Per semplicità verrà tralasciata la gestione delle eccezioni.
|
Questo esempio di codice merita un pò di attenzione.
Per prima cosa è stato allocato un buffer di lunghezza 50 necessario per contenere i vari record del recordStore che di volta in volta verranno letti. Dato che non si può sapere a priori la lunghezza massima del più grande record memorizzato, il primo if
del ciclo for
serve proprio per verificare se la lunghezza occupato dall'i-esimo record è maggiore della lunghezza del buffer. In caso affermativo viene ri-allocato un nuovo buffer capace di memorizzare il record. Con questo metodo si è mostrato anche l'utilizzo del metodo getRecordSize(...)
Il ciclo for inizia da uno ed itera fino a che non si raggiunge (notare il <=) il numero di record memorizzato nel recordStore. Il metodo getNumRecords()
restituisce infatti il numero di record memorizzati nel recordStore. I record memorizzati hanno un indice che inizia da uno.
Il metodo getRecord(...)
interno al ciclo for ha un indice che indica la posizione del record da leggere (che si è detto deve partire da uno), il buffer dove memorizzare il record (che grazie alla if
precedente è grande abbastanza per contenere il record) e l' offset (interno a buffer) da dove iniziare a copiare il record. Il valore restituito rappresenta il numero di byte letti (cioè la lunghezza del record) e viene utilizzato per costruire una stringa (lunga da zero a len) che servirà per stampare il contenuto valore del record.
A questo punto una domanda sorge spontanea. Ma nel recordStore si possono memorizzare solo Stringhe? La risposta è sicuramente no, ma tutto ciò che è possibile registrare sotto forma di array di byte.
Se necessario, in un prossimo articolo, mostrerò una tecnica per memorizzare strutture dati più complesse (tecnica che ha a che fare più con J2SE che con J2ME), dipende da quanto interesse susciterà quest'articolo.
L'esempio mostrato per leggere tutti i record presenti in un recordStore (tramite il metodo getRecord
) va bene per letture semplici. L' API RMS mette a disposizione degli strumenti più potenti per poter iterare sui record, ordinarli e filtrarli.
La classe RecordEnumeration
fornisce i metodi per potersi muovere avanti ed indietro all'interno di un recordStore. L'impostazione di un enumeratore richiede solo qualche riga di codice, infatti l'esempio precedente, utilizzando l'enumeratore diventa:
|
La classe RecordEnumeration
mette a disposizione i seguenti metodi
Interfaccia RecordEnumeration |
int numRecords() |
void destroy() |
boolean hasPreviousElement() |
|
boolean isKeptUpdated() |
void keepUpdated(boolean keepUpdated) |
byte[] nextRecord() |
|
int previousRecordId() |
byte[] previousRecord() |
void rebuild() |
void reset() |
L'enumeratore conserva un indice interno del recordStore però è necessario porre attenzione al fatto che se una volta ottenuto un enumeratore, il recordStore viene modificato, l' enumeratore potrevve restituire risultati scorretti. Per risolvere questo problema la classe RecordEnumerator
mette a disposizione un metodo per poter "reindicizzare" il record in modo da tener conto delle modifiche apportate ma questa non è la sola possibilità messa a disposizione dall' RMS. Infatti per poter reindecizzare è possibile:
rebuild()
della classe RecordEnumeration
ogni volta che viene aggiornato, cancellato o aggiunto un record. Questo metodo funziona solo si è molto precisi e non vengono lasciati buchi Per poter configurare un listener è necessario creare un oggetto che implementi l'interfaccia RecordListener
ed implementare i suoi metodi. L'interfaccia RecordListener
espone i seguenti metodi:
Interfaccia RecordLister |
|
|
|
Se si osserva la firma del metodo enumerateRecord
della classe RecordStore
|
si osserva che questi prende in input tre parametri. In particolare
filter : E' un oggetto che implementa l'interfaccia RecordFilter e serve ad indicare quali record (secondo un filtro) devono far parte dell'enumeration
comparator : E' un oggetto che implementa l'interfaccia RecordComparator e server per poter definire una regola per ordinare i record
keepUpdated : Se true, l'enumeratore manterrà aggiornato l'enumeration in modo da riflettere ogni modifica che riguardi il recordStore. E' da usare con cautela dato che può impattare molto sulle performance. Se false l'enumeration non sarà aggiornata e potrebbe quindi restituire anche record che intanto sono stati cancellati o che sono stati aggiunti successivamente alla chiamata della creazione dell' enumeration.
Molto importanti sono i primi due parametri, infatti, grazie ad essi è possibile creare enumeration di record molto particolari. L'interfaccia RecordFilter
serve per poter specificare dei criteri di ricerca all'interno del recordStore, verranno mostrati solo quei record che soddisfano il filtro. L'interfaccia RecordComparator
serve ad ordinare i record in base a qualche criterio.
Vediamo alcuni esempi.
Supponiamo di avere un recordStore che contenga Stringhe e che si voglia ottenere tutti i record che inizino per la Stringa J2ME, il filtro necessario allo scopo sarà:
|
Questa classe deve essere prima inizializzata con un patter che verrà utilizzato per verificare se la stringa ricevuta in input al metodo matches() soddisfa o meno il filtro. Il codice per poter eseguire una ricerca con questo filtro è il seguente:
|
ESEMPIO 1 |
Supponiamo ora di voler ordinare le stringhe presenti nel database in ordine alfabetico. Per fare questo bisogna creare una classe che implementi l'interfaccia RecordComparator
e che sia in gradi di effetuare l'ordinamento tra due stringhe. La classe che segue fa al nostro caso:
|
Questa classe implementa l'unico metodo dell'interfaccia RecordComparator
e si occupa di convertire i due array di byte in stringhe e di effettuare poi un confronto tra le due strighe per poter decidere quale venga prima e quale venga dopo. Sarà l'implmentazione stessa del metodo enumerateRecords
a richiamare più volte questa classe per verificare (secondo un algoritmo di ordinamento interno) quale record vada prima dell'altro.
Un esempio di codice che esegua un ordinamento utilizzando questa classe è il seguente:
|
ESEMPIO 2 |
Notate come il codice di ESEMPIO 2 sia quasi identico a quello di ESEMPIO 1. In pratica quello che cambiano sono solo i parametri dati in pasto al metodo enumerateRecords
.
Chiaramente la fantasia non ha limiti e quindi provate ad immaginare una combinazione delle due cose, cioè una chiamata a metodo enumerateRecords
nella quale indicate sia un filtro che un ordinamento ...
La possibilità di poter effettuare degli ordinamenti e di poter impostare dei filtri è molto potente però è anche molto dispendiosa in termini di tempo ed il tempo è prezioso (specialmente per applicazioni che girano su un telefonino). Quindi il mio consiglio è quello di non abusarne e (proprio quando non se ne puo fare a meno) implmentare degli algoritmi "furbi". Facciamo un esempio.
Supponiamo di voler ordinare le stringhe di un recordStore in base ... alla lunghezza (visto che stiamo ordinando, inventiamoci algoritmi strani )
Vi propongo due implementazioni di una ipotetica classe LenghtComparator
.
class LengthComparator implements RecordComparator { } |
ESEMPIO 3A |
|
ESEMPIO 3B |
La differenza tra le due implementazioni è che se devo verificare la lunghezza di due record non è necessario convertire l'array di byte in Stringa e poi successivamente confrontare la lunghezza delle due stringhe. La creazione di una stringa in java è un'operazione molto dispendiosa. Provate a verificare i tempi necessari ad ordinare una decina di record utilizzando queste due implementazioni e vedrete quale delle due è più performante.
Con questo è terminato questo articolo sull' RMS. Qualsiasi commento e/o richiesta di approfondimento sarà molto gradita.
Grazie