Almacenamiento Persistente de Datos
Versión: 1.0, Septiembre, 2003

Autor: Luis Javier Herrera Maldonado
Mail: jherrera@atc.ugr.es

IndiceInicioPerl

(C) Dpto. de CCIA
Web: http://decsai.ugr.es

1.- Introducción

    Prácticamente, en toda aplicación que se desarrolle, por muy pequeña que sea, vamos a tener la necesidad de almacenar datos. Siempre habrá cierta información que nos hará falta conservar entre distintas ejecuciones de una aplicación. Ejemplos de esta información para un MIDlet pueden ser:

    En los ordenadores tenemos la posibilidad de acceder a la información que tengamos guardada en nuestros discos duros o incluso podemos acceder a ella a través de una LAN, o Internet, caso de no tenerla guardada localmente. En los dispositivos móviles, tenemos poca capacidad de almacenamiento y carecemos de una conexión de red rápida y barata para acceder a los datos. Además, la forma de guardar información varía de dispositivo en dispositivo. MIDP nos proporciona una interfaz de programación independiente de cada dispositivo y que nos va a permitir tener  almacenados datos en nuestro dispositivo móvil para nuestras aplicaciones independientemente de que se estén ejecutando en ese momento.

    El paquete de java MIDP que se ocupa del almacenamiento persistente de datos es javax.microedition.rms. [ RMS (Record Management System)]

 

2.- Almacén de registros. Registros.

    Para que una aplicación pueda almacenar datos en el dispositivo móvil, debe crear un almacén de registros ("RecordStore"). Es una base de datos especial donde la unidad de información son los registros ("Records"). Una vez añadimos un registro a un almacén de registros, a éste se le asigna un identificador único (recordid). Los almacenes de registros pueden ser compartidos por los MIDlets dentro de una misma Suite de Midlets, pero un MIDlet no puede acceder nunca a un almacén que no haya sido creado por un MIDlet que no esté dentro de su Suite.

    Cada almacén de registros tiene un identificador que es una cadena de 1-32 caracteres Unicode, así pues, la combinación (nombre del almacén, MIDlet Suite) identifican unívocamente a un almacén de registros. Un MIDlet perteneciente a una Suite, puede acceder por tanto a cualquier almacén de datos de cualquiera de los otros MIDlets de su propia suite, pero sin embargo, no puede obtener ningún tipo de información de los almacenes de registros de cualquier otra Suite. Igualmente cabe notar que ninguna aplicación en un dispositivo móvil que no sea de Java tampoco puede acceder a los datos almacenados por los MIDlets de J2ME.

    Un almacén de registros tiene cero o más registros, donde cada registro es un array de bytes con un identificador único asociado que permite identificarlo unívocamente dentro del almacén. Los identificadores se les asignan a los registros empezando por 1 y dando al siguiente registro insertado un identificador igual a 1 + el identificador dado al último registro añadido anteriormente. El borrado de los identificadores no es relevante en la secuencia dada a la inserción de registros.

 

3.- API de MIDP para RMS: javax.microedition.rms.

    Recordemos que esta API nos permitirá almacenar información en un dispositivo móvil de manera persistente. Pero que el cómo la información se almacena es algo dependiente del dispositivo en concreto y que no es visible a los MIDlets.

3.1.- Interfaces

RecordComparator .- Nos ayudará a comparar dos registros para la obtención ordenada de todos los registros de un almacén.

RecordEnumeration .- Nos devolverá una enumeración de los registros de un almacén de datos. Nos servirá para recorrer todos los registros de un almacén.

RecordFilter .- A la hora de obtener una enumeración nos permitirá filtrar los registros que recorreremos.

RecordListener .- Monitorizador de eventos que usaremos para controlar cambios en los registros.

3.2.- Clase RecordStore

    Esta clase es la clase central del paquete rms, que nos representará una colección de registros. Podremos realizar operaciones para abrir, cerrar y borrar almacenes así como operar con los registros de éstos, añadiendo, eliminando, modificando y enumerando los registros dentro de un almacén.

    Métodos de la clase:

  • openRecordStore() .- Abre el almacén de registros
  • closeRecordStore() .- Cierra el almacén de registros
  • deleteRecordStore() .- Borra el almacén de registros
  • getName() .- Recupera el nombre del almacén de registros
  • getNumRecords() .- Recuperar el número de registros del almacén
  • addRecord() .- Añadir un registro al almacén de registros
  • getRecord() .- Recupera un registro del almacén de registros
  • deleteRecord() .- Borra un registro del almacén de registros
  • enumerateRecord() .- Obtiene un enumeration del almacén de registros

3.3.- Excepciones

InvalidRecordIDException: cuando realizamos una operacion con "RecordStore" sobre un registro que no existe. Tanto cero como los números negativos son identificadores inválidos para los registros.

RecordStoreException: excepción genérica para las operaciones realizadas sobre un "RecordStore".

RecordStoreFullException: excepción generada cuando no hay espacio suficiente.

RecordStoreNotFoundException: excepción lanzada cuando no se puede encontrar el "RecordStore" a abrir o cerrar.

RecordStoreNotOpenException: intento de realizar una operación con un "RecordStore" que ya ha sido cerrado mediante la operación closeRecordStore().

 

4.- UTILIZANDO RMS (Record Management System)

4.1.- ALMACENES

4.1.1.- Abriendo - creando un almacén de registros

    El método para abrir un almacén de registros es

public static RecordStore openRecordStore (String name, boolean create)

    Este método localiza un almacén con el nombre dado, lo abre y retorna un objeto "RecordStore" que se puede utilizar para acceder a él. El segundo parámetro "create" tiene doble significado, significa crear un almacén si no existe ninguno con dicho nombre.

    Por tanto una llamada a dicho método con el segundo parámetro a false devolverá una excepci ón "RecordStoreNotFound" si el almacén indicado en "name" no existe, y por otro lado por tanto si queremos crear un almacén nuevo o bien abrir uno si existía ya con ese nombre, pondremos dicho parámetro a true de la siguiente forma:

RecordStore almacen = RecordStore.openRecordStore("Scores", true)

    Poniendo el segundo argumento a true por tanto nos ahorra la comprobación de si nuestro almacén ya existía incluso si había sido creado por otro MIDlet dentro de un mismo Suite.

 

4.1.2.- Cerrando un almacén de registros. Borrando un almacén de registros.

    Una vez hemos terminado de utilizar un almacén de registros, debemos de cerrarlo usando el método closeRecordStore().

public void closeRecordStore()

    Si el MIDlet ha abierto el almacén más de una vez, el almacén no se cerrará hasta que cada instancia del almacén invoque al método closeRecordStore(). Una vez cerrado el almacén, cualquier intento de utilización de su objeto "RecordStore" devolverá una excepción "RecordStoreNotException".

    Para eliminar un almacén del dispositivo móvil, deberemos de llamar al método deleteRecordStore()

public static void deleteRecordStore(String name)

    Recordemos que:

        - no podremos borrar ningún almacén que no pertenezca al Suite al que pertenece el       MIDlet

        - no podremos borrar ningún almacén que esté siendo usado (no se haya cerrado convenientemente)

        - al eliminar un Suite de MIDlets automáticamente se borran sus almacenes de registros.

 

4.1.3.- Otras operaciones

 

4.2.- REGISTROS

4.2.1.- Añadiendo un registro al almacén de registros

    Para añadir un registro utilizaremos el método siguiente:

public int addRecord(byte[] data, int offset, int size)

    Este método añade como nuevo registro el subarray de bytes de "data" empezando por "offset" de un tamaño de "size". Por tanto para añadir un registro, los datos tienen que estar almacenados como un array de bytes. Esto en principio parece antinatural, pues habitualmente querremos almacenar en los registros datos bien estructurados de nuestro programa. Para realizar este paso utilizaremos la clase "DataOutputStream" para escribir los valores de la estructura de datos que queremos guardar en un "ByteArrayOutputStream", que creará el array de bytes adecuado para ser utilizado por el método addRecord() de la clase "RecordStore".

    Supongamos que tenemos como ejemplo una estructura de datos -clase- que tiene los datos correspondientes a un jugador en concreto junto con su puntuación en un juego y que queremos almacenarlo.

public class RegistroPuntuacion{
	public String jugador;
      public int puntuacion;
}

    para almacenar la puntuación de un jugador en un almacén de registros como un registro realizaríamos el siguiente código. Utilizaremos las clases "DataOutputStream" y "ByteArrayOutputStream" para quitarnos el problema de cómo convertir tipos de Java en una serie de bytes, así como de reservar la memoria para el array.

    //Creamos el objeto a escribir
    RegistroPuntuacion registro = new RegistroPuntuacion();
    registro.jugador = "Pepe";
    registro.puntuacion = 1234;

    //Creamos el flujo de salida
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    DataOutputStream dos = new DataOutputStream(baos);

    //Escribimos los valores de nuestra clase RegistroJugador para 
    //ser guardados en el flujo de salida
    dos.writeUTF(registro.jugador);
    dos.writeInt(registro.puntuacion);
    dos.close();

    //Obtenemos el array de bytes con los valores guardados
    byte[] datos = baos.toByteArray();

    //Guardamos el registro en el almacén de registros
    int id = recordStore.addRecord(datos,0,datos.length);
 

 

4.2.2.- Recuperando un registro del almacén de registros

    Para recuperar un registro del almacén bastará con invertir el código anterior para tener el array de bytes convertido a datos inteligibles para nuestro programa en la forma de nuestra estructura de datos -clase-. El método para recuperar un registro del almacén de registros es:

public byte[] getRecord(int recordId)

   el código para recuperar los datos del registro quedaría:

byte[] datos = recordStore.getRecord(id);

DataInputStream dis = new DataInputStream(new ByteArrayInputStream(datos));
RegistroPuntuacion registro = new RegistroPuntuacion();

registro.jugador = dis.readUTF();
registro.puntuacion = dis.readInt();
dis.close();  

 

4.2.3.- Modificación de un registro de un almacén de registros.

    El método para modificar un registro del almacén de registros es:

public void setRecord(int recordID, byte [] data, int offset, 
int size);

    para modificar un registro bastará con combinar los métodos de lectura y escritura de un registro vistos anteriormente solo que la llamada para almacenar el registro lleva indicado el ID del registro a modificar.

    registro.puntuacion = 
registro.puntuacion + 100;

    //Creamos el flujo de salida
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    DataOutputStream dos = new DataOutputStream(baos);

    //Escribimos los valores de nuestra clase RegistroJugador 
para ser guardados en el flujo de salida
    dos.writeUTF(registro.jugador);
    dos.writeInt(registro.puntuacion);
    dos.close();

    //Obtenemos el array de bytes con los valores guardados
    byte[] datos = baos.toByteArray();

    //Guardamos el registro en el almacén de registros, 
sobreescribiendo el registro guardado anteriormente
    recordStore.setRecord(id, datos,0,datos.length);

 

4.2.4.- Borrado de un registro del almacén

    Para eliminar un registro del almacén, bastaría con ejecutar el método

public void deleteRecord(int recordID)

 

4.2.5.- Eventos para controlar la modificación de un almacén de registros

    Los cambios en un almacén de registros vienen controlados por los objetos que implemente la interfaz "RecordListener" y que se configuren con el método addRecordListener() al correspondiente almacén. La interfaz tiene tres métodos:

public void recordAdded(RecordStore store, int recordID);
public 
void recordChanged(RecordStore store, int recordID);
public 
void recordDeleted(RecordStore store, int recordID);

    Todos estos métodos toman como parámetros el almacén donde tuvo lugar la modificación, así como el identificador del registro afectado. Para eliminar un "listener" de un almacén podemos utilizar el método removeRecordListener(). También una vez un almacén está completamente cerrado, también se eliminan los "listeners" asociados a el.

 

4.2.6.-  Otros métodos para el manejo de registros

    - getNumRecords(): devuelve el número de registros en el almacén de registros.

    - getRecordSize(): devuelve el tamaño de los datos de ese registro (sin contar los datos internos del almacén asociados a este registro).

    - getNextRecordId(): nos devuelve el id del siguiente registro que va a ser creado en el almacén de datos. Puede ser útil para almacenar claves externas o para almacenar el ID dentro de los datos del mismo registro. -OJO!: cuidado con el acceso concurrente al almacén de datos-.

 

4.3.- RECORRER UN ALMACÉN DE REGISTROS. ALMACENES DE REGISTROS.

4.3.1 Introducción

    Hasta ahora todos los métodos que habíamos visto para acceder a los registros de un almacén hacían uso de un identificador que se les pasaba como parámetro para encontrar el registro que queríamos. Pero dos cuestiones

    - ¿Vamos a tener conocimiento previo del identificador de un registro concreto que necesitamos?

    - ¿Como podemos realizar un recorrido completo de un almacén? 

    - ¿Cómo podemos buscar un registro concreto de un almacén sin necesidad de realizar un recorrido completo?

    Podríamos pensar en recorrer todos los identificadores posibles de un almacén empezando por 1 y descartando aquellos identificadores para los que no exista un registro. Pero esto resulta ineficiente, y más aún con las limitaciones de recursos que tenemos entre manos.

    Por tanto utilizaremos un método de la clase "RecordStore" que nos devolverá una enumeración de registros, en un orden que nosotros mismos especificaremos y con un filtro para seleccionar aquellos registros que cumplan una cierta condición.

    El método tiene la siguiente sintaxis:

public RecordEnumeration enumerateRecords(RecordFilter 
filter, RecordComparator comparator, boolean keepUpdated)

        - el término "filter" nos especificará los registros del almacén que serán incluidos en la enumeración devuelta.

        - el orden de los registros está controlado por el parámetro "comparator".

        - el último argumento "keepUpdated" indica si el resultado de la enumeración reflejará los cambios que se produzcan en el almacén desde que se hizo la llamada hasta que se utilice el almacén.

    Se puede realizar una llamada a esta función tomando los dos primeros parámetros a null. Una vez tenemos la enumeración, la manera de trabajar con ella es la siguiente:

    Disponemos de los siguientes métodos:

        - public abstract int numRecords(): nos devuelve el número de registros de la enumeración. 

        - hasNextElement() y hasPreviousElement() nos permitirá comprobar si quedan elementos a recorrer delante o detrás del registro actual, y con 

        - nextRecordId() y previousRecordId() obtenemos los identificadores de el registro previo o siguiente completando la lectura con getRecord(int id) y con 

        - nextRecord() o previousRecord() obtenemos directamente el registro sin tener que utilizar después getRecord().

        - otros métodos: reset(), isKeptUpdated(), keepUpdated(), rebuild().

EJERCICIO: Realizar un recorrido hacia alante y hacia atrás de un almacén y mostrar los resultados por pantalla en una lista.

 

4.3.2.- Filtros y comparadores

    Para  implementar un filtro basta con implementar la interfaz "RecordFilter", que tiene un solo método 

public boolean matches(byte[] datos)

    la enumeración llamará a este método para cada registro y tan solo considerará en ella a aquellos registros que devuelvan true a la llamada.

EJERCICIO: Crear un filtro y probar un recorrido con una enumeración que lo utilice.

    Para imponer un orden en una enumeración implementaremos la interfaz "RecordComparator" que también tiene un solo método:

public int compare(byte[] first, byte[] second)

    este método devolverá la relación entre los dos "registros" pasados: RecordComparator.EQUIVALENT, RecordComparator.PRECEDES (primero el primero), RecordComparator.FOLLOWS (primero el segundo). A la hora de implementar el método hay que seguir unas normas en el resultado, de manera que éstos sean consistentes e independientes de los registros comparados.

EJERCICIO: Crear un comparador y probar un recorrido con una enumeración que lo utilice.