Monday, July 8, 2013

SQLite в андроид приложении

Android предоставляет различные механизмы хранения данных приложения, от простых preference в виде ключ-значение до пользовательских баз данных в виде sqlite. Сегодня речь пойдет о втором способе - встраиваемой БД. Мотивацией к написанию данного топика послужило то, что информация о некоторых моментах весьма скудна, примеры приведенные в руководстве разработчика андроид приложений и в книгах, порой вызывают больше вопросов чем их было до момента чтения. Можно найти типовые примеры с небольшими вариациями, но пользы от этого мало. Я прекрасно понимаю, что цель упомянутых статей скорее не научить, а дать представление, но ознакомившись с базовыми техниками, хотелось бы найти источники демонстрирующие т.н. best practices или хотябы раскрывающие вопросы отличные от начального уровня. Интересовало, собственно, следующее:

  • Где/когда и как создавать(правильно) SQLiteOpenHelper?
  • Сколько экземпляров данного класа может создать в приложении? Один или несколько, скажем, по экземпляру в ContentProvider?
  • Если в каждом ContentProviderможет быть по экземпляру SQLiteOpenHelper, то как ими управлять(и нужно ли)? Я имею ввиду закрывать соединение с базой.
  • Как правильно организовать доступ к базе в многопоточном приложении? Например, что будет, если в одновременно попытаться сделать запись в базу из разных тредов?
Очевидно, что я не первый, кто задался похожими вопросами. Поэтому поиск ответов начал с интенсивного гугления. И таки кое-что удалось найти. Полный перевод приводить не буду, лишь сделаю выжимку из прочитанного. Итак по порядку.

Абстрактная фабрика для SQLiteDatabase

Самый простой способ организации доступа к БД приложения - создание наследника класса SQLiteDatabase, который реализует статический метод фабрику. Момент который нужно усчесть - это использование контекста приложения, а не активти либо сервиса:

public class DatabaseHelper extends SQLiteOpenHelper { 
 
  private static DatabaseHelper instance = null;
 
  private static final String DATABASE_NAME = "dbname.db";
  private static final int DATABASE_VERSION = 1;
 
  public synchronized static DatabaseHelper getInstance(Context ctx) {
      
    // Use the application context, which will ensure that you 
    // don't accidentally leak an Activity's context.    
    if (instance == null) {
        instance = new DatabaseHelper(ctx.getApplicationContext());
    }
    return instance;
  }
     
  private DatabaseHelper(Context ctx) {
      super(ctx, DATABASE_NAME, null, DATABASE_VERSION);
  } 
  ...
} 

В рантайме будет только один экземпляр SQLiteOpenHelper, которую можно получить из любого компонента, вызвав соответствующий метод фабрики. Использование контекста приложения, позволяет избежать такого рода ошибок:

E/Database(234): Leak found
E/Database(234): Caused by: java.lang.IllegalStateException:
                 SQLiteDatabase created and never closed

SQLiteDatabase обернутый в ContentProvider

Как говорят сторожилы: This is also a nice approach. Каждая реализация ContentProvider-а может содержать приватную ссылку на SQLiteDatabase. Более того, нет необходимости управлять соединением вручную(закрывать его). Вот такой ответ я нашел по этому поводу: A content provider is created when its hosting process is created, and remains around for as long as the process does, so there is no need to close the database -- it will get closed as part of the kernel cleaning up the process's resources when the process is killed. Резюмируя написанное, будем иметь следующий код:

public class SomeContentProvider extends ContentProvider {
  private HelperDatabase helper;
 
  @Override
  public boolean onCreate() {  
      helper = new HelperDatabase(getContext());
      return true;
  }
  ...
}
public class HelperDatabase extends SQLiteOpenHelper {
  private static final String DATABASE_NAME = "dbname.db";
  private static final int DATABASE_VERSION = 1;
 
  public HelperDatabase(Context ctx){
      super(ctx, DATABASE_NAME, null, DATABASE_VERSION);
  }
  ...
}

Многопоточность

Недавно на хабре был замечательный перевод статьи, раскрывающей некоторые внутренние детали работы SQLite-а. Советую почитать. Я лишь приведу краткую информацию по теме. Если попытаться осуществить одновременную запись из разных соединений(потоков) в базу, то одна из операций отвалит с ошибкой. Поток не будет заблокирован на ожидании(тут рассматривается второй случай доступа к базе данных, когда мы создаем наследника от ContentProvider). Более того, если выполнить неверный inser/update sql запрос, то Android не выбросит исключение, а лишь сделает соотв. запись в logcat. Сказанное, может показаться не существенным, но если у вас десяток потоков пишет в базу, то сообщение об ошибке может остаться не замеченным среди прочих. В общем, я к тому, что нужно быть внимательным и об этом помнить.

Резюмируя

  • Для создания экземпляров SQLiteOpenHelper можно использовать метод фабрику либо создать наследника ContentProvider.
  • Ограничений на количество экземпляров SQLiteOpenHelper нет, в том смысле, что это не обязательно должен быть сенглетон.
  • Управление SQLiteOpenHelper в рамках ContentProvider осуществляется самим провайдером. Удерживаемые ресурсы будут утилизированы месте с процессом удерживающим их.
  • SQLite имеет блокировку на уровне файла. Читать данные можно из различных потоков, писать может только один. При попытке одновременной записи из двух разных соединений, один из них вернет ошибку(поток не будет блокирован на ожидание)

Линки

  1. Механизм атомарного коммита в SQLite
  2. Настройки в Android-приложениях
  3. Android Preferences
  4. Correctly Managing Your SQLite Database
  5. Android Sqlite Locking

1 comment:

  1. Предлагаю посмотреть бесплатный инструмент - Valentina Studio. Супер вещь!!! Очень рекомендую - best manager for SQLite!!! http://www.valentina-db.com/en/valentina-studio-overview

    ReplyDelete