Разработка

Использование 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 решения.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *