Использование Berkeley DB в 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;

....
}

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

    private double count;
    private double price;
    private boolean full;
.....
}


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


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

Осталось только инициализировать DatabaseConfig, и здесь вообще ни каких сложностей нет.
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 операции над объектами, что облегчает последующую логическую работу с данными. Для меня это имело больший вес, нежели привычный интерфейс выдачи данных.

P.S.
Ссылка для скачивания. Нужна версия
Berkeley DB Java. Внутри архива найдёте библиотеку для Android.
Поделиться публикацией
Комментарии 12
    0
    Плюс, выскочила проблема создания/закрытия транзакции. Транзакция открывается в одном классе, а закрывается в другом, что так же выглядит не самым лучшим образом с точки зрения разработки. Пока эту «оплошность» я не смог «красиво» исправить.

    «Оплошность»?!?! Вы серьезно??
      –1
      Пока не знаю, как сделать это «красиво».
      По этому здесь вопрос в руках разработчика. Заметьте, транзакции у меня все закрываются. Именно по этому, я отнёс это в разряд красивостей/«оплошностей»
        +1
        Если разобрать по кусочкам, то:
        Вы в класе DatabaseConfig создаете объект Environment, из него получаете методом beginTransaction конкретную транзацкию(вы не в DatabaseConfig открываете, а его методом с его настройками, получаете), и потом в том же контексте её закрываете методом commit. Почему вы написали что создаете в одном закрываете в другом?
          0
          Мне такое решение режет глаз, да ещё при условии, что транзакцию надо открывать для получения всего лишь следующего элемента в последовательности.
      +1
      Удачи с отображением данных в списках и прочих AdapterView.
        –1
        Мне AdapterView не подходит по умолчанию. У меня уровень отображения данных сильно отличается от уровня представления данных. Лично для меня прокидывать «сырые» данные прямо в уровень отображения — плохая норма.
          0
          В исходниках есть пример для Android и вложен класс JECursosAdapter и пример его работы в ListActivity. Так что с «удачей» все в порядке:
          Код примера
          public class JECursorAdapterExample extends ListActivity {
              private final int RECORDNUM = 5000;
              private final int KEYRANGEMIN = 0;
              private final int KEYRANGEMAX = 4999;
              private final String DBPATH = "data/tmp/JECursorAdapterDemo";
              
              private File myDbEnvPath = new File(DBPATH);
              private DataAccessor da;
              private static MyDbEnv myDbEnv = new MyDbEnv();
              private EntityCursor<Data> valueCursor;
              private EntityCursor<Integer> keyCursor;
              
              @Override
              public void onCreate(Bundle savedInstanceState) {
                  super.onCreate(savedInstanceState);
                  setContentView(R.layout.main);
                  myDbEnv.setup(myDbEnvPath, false /*read-only*/);  
                  da = new DataAccessor(myDbEnv.getEntityStore());
                  
                  /* If there is no data, then load the data into JE DB. */
                  if (da.dataByKey.count() == 0) {
                      LoadJEData();
                  }
                  ReadJEDataFromCursor();
                  setListAdapter(new JECursorAdapter<Integer, Data>
                                 (this, android.R.layout.simple_list_item_1, 
                                  keyCursor, valueCursor, da.dataByKey));
                                 
                  /* Jump to a specific position. */
                  final Button buttonJump = (Button) findViewById(R.id.jump);
                  buttonJump.setOnClickListener(new Button.OnClickListener() {
                          public void onClick(View v) {
                              final EditText editText =
                                  (EditText) findViewById(R.id.position);
                              int position = Integer.
                                  valueOf(editText.getText().toString());
                              position = position < KEYRANGEMIN ? 
                                         KEYRANGEMIN : 
                                         position;
                              position = position > KEYRANGEMAX ? 
                                         KEYRANGEMAX : 
                                         position;
                              getListView().setSelection(position);
                          }
                      });
                  
                  /* Jump to the beginning of the database. */
                  final Button buttonJumpStart = (Button) findViewById(R.id.jump_start);
                  buttonJumpStart.setOnClickListener(new Button.OnClickListener() {
                          public void onClick(View v) {
                              getListView().setSelection(KEYRANGEMIN);
                          }
                      });
                  
                  /* Jump to the end of the database. */
                  final Button buttonJumpEnd = (Button) findViewById(R.id.jump_end);
                  buttonJumpEnd.setOnClickListener(new Button.OnClickListener() {
                          public void onClick(View v) {
                              getListView().setSelection(KEYRANGEMAX);
                          }
                      });
              }
              
              private void LoadJEData() 
                  throws DatabaseException {
                  
                  Data theData = new Data();
                  try {
                      for (int i = 0; i < RECORDNUM; i++) {
                          theData.setKey(i);
                          theData.setData("Record " + i);
                          da.dataByKey.put(theData);
                      }
                  } catch (DatabaseException dbe) {
                      dbe.printStackTrace();
                  }
              }
              
              private void ReadJEDataFromCursor() 
                  throws DatabaseException {
                  
                  valueCursor = 
                      da.dataByKey.entities(KEYRANGEMIN, true, KEYRANGEMAX, true);
                  CursorConfig cursorCon = new CursorConfig();
                  cursorCon.setReadUncommitted(true);
                  keyCursor = da.dataByKey.keys
                      (null, KEYRANGEMIN, true, KEYRANGEMAX, true, cursorCon);
              }
          }
          

          0
          А где ссылка на библиотеку для встраивания Berkeley DB?
            0
            Исправил.
            Ссылка для скачивания. Нужна версия
            Berkeley DB Java. Внутри архива найдёте библиотеку для Android
              0
              А под iOS кто-то осилил собрать?
              Что-то у меня не получилось с первого раза…

              P.S. RTFM не помог
            0
            Интересен такой вопрос — как вы делали пагинацию в случае с наличием связи 1-N или N-N. Грубо говоря, есть у меня список учеников, список классов, связанные между собой, очевидно, идентификаторами. Можно ли каким-то образом получить выборку учеников, отсортированную по любому из SecondaryIndex-ов по id класса?
              0
              Нет, такой задачи передо мной не стояло. Я использую Berkley для простых, не связанных данных.
              К тому же это очень древняя версия библиотеки. Более новую с открытой лицензией я не смог найти.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое