Приветствую всех дроидеров в эти непростые для нас времена.
Честно говоря, заколебала эта шумиха о патентах, войнах и т.д., но в данной статье речь пойдет не об этом.
Я не собирался писать статью на данную тему, так как везде всего полно о работе с базой данных в Android и вроде бы все просто, но уж очень надоело получать репорты об ошибках, ошибках специфичных и связанных с БД.
Поэтому, я рассматрю пару моментов с которыми я столкнулся на практике, чтобы предостеречь людей, которым только предстоит с этим разбираться, а дальше жду ваших комментариев на тему решения указанных проблем после чего внесу изменения в пост и мы сделаем отличный туториал, который будет образцом работы с SQLite в Android не только для начинающих, но и для тех, кто уже знаком с основами и написал простые приложения.
Способы работы с БД
Существует три способа работы с данными в БД, которые сразу бросаются на ум:
1) Вы создаете пустую структуру базы данных. Пользователь работает с приложением(создает заметки, удаляет их) и база данных наполняется. Примером может служить приложение NotePad в демо-примерах developer.android.com или на вашем дроид-девайсе.
2) Вы уже имеете готовую БД, наполненную данными, которую нужно распространять с приложением, либо парсите данные из файла в assets.
3) Получать данные из сети, по мере необходимости.
Если есть какой-то еще один или два способа, то с радостью дополню данный список с вашей помощью.
Все основные туториалы расчитаны как раз на первый случай. Вы пишите запрос на создание структуры БД и выполняете этот запрос в методе onCreate() класса SQLiteOpenHelper, например так:
Примерно так. Более полный вариант класса и других составляющих можно посмотреть по ссылке внизу статьи.
Дополнительно можно переопределить мет��ды 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). Используя первый вариант работы с базой, наполнить ее данными после создания, например:
Данный подход еще нужно проверить на практике, но так как этот пост нацелен на выработку верного коллективного решения по данной тематике, то комментарии и пробы на даннную тему только приветствуются.
Мораль истории такова: если вы нашли какой-то хороший кусок кода для вашего решения, то проверьте его, не поленитесь, прежде чем копипастить в свой проект.
Заключение
Вцелом, данный пост показывает(касательно способа №2) как делать не надо, но и также содержит пару любопытных мыслей.
Метод getReadableDatabase() можно переопределить например так:
Отличный туториал по данной теме тут — www.vogella.de/articles/AndroidSQLite/article.html
Кстати: следуя практике самой платформы, поле первичного ключа стоит называть "_id".
Пишите в комментарии свои используемые практики. Мы сделаем данный пост лучше для всех, а может и мир станет чуточку добрее.
UPD Только что проверил свой подход. Все работает в эмуляторе, но будьте осторожны.
Файлик data.txt лежит в assets такой:
Zametka #1
Zametka #2
Zametka #3
Zametka #4
И класс приложения:
Отмечу, что данный класс используется только для демонстрации и проверки того, что произойдет при вызове методов getReadableDatabase()/getWritableDatabase() и создании базы. В реальных проектах код нужно адаптировать.
Кроме того в базе появилась табличка android_metadata(без моего участия), поэтому указанная выше ошибка решена.
Надеюсь кому-то пригодится.
Любопытные дополнения №1(от хабраюзера Kalobok)
Я пока совсем отказался от SQLiteOpenHelper — оказалось, что в нем невозможно создать базу на SD карте. Теоретически, то, что он возвращает, должно использоваться как путь к базе. На практике SQLiteOpenHelper иногда использует его, а иногда обходит стороной — зависит от того, открываем ли мы базу на чтение или запись, существует ли она уже и т.д. SQLiteOpenHelper.getWritableDatabase вызывает Context.openOrCreateDatabase, который, в свою очередь, использует Context.validateFilePath, чтобы получить полный путь к файлу. Там используется приватный метод Context.getDatabasesDir, переопределить который нельзя — приехали. База будет создана в стандартной директории.
А вот если мы вызвали SQLiteOpenHelper.getReadableDatabase, сначала он попытается вызвать все тот же getWritableDatabase. Но если это не получится, то он пойдет в обход Context.openOrCreateDatabase — сам вызовет Context.getDatabasePath (вот его-то мы можем подправить) и сам откроет нужную базу. Этот способ нас бы устроил, если бы он использовался всегда. Но увы. :(
Вобщем, задумка с этим хелпером была хорошая, а реализация — левой ногой с бодуна.
Честно говоря, заколебала эта шумиха о патентах, войнах и т.д., но в данной статье речь пойдет не об этом.
Я не собирался писать статью на данную тему, так как везде всего полно о работе с базой данных в 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 (вот его-то мы можем подправить) и сам откроет нужную базу. Этот способ нас бы устроил, если бы он использовался всегда. Но увы. :(
Вобщем, задумка с этим хелпером была хорошая, а реализация — левой ногой с бодуна.