Использование Berkeley DB в Android приложении

в 13:21, , рубрики: android, базы данных, Разработка под android, метки: ,

После успешно пройденного этапа «Hello World» под Android, решил написать для интереса простенькое приложение под Android, основной функционал которого сводился к хранению некоторого набора данных на устройстве. И очень мне не хотелось работать c SQL. Привык как-то уже работать с объектами. По-этому порыскав по интернету в поисках совместимых с Android решений нашёл только одно — Berkeley DB, встраиваемая БД.
Причём документация от Oracle показывала значительно лучшие показатели по производительности по сравнению с SQlite.По этому для своего приложения (дальше моего телефона оно так и не ушло) я выбрал этот формат хранения данных.
Класс являющийся ядром работы с БД сделан по шаблону Singleton, и получился следующим:

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.

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 хранить иерархию объектов.

@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;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public long getOdometer() {
        return odometer;
    }

    public void setOdometer(long odometer) {
        this.odometer = odometer;
    }

    public Date getOperationDate() {
        return operationDate;
    }

    public void setOperationDate(Date operationDate) {
        this.operationDate = operationDate;
    }

    public double getSum() {
        return sum;
    }

    public void setSum(double sum) {
        this.sum = sum;
    }
}

@Entity(version = 1)
public class FuelItem extends SpentItem {

    private double count;
    private double price;
    private boolean full;

    public double getCount() {
        return count;
    }

    public void setCount(double count) {
        this.count = count;
    }

    public boolean isFull() {
        return full;
    }

    public void setFull(boolean full) {
        this.full = full;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}

В классах сущностей, через аннотации передаётся информация:

  • о версии структуры объектов, которая сейчас должна храниться в БД. Если меняется структура объекта, то надо писать транслятор, который переведёт структуру данных из более ранней версии в текущую. Я решил проблему миграции через try/catch блок в конструкторе FuelItemDA.
  • Primary и Secondory ключах, по которым потом строятся индексы, которые у меня определяются на уровне DataAccess

Лично мне такой подход к организации хранения данных понравился. Т.к. для отображения мне нужны не столько данные, которые хранятся в БД, а логически обработанные, что проще делать именно с объектами.

В плюсы к работе с SQlite можно отнести привычный и более развитый инструментарий доступа к данным в виде SQL.
В плюсы к работе с Berkley Db можно отнести прямые CRUD операции над объектами, что облегчает последующую логическую работу с данными. Для меня это имело больший вес, нежели привычный интерфейс выдачи данных.

Автор: gubber

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js