Pull to refresh

Базы данных в MIDP, часть 1: понятие Record Management System

Reading time10 min
Views4.5K
Original author: Eric Giguere
Одним из ключевых компонентов MIDP является Record Management System (RMS). Это API, предоставляющий возможность хранить данные локально, в памяти устройства. Для большинства MIDP-совместимых телефонов это единственный способ хранения данных — лишь небольшое число устройств поддерживают доступ к обычной файловой системе. Легко догадаться, что полное понимание механизма RMS необходимо для написания любого приложения, требующего хранения локальных данных.

Это первая статья цикла, в котором будут рассмотрены наиболее распространенные проблемы, касающиеся использования RMS в приложениях, например, взаимодействие с внешними источниками данных, такими как реляционные БД. Для начала мы узнаем, что может нам предложить RMS, и напишем несколько простых отладчиков.

Ключевые понятия


Записи

Из названия понятно, что RMS — это система для управления записями. Запись (record) — это элемент данных. RMS не накладывает никаких ограничений на содержимое записи, она может содержать число, строку, массив, изображение — что угодно, что можно представить в виде последовательности байтов. Если вы можете закодировать имеющиеся данные в бинарный формат (и раскодировать обратно), то вы можете сохранить их в записи, если, конечно, они укладываются в ограничение по размеру, накладываемое системой.

Многих новичков в RMS озадачивает понятие записи. Они спрашивают: «где же поля?», удивляясь, как система разделяет индивидуальные записи на отдельные последовательности данных. Ответ прост: запись RMS не содержит никаких полей. Точнее, запись содержит одно бинарное поле произвольной длины. Функция интерпретации содержимого записи целиком возлагается на приложение. RMS предоставляет хранилище и уникальный идентификатор, больше ничего. Это создает трудности для приложений, но сохраняет RMS простой и гибкой, что довольно важно для подсистемы MIDP.

На уровне API записи — это просто массивы байтов.

Хранилища записей

Хранилище (record store) — это упорядоченная коллекция записей. Каждая запись принадлежит хранилищу и доступна только через него. Хранилище гарантирует атомическое чтение и запись данных, предотвращая их повреждение.

Когда создается запись, хранилище присваивает ей уникальный целочисленный идентификатор (record ID). Первая запись получает id 1, вторая — 2 и т. д. Это не индекс: при удалении записи оставшиеся элементы не перенумеровываются.

Имя (name) используется для идентификации хранилища внутри мидлета. Имя может содержать от 1 до 32 unicode символов и должно быть уникально внутри мидлета, создавшего хранилище. В MIDP 1.0 хранилища не могут использоваться более чем одним приложением. MIDP 2.0 опционально разрешает это делать, в этом случае хранилище идентифицируется не только именем, но и названием и производителем приложения, создавшего это хранилище.

Кроме того, хранилище содержит информацию о дате последнего изменения и версии. Приложения также могут привязать к хранилищу обработчик события изменения данных в нем.

На уровне API хранилище представлено экземпляром класса javax.microedition.rms.RecordStore. Все классы и интерфейсы RMS определены в пакете javax.microedition.rms.

Аспекты RMS


Ограничения на размер данных

Количество памяти, доступной для хранения записей, отличается на разных устройствах. Спецификация MIDP требует резервирования не менее 8 кб памяти для постоянного хранения данных. При этом размер одной записи не ограничивается. RMS предоставляет методы для определения размера размера записи, общего размера хранилища и размера свободной памяти. Помните, что постоянная память общая для всех приложений, используйте ее экономно.

Любой мидлет, использующий RMS, должен указать минимальный размер хранилища в байтах, необходимый для его работы. Для этого в манифесте jar-файла и в jad-файле должен быть установлен аттрибут MIDlet-Data-Size. Не указывайте там слишком большое значение — некоторые устройства могут запретить установку приложения, если свободного места недостаточно. На практике большинство устройств позволяют приложениям выходить за пределы стартового размера данных.

Обратите внимание, что некоторые реализации MIDP требуют указания дополнительных атрибутов, относящихся к необходимому размеру памяти. Это должно быть указано в документации к устройству.

Скорость работы

Операции с постоянной памятью обычно производятся медленнее, чем с оперативной. В частности, на некоторых платформах запись данных может занимать длительное время. Для увеличения производительности используйте кеширование часто используемых данных в оперативной памяти. Чтобы не замедлять отклик пользовательского интерфейса, не выполняйте операций с RMS в потоке обработки событий мидлета.

Безопасность использования потоков

Операции с RMS являются thread-safe. Тем не менее, потоки необходимо координировать между собой, как при работе с любым общим ресурсом. Это касается и одновременно запущенных мидлетов, использующих одно и то же хранилище.

Исключения

Вообще, методы RMS API бросают несколько исключений (в дополнение к стандартным исключениям вроде java.lang.IllegalArgumentException). Эти исключения определены в пакете javax.microedition.rms:
  • InvalidRecordIDException — операция не может быть выполнена, потому что передан неправильный record id.
  • RecordStoreFullException — закончился доступный объем памяти.
  • RecordStoreNotFoundException — указанное хранилище не существует.
  • RecordStoreNotOpenException — приложение пытается использовать хранилище, которое было закрыто.
  • RecordStoreException — суперкласс предыдущих исключений, также бросается при общих ошибках, не охватываемых ими.

Заметьте, что для краткости мы будем пренебрегать обработкой исключений в некоторых примерах (ввиду ее простоты).

Использование RMS


Остаток статьи мы посвятим основным операциям с записями через RMS API. Некоторые из них представлены в классе RMSAnalyzer, предназначенном для анализирования хранилищ. Вы можете использовать его как инструмент отладки в ваших проектах.

Поиск хранилищ

Список хранилищ можно получить с помощью RecordStore.listRecordStores(). Этот статический метод возвращает массив строк, каждая из которых является именем принадлежащего мидлету хранилища. Если не создано ни одного хранилища, возвращается null.

Метод RMSAnalyzer.analyzeAll() использует listRecordStores(), чтобы вызвать analyze() для каждого хранилища:
public void analyzeAll() {
    String[] names = RecordStore.listRecordStores();

    for( int i = 0;
         names != null && i < names.length;
         ++i ) {
        analyze( names[i] );
    }
}

Обратите внимание, что массив содержит имена только хранилищ, созданных нашим мидлетом. Спецификация MIDP не содержит какого-либо способа получить список всех хранилищ остальных мидлетов. MIDP 1.0 не дает доступа к чужим хранилищам. В MIDP 2.0 приложение может отметить хранилище как общее (shareable), но другие мидлеты смогут использовать его, только если им известно его имя.

Открытие и закрытие хранилища

RecordStore.openRecordStore() используется для открытия (и иногда создания) хранилища. Этот статический метод возвращает экземпляр объекта RecordStore, как видно из этой версии RMSAnalyzer.analyze():
public void analyze( String rsName ) {
    RecordStore rs = null;

    try {
        rs = RecordStore.openRecordStore( rsName, false );
        analyze( rs ); // перегруженный метод
    } catch( RecordStoreException e ) {
        logger.exception( rsName, e );
    } finally {
        try {
            rs.closeRecordStore();
        } catch( RecordStoreException e ){
            // игнорируем это исключение
        }
    }
}

Второй параметр метода openRecordStore() показывает, будет ли создано хранилище, если оно не существует. В MIDP 2.0 для открытия хранилища, созданного другим приложением, используется следующая форма openRecordStore():
...
String name = "mySharedRS";
String vendor = "EricGiguere.com";
String suite = "TestSuite";
RecordStore rs = RecordStore.openRecordStore( name, vendor, suite );
...

Производитель и имя мидлета должны совпадать с указанными в манифесте.

После окончания работы закройте хранилище вызовом RecordStore.closeRecordStore(), как в приведенном выше методе analyze().

Экземпляр RecordStore уникален в мидлете: после его открытия все последующие вызовы openRecordStore() с теми же аргументами вернет ссылку на тот же объект. Этот экземпляр общий для всех мидлетов в коллекции.

Каждый экземпляр RecordStore подсчитывает, сколько раз было открыто хранилище. Оно не закроется, пока closeRecordStore() не будет вызван такое же число раз. После закрытия хранилища попытка его использования приведет к исключению RecordStoreNotOpenException.

Создание хранилища

Чтобы создать хранилище (недоступное другим мидлетам), нужно вызвать openRecordStore(), установив второй параметр в true:
...
// создать хранилище
RecordStore rs = null;

try {
    rs = RecordStore.openRecordStore( "myrs", true );
} catch( RecordStoreException e ){
    // не удалось создать или открыть хранилище
}
...

Чтобы совершить первоначальную инициализацию хранилища, проверим значение getNextRecordID() — если оно равно 1, в хранилище нет записей:
if( rs.getNextRecordID() == 1 ){
    // первоначальная инициализация
}

Альтернативный способ — проверка значения, возвращаемого getNumRecords():
if( rs.getNumRecords() == 0 ){
    // хранилище пусто, реинициализация
}

Чтобы создать общедоступное хранилище (только в MIDP 2.0), используйте следующий вариант вызова openRecordStore() с четырьмя параметрами:
boolean writable = true;

rs = RecordStore.openRecordStore( "myrs", true, 
       RecordStore.AUTHMODE_ANY, writable );

Если второй параметр true и хранилище не существует, два последних параметра контролируют его режим доступа и возможность записи. Режим доступа определяет, смогут ли другие приложения пользоваться этим хранилищем. Возможны два варианта: RecordStore.AUTHMODE_PRIVATE (доступ имеет только наше приложение) или RecordStore.AUTHMODE_ANY (доступ имеет любое приложение). Флаг writable определяет, будет ли другое приложение иметь доступ на запись — если writable=false, оно сможет только читать данные.

Приложение-владелец хранилища может изменить эти параметры в любое время с помощью RecordStore.setMode():
rs.setMode( RecordStore.AUTHMODE_ANY, false );

Фактически, лучше всего создать хранилище приватным, а доступ открыть только после его инициализации.

Добавление и изменение записей

Напомним, что записи — это массивы байтов. Для добавления новой записи в открытое хранилище используется метод RecordStore.addRecord():
...
byte[] data = new byte[]{ 0, 1, 2, 3 };
int    recordID;

recordID = rs.addRecord( data, 0, data.length );
...

Вы можете создать пустую запись, передав в качестве первого параметра null. Второй и третий параметр задают стартовую позицию чтения и количество байтов, которое нужно сохранить. В случае успеха возвращается id записи, иначе бросается исключение (например, RecordStoreFullException).

В любой момент можно обновить запись с помощью RecordStore.setRecord():
...
int    recordID = ...; // ID некоторой записи
byte[] data = new byte[] { 0, 10, 20, 30 };

rs.setRecord( recordID, data, 1, 2 ); 
    // заменить все данные в записи на 10, 20
...

Узнать, какой id будет присвоен следующей добавляемой записи, можно с помощью метода RecordStore.getNextRecordID(). Все существующие записи имеют id меньше этого.

Во второй части мы рассмотрим способы конвертирования объектов и других данных в массив байтов.

Чтение записей

Для чтения записей используется RecordStore.getRecord() в одной из двух форм. В первом варианте этот метод создает массив нужной длины и записывает в него данные:
...
int    recordID = .... // ID некоторой записи
byte[] data = rs.getRecord( recordID );
...

Во втором варианте данные копируются в уже созданный массив, начиная с заданной позиции, при этом возвращается количество записанный байтов:
...
int    recordID = ...; // ID записи
byte[] data = ...; // массив
int    offset = ...; // стартовая позиция

int numCopied = rs.getRecord( recordID, data, offset );
...

Массив должен иметь достаточную длину для вмещения данных, иначе произойдет java.lang.ArrayIndexOutOfBoundsException. Для определения необходимого размера массива используется RecordStore.getRecordSize(). Фактически первая форма getRecord() эквивалентна следующему:
...
byte[] data = new byte[ rs.getRecordSize( recordID ) ];
rs.getRecord( recordID, data, 0 );
...

Вторая форма полезна, если вы перебираете много записей в цикле, так как снижается число запросов памяти. Например, вы можете использовать ее с getNextRecordID() и getRecordSize() для поиска перебором всех записей в хранилище:
...
int    nextID = rs.getNextRecordID();
byte[] data = null;

for( int id = 0; id < nextID; ++id ) {
    try {
        int size = rs.getRecordSize( id );

        if( data == null || data.length < size ) {
            data = new byte[ size ];
        }

        rs.getRecord( id, data, 0 );

        processRecord( rs, id, data, size ); // что-то делаем с найденной записью
    } catch( InvalidRecordIDException e ){
        // игнорируем и переходим к следующей записи
    } catch( RecordStoreException e ){
        handleError( rs, id, e ); // обработка ошибок
    }
}
...

Однако для этого лучше использовать RecordStore.enumerateRecords(). Мы рассмотрим этот метод в третьей части цикла статей.

Удаление записей и хранилищ

Для удаления записей предназначена функция RecordStore.deleteRecord():
...
int recordID = ...; 
rs.deleteRecord( recordID );
...

После удаления записи любая попытка ее использовать приводит к InvalidRecordIDException.

Можно удалить всё хранилище целиком с помощью RecordStore.deleteRecordStore():
...
try {
    RecordStore.deleteRecordStore( "myrs" );
} catch( RecordStoreNotFoundException e ){
    // нет такого хранилища
} catch( RecordStoreException e ){
    // хранилище открыто
}
...

Хранилище нельзя удалить, пока оно открыто каким-либо приложением. Удалить хранилище может только мидлет, создавший его.

Другие операция

Осталось всего несколько операций с RMS, все они являются методами класса RecordStore:
  • getLastModified() возвращает время последней модификации хранилища в том же формате, что и System.currentTimeMillis().
  • getName() возвращает имя хранилища.
  • getNumRecords() возвращает количество записей в хранилище.
  • getSize() возвращает полный размер хранилища в байтах, включая длину записей и служебные поля, необходимые системе для его организации.
  • getSizeAvailable() возвращает размер свободного пространства в байтах. Реальный доступный размер может оказаться меньше из-за дополнительных байтов, используемых хранилищем для хранения каждой записи.
  • getVersion() возвращает номер версии хранилища. Это положительное целое число, увеличивающееся на единицу при каждом изменении данных.

Мидлет также может следить за изменениями хранилища, зарегистрировав обработчик с помощью addRecordListener(), для его удаления предназначен removeRecordListener(). В третьей части эти методы будут рассмотрены более подробно.

Класс RMSAnalyzer


Закончим эту статью исходным кодом класса RMSAnalyzer, нашего анализатора хранилищ. Для анализа нужно выполнить следующий код:
...
RecordStore rs = ...; // открываем хранилище
RMSAnalyzer analyzer = new RMSAnalyzer();
analyzer.analyze( rs );
...

По умолчанию вывод перенаправляется в System.out и выглядит примерно так:
=========================================
Record store: recordstore2
Number of records = 4
Total size = 304
Version = 4
Last modified = 1070745507485
Size available = 975950

Record #1 of length 56 bytes
5f 62 06 75 2e 6b 1c 42 58 3f _b.u.k.BX?
1e 2e 6a 24 74 29 7c 56 30 32 ..j$t)|V02
5f 67 5a 13 47 7a 77 68 7d 49 _gZ.Gzwh}I
50 74 50 20 6b 14 78 60 58 4b PtP k.x`XK
1a 61 67 20 53 65 0a 2f 23 2b .ag Se./#+
16 42 10 4e 37 6f .B.N7o
Record #2 of length 35 bytes
22 4b 19 22 15 7d 74 1f 65 26 "K.".}t.e&
4e 1e 50 62 50 6e 4f 47 6a 26 N.PbPnOGj&
31 11 74 36 7a 0a 33 51 61 0e 1.t6z.3Qa.
04 75 6a 2a 2a .uj**
Record #3 of length 5 bytes
47 04 43 22 1f G.C".
Record #4 of length 57 bytes
6b 6f 42 1d 5b 65 2f 72 0f 7a koB.[e/r.z
2a 6e 07 57 51 71 5f 68 4c 5c *n.WQq_hL\
1a 2a 44 7b 02 7d 19 73 4f 0b .*D{.}.sO.
75 03 34 58 17 19 5e 6a 5e 80 u.4X..^j^?
2a 39 28 5c 4a 4e 21 57 4d 75 *9(\JN!WMu
80 68 06 26 3b 77 33 ?h.&;w3

Actual size of records = 153
-----------------------------------------

Этот формат удобно использовать при тестировании с помощью J2ME Wireless Toolkit. При тестировании на реальном устройстве можно отправить вывод анализатора на последовательный порт или даже по сети. Для этого нужно создать новый класс с интерфейсом RMSAnalyzer.Logger и передать его экземпляр конструктору RMSAnalyzer.

Завершает статью проект J2ME Wireless Toolkit под названием RMSAnalyzerTest, демонстрирующий использование анализатора: pastebin.com/n36QLuAs

Остальные части статьи на английском можно увидеть здесь. Имеет ли смысл продолжать их перевод?
Tags:
Hubs:
Total votes 28: ↑25 and ↓3+22
Comments3

Articles