Использование Berkeley DB в Android приложении
Моя старая статья, написанная в 2013 году на habr.com
После успешно пройденного этапа Android «Hello World», решил написать для интереса простенькое приложение на Android, основной функционал которого сводился к хранению некоторого набора данных на устройстве. И очень мне не хотелось работать c SQL. Привык как-то уже работать с объектами. По-этому порыскав по интернету в поисках совместимых с Android решений нашёл только одно: Berkeley DB — встраиваемая БД.
Причём документация от Oracle показывала значительно лучшие показатели по производительности по сравнению с SQlite.По этому для своего приложения (дальше моего телефона оно так и не ушло) я выбрал этот формат хранения данных.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | public class DatabaseConfig { private static DatabaseConfig ourInstance; private Environment envmnt; private EntityStore store; public static DatabaseConfig getInstance() { if (ourInstance == null) throw new IllegalArgumentException("You need initialize database config previously!"); return ourInstance; } public static void init(File envDir) { ourInstance = new DatabaseConfig(envDir); } private DatabaseConfig(File envDir) { EnvironmentConfig envConfig = new EnvironmentConfig(); StoreConfig storeConfig = new StoreConfig(); envConfig.setTransactional(true); envConfig.setAllowCreate(true); storeConfig.setAllowCreate(true); storeConfig.setTransactional(true); envmnt = new Environment(envDir, envConfig); try { store = new EntityStore(envmnt, "autocalc", storeConfig); } catch (IncompatibleClassException e) { //todo: реализовать преобразования данных. } } public static void shutdown() { if (ourInstance != null) { ourInstance.close(); } } private void close() { store.close(); envmnt.close(); } public EntityStore getStore() { return store; } public Transaction startTransaction() { return envmnt.beginTransaction(null, null); } } |
Проблемы этого класса достаточно прозаичны, перед тем как получить доступ к сущности, её надо инициализировать, что можно забыть. Плюс, выскочила проблема создания/закрытия транзакции. Транзакция открывается в одном классе, а закрывается в другом, что так же выглядит не самым лучшим образом с точки зрения разработки. Пока эту «оплошность» я не смог «красиво» исправить. Особенно криво это смотрится в свете того, что транзакции используются для того, чтобы получить следующее значение идентификатора для сохраняемой сущности.
На более высоком уровне были созданы классы доступа к данным DataAccess.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | public class FuelItemDA { private PrimaryIndex<Long, FuelItem> prIndex; private SecondaryIndex<Long, Long, FuelItem> odometerIndex; private SecondaryIndex<Date, Long, FuelItem> dateIndex; private DatabaseConfig dbConfig; public FuelItemDA() { dbConfig = DatabaseConfig.getInstance(); prIndex = dbConfig.getStore().getPrimaryIndex( Long.class, FuelItem.class); odometerIndex = dbConfig.getStore().getSecondaryIndex( prIndex, Long.class, "odometer"); dateIndex = dbConfig.getStore().getSecondaryIndex( prIndex, Date.class, "operationDate"); } public void save(FuelItem item) { Transaction tx = dbConfig.startTransaction(); try { if (item.getId() == 0) { long id = dbConfig.getStore().getSequence("SPENT_ID").get(tx, 1); item.setId(id); } prIndex.put(tx, item); tx.commit(); } catch (Exception e) { e.printStackTrace(); if (tx != null) { tx.abort(); tx = null; } } } public FuelItem load(long id) { return prIndex.get(id); } public List<FuelItem> getItemsInDates(Date bDate, Date eDate) { List<FuelItem> result = new LinkedList<FuelItem>(); EntityCursor<FuelItem> cursor = dateIndex.entities(bDate, true, eDate, true); for (Iterator<FuelItem> iterator = cursor.iterator(); iterator.hasNext(); ) { FuelItem spentItem = iterator.next(); result.add(spentItem); } cursor.close(); return result; } public void removeFuelItem(long id) { try { prIndex.delete(id); } catch (DatabaseException e) { e.printStackTrace(); prIndex.delete(id); } } } |
Здесь надо обратить внимание на создание индексов, по которым потом осуществляется поиск и фильтрация. Т.е. если появляется необходимость искать и фильтровать данные по другому набору полей, то надо будет создавать дополнительный индекс.
Ещё одной особенностью работы с Berkley DB было написание классов-сущностей, которые используются для хранения информации. По задумке была реализована возможность Berkley DB хранить иерархию объектов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @Persistent(version = 1) public class SpentItem implements Item{ @PrimaryKey(sequence="SPENT_ID") private long id; @SecondaryKey(relate= Relationship.MANY_TO_ONE) private long odometer; @SecondaryKey(relate= Relationship.MANY_TO_ONE) private Date operationDate; private double sum; .... } @Entity(version = 1) public class FuelItem extends SpentItem { private double count; private double price; private boolean full; ..... } |
В классах сущностей, через аннотации передаётся информация:
- о версии структуры объектов, которая сейчас должна храниться в БД. Если меняется структура объекта, то надо писать транслятор, который переведёт структуру данных из более ранней версии в текущую. Я решил проблему миграции через try/catch блок в конструкторе FuelItemDA.
- Primary и Secondory ключах, по которым потом строятся индексы, которые у меня определяются на уровне DataAccess
Лично мне такой подход к организации хранения данных понравился. Т.к. для отображения мне нужны не столько данные, которые хранятся в БД, а логически обработанные, что проще делать именно с объектами.
Осталось только инициализировать DatabaseConfig, и здесь вообще ни каких сложностей не возникает.
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class Calc extends Activity { private void setup() throws DatabaseException { File envDir = new File(android.os.Environment.getExternalStorageDirectory(), "data"); envDir = new File(envDir, "autoexpence"); if (!envDir.exists()) { if (!envDir.mkdirs()) { Log.e("TravellerLog :: ", "Problem creating Image folder"); } } DatabaseConfig.init(envDir); } } |
В плюсы к работе с SQlite можно отнести привычный и более развитый инструментарий доступа к данным в виде SQL.
В плюсы к работе с Berkley Db можно отнести прямые CRUD операции над объектами, что облегчает последующую логическую работу с данными. Для меня это имело больший вес, нежели привычный интерфейс выдачи данных. Отсутствие потери производительности для работы с объектами, в отсутствии необходимости использовать ORM решения.
2 комментария
Кемпи
В плюсы к работе с SQlite можно отнести привычный и более развитый инструментарий доступа к данным в виде SQL. В плюсы к работе с Berkley Db можно отнести прямые CRUD операции над объектами, что облегчает последующую логическую работу с данными. Для меня это имело больший вес, нежели привычный интерфейс выдачи данных. P.S. Ссылка для скачивания. Нужна версия Berkeley DB Java. Внутри архива найдёте библиотеку для Android.
gubber
Основное преимущество — это скорость работы, т.к. нет этапа преобразования данных.
Я использую версию из maven — репозитория.