Правильная работа с БД в Android

Development for Android
Приветствую всех дроидеров в эти непростые для нас времена.
Честно говоря, заколебала эта шумиха о патентах, войнах и т.д., но в данной статье речь пойдет не об этом.
Я не собирался писать статью на данную тему, так как везде всего полно о работе с базой данных в Android и вроде бы все просто, но уж очень надоело получать репорты об ошибках, ошибках специфичных и связанных с БД.
Поэтому, я рассматрю пару моментов с которыми я столкнулся на практике, чтобы предостеречь людей, которым только предстоит с этим разбираться, а дальше жду ваших комментариев на тему решения указанных проблем после чего внесу изменения в пост и мы сделаем отличный туториал, который будет образцом работы с SQLite в Android не только для начинающих, но и для тех, кто уже знаком с основами и написал простые приложения.

Способы работы с БД

Существует три способа работы с данными в БД, которые сразу бросаются на ум:
1) Вы создаете пустую структуру базы данных. Пользователь работает с приложением(создает заметки, удаляет их) и база данных наполняется. Примером может служить приложение NotePad в демо-примерах developer.android.com или на вашем дроид-девайсе.
2) Вы уже имеете готовую БД, наполненную данными, которую нужно распространять с приложением, либо парсите данные из файла в assets.
3) Получать данные из сети, по мере необходимости.
Если есть какой-то еще один или два способа, то с радостью дополню данный список с вашей помощью.
Все основные туториалы расчитаны как раз на первый случай. Вы пишите запрос на создание структуры БД и выполняете этот запрос в методе onCreate() класса SQLiteOpenHelper, например так:
class MyDBHelper extends SQLiteOpenHelper {
    final String CREATE_TABLE = "CREATE TABLE myTable(...)";
    final String DB_NAME = "mySuperDB.db";
    Context mContext;
	
    public MyDBHelper(Context context, int dbVer){
	super(context, DB_NAME, null, dbVer);
	mContext = context;
    }
	
     @Override
     public void onCreate(SQLiteDatabase db) {
	 db.execSQL(CREATE_TABLE);
     }
	
     @Override
     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
	 //проверяете какая версия сейчас и делаете апдейт
	 db.execSQL("DROP TABLE IF EXISTS tableName");
	 onCreate(db);
     }
	...какой-то код
}

Примерно так. Более полный вариант класса и других составляющих можно посмотреть по ссылке внизу статьи.
Дополнительно можно переопределить методы onOpen(), getReadableDatabase()/getWritableDatаbase(), но обычно хватает того, что выше и методов выборки данных.
Далее, экземпляр этого класса создаем в нашем приложении при его запуске и выполняем запросы, то бишь проблемная часть пройдена. Почему она проблемная? Потому что, когда пользователь качает приложения с маркета, то не задумывается о вашей базе данных и может произойти что угодно. Скажем сеть пропала или процесс другой запустился, или вы написали уязвимый к ошибкам код.

Кстати, есть еще один момент, на который стоит обратить внимание. Переменную экземпляра нашего класса можно создать и хранить в объекте Application и обращаться по мере необходимости, но нужно не забывать вызывать метод close(), так как постоянный коннект к базе — это тяжелый ресурс. Кроме того могут быть коллизии при работе с базой из нескольких потоков.
Но есть и другой способ, например, создавать наш объект по мере необходимости обращения к БД. Думаю это вопрос предпочтения, но который также необходимо обсудить.

А теперь самое главное. Что, если нам понадобилось использовать уже сушествующую БД с данными в приложении?
Немного погуглив, Вы сразу наткнетесь на такую «замечательную статью» — www.reigndesign.com/blog/using-your-own-sqlite-database-in-android-applications в которой, как покажется, есть нужная панацея. Но не тут то было. В ней еще и ошибок несколько.

Вот они:
1) В методе createDataBase() строка:
SQLiteDatabase dbRead = getReadableDatabase();
и далее код… содержит crash приложения на НТС Desire, потому что получаем БД для чтения(она создается), но не закрывается.
Добавляем строкой ниже dbRead.close() и фикс готов, но момент спорный.
Вот что говорит дока на тему метода getReadableDatabase():
Create and/or open a database. This will be the same object returned by getWritableDatabase() unless some problem, such as a full disk, requires the database to be opened read-only. In that case, a read-only database object will be returned. If the problem is fixed, a future call to getWritableDatabase() may succeed, in which case the read-only database object will be closed and the read/write object will be returned in the future.
Like getWritableDatabase(), this method may take a long time to return, so you should not call it from the application main thread, including from ContentProvider.onCreate().

И так. Данный метод не стоит вызывать в главном потоке приложения. В остальном все понятно.
2) Ошибка: No such table android_metadata. Автор поста выкрутился, создав данную таблицу заранее в БД. Не знаю на сколько это правильный способ, но данная таблица создается в каждой sqlite-бд системой и содержит текущую локаль.
3) Ошибка: Unable to open database file. Здесь много мнений, разных мнений, которые Вы можете прочесть по ссылкам ниже.

stackoverflow.com/questions/3563728/random-exception-android-database-sqlite-sqliteexception-unable-to-open-database
groups.google.com/group/android-developers/browse_thread/thread/a0959c4059359d6f
code.google.com/p/android/issues/detail?id=949
stackoverflow.com/questions/4937934/unable-to-open-database-file-on-device-htc-desire
androidblogger.blogspot.com/2011/02/instable-android-and-unable-to-open.html

Возможно, что проблемы связаны с тем, что один поток блокирует БД и второй не может к ней обратиться, возможно проблема в правах доступа к приложению(было замечено, что чаще проблемы с БД проявляются на телефонах марки НТС именно на тех моделях, которые нельзя рутануть, хотя не только на них, например на планшетах Асер), но как бы то ни было проблемы эти есть.
Я склоняюсь к варианту, что проблема в потоках, не зря ведь нам не рекомендуют вызывать методы создания базы в главном потоке.

Возможно выходом из этого будет следующее решение(рассматривается вариант №2). Используя первый вариант работы с базой, наполнить ее данными после создания, например:
        @Override
	public void onCreate(SQLiteDatabase db) {
	    db.execSQL(CREATE_TABLE);
	    fillData(db);
	}
	
	private void fillData(SQLiteDatabase db) {
	    //разбираем файл data.xml лежащий например в assets 
            //и вставляем данные в базу	
            //либо читаем лежащие там же sql-скрипты и выполняем с помощью все того же db.execSQL() или аналогично	
	}

Данный подход еще нужно проверить на практике, но так как этот пост нацелен на выработку верного коллективного решения по данной тематике, то комментарии и пробы на даннную тему только приветствуются.
Мораль истории такова: если вы нашли какой-то хороший кусок кода для вашего решения, то проверьте его, не поленитесь, прежде чем копипастить в свой проект.

Заключение

Вцелом, данный пост показывает(касательно способа №2) как делать не надо, но и также содержит пару любопытных мыслей.
Метод getReadableDatabase() можно переопределить например так:
        @Override
	public synchronized SQLiteDatabase getReadableDatabase() {
		//Log.d(Constants.DEBUG_TAG, "getReadableDatabase() called");
		SQLiteDatabase db;
		try {
			db = super.getReadableDatabase();
		} 
		catch (SQLiteException e) {
			Log.d(Constants.DEBUG_TAG, e.getMessage());
			File dbFile = myContext.getDatabasePath(DB_NAME);
			Log.d(Constants.DEBUG_TAG,"db path="+dbFile.getAbsolutePath());
			//db = SQLiteDatabase.openDatabase(/*DB_PATH + DB_NAME*/ dbFile.getAbsolutePath(), null, SQLiteDatabase.NO_LOCALIZED_COLLATORS);
			db = SQLiteDatabase.openOrCreateDatabase(dbFile.getAbsolutePath(), null);
		}
		return db;
	}

Отличный туториал по данной теме тут — www.vogella.de/articles/AndroidSQLite/article.html

Кстати: следуя практике самой платформы, поле первичного ключа стоит называть "_id".

Пишите в комментарии свои используемые практики. Мы сделаем данный пост лучше для всех, а может и мир станет чуточку добрее.

UPD Только что проверил свой подход. Все работает в эмуляторе, но будьте осторожны.

public class DBHelper extends SQLiteOpenHelper {
    
	final static int DB_VER = 1;
	final static String DB_NAME = "todo.db";
	final String TABLE_NAME = "todo";
	final String CREATE_TABLE = "CREATE TABLE "+TABLE_NAME+
	                            "( _id INTEGER PRIMARY KEY , "+
	                            " todo TEXT)";
	final String DROP_TABLE = "DROP TABLE IF EXISTS "+TABLE_NAME;
	final String DATA_FILE_NAME = "data.txt";
	
	Context mContext;
	
	public DBHelper(Context context) {
		super(context, DB_NAME, null, DB_VER);
		Log.d(Constants.DEBUG_TAG,"constructor called");
		mContext = context;
	}
	
	@Override
        public void onCreate(SQLiteDatabase db) {
		Log.d(Constants.DEBUG_TAG,"onCreate() called");
		db.execSQL(CREATE_TABLE);
    	        fillData(db);
        }
    
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    	       db.execSQL(DROP_TABLE);
    	       onCreate(db);
        }
    
        private ArrayList<String> getData() {
    	      InputStream stream = null;
    	      ArrayList<String> list = new ArrayList<String>();
    	      try {
                    stream = mContext.getAssets().open(DATA_FILE_NAME);	
    	      }
    	      catch (IOException e) {
		   Log.d(Constants.DEBUG_TAG,e.getMessage());
	      }
    	
    	      DataInputStream dataStream = new DataInputStream(stream);
    	      String data = "";
    	      try {
    	             while( (data=dataStream.readLine()) != null ) {
    		             list.add(data);
    	             }
    	      }
    	      catch (IOException e) {
		    e.printStackTrace();	
	      }    
    	
    	      return list;
        }
    
        private void fillData(SQLiteDatabase db){
    	      ArrayList<String> data = getData();
    	      for(String dt:data) Log.d(Constants.DEBUG_TAG,"item="+dt);
    	
    	      if( db != null ){
    		    ContentValues values;
    		
    		    for(String dat:data){
    			values = new ContentValues();
    			values.put("todo", dat);
    			db.insert(TABLE_NAME, null, values);
    		    }
    	      }
    	      else {
    		    Log.d(Constants.DEBUG_TAG,"db null");
    	      }
        }
}


Файлик data.txt лежит в assets такой:
Zametka #1
Zametka #2
Zametka #3
Zametka #4

И класс приложения:
public class TODOApplication extends Application {
	private DBHelper mDbHelper;
	
	@Override
	public void onCreate(){
		super.onCreate();
		
		mDbHelper = new DBHelper(getApplicationContext());
		mDbHelper.getWritableDatabase();
	}
	
	@Override
	public void onLowMemory() {
		super.onLowMemory();
		mDbHelper.close();
	}
	
	@Override
	public void onTerminate(){
		super.onTerminate();
		mDbHelper.close();
	}
}

Отмечу, что данный класс используется только для демонстрации и проверки того, что произойдет при вызове методов getReadableDatabase()/getWritableDatabase() и создании базы. В реальных проектах код нужно адаптировать.
Кроме того в базе появилась табличка android_metadata(без моего участия), поэтому указанная выше ошибка решена.
Надеюсь кому-то пригодится.

Любопытные дополнения №1(от хабраюзера Kalobok)

Я пока совсем отказался от SQLiteOpenHelper — оказалось, что в нем невозможно создать базу на SD карте. Теоретически, то, что он возвращает, должно использоваться как путь к базе. На практике SQLiteOpenHelper иногда использует его, а иногда обходит стороной — зависит от того, открываем ли мы базу на чтение или запись, существует ли она уже и т.д. SQLiteOpenHelper.getWritableDatabase вызывает Context.openOrCreateDatabase, который, в свою очередь, использует Context.validateFilePath, чтобы получить полный путь к файлу. Там используется приватный метод Context.getDatabasesDir, переопределить который нельзя — приехали. База будет создана в стандартной директории.

А вот если мы вызвали SQLiteOpenHelper.getReadableDatabase, сначала он попытается вызвать все тот же getWritableDatabase. Но если это не получится, то он пойдет в обход Context.openOrCreateDatabase — сам вызовет Context.getDatabasePath (вот его-то мы можем подправить) и сам откроет нужную базу. Этот способ нас бы устроил, если бы он использовался всегда. Но увы. :(
Вобщем, задумка с этим хелпером была хорошая, а реализация — левой ногой с бодуна.
Tags:androidjavasqlite3database
Hubs: Development for Android
+39
141.4k 294
Comments 31

Popular right now