Каковы лучшие практики для SQLite на Android?
Что будет считаться наилучшей практикой при выполнении запросов к базе данных SQLite в приложении Android?
Безопасно ли запускать вставки, удалять и выбирать запросы из doInBackground AsyncTask? Или я должен использовать поток пользовательского интерфейса? Я полагаю, что запросы к базе данных могут быть "тяжелыми" и не должны использовать поток пользовательского интерфейса, поскольку он может заблокировать приложение - в результате приложение не отвечает (ANR).
Если у меня есть несколько AsyncTasks, они должны совместно использовать соединение или они должны открывать соединение каждый?
Есть ли лучшие практики для этих сценариев?
10 ответов
Вставки, обновления, удаления и чтения, как правило, в порядке из нескольких потоков, но ответ Брэда не является правильным. Вы должны быть осторожны с тем, как вы создаете свои соединения и используете их. Существуют ситуации, когда ваши вызовы обновления не будут работать, даже если ваша база данных не будет повреждена.
Основной ответ.
Объект SqliteOpenHelper удерживает одно соединение с базой данных. Похоже, предлагает вам соединение для чтения и записи, но на самом деле это не так. Вызовите только для чтения, и вы получите соединение для записи базы данных независимо от.
Итак, один вспомогательный экземпляр, одно соединение с БД. Даже если вы используете его из нескольких потоков, по одному соединению за раз. Объект SqliteDatabase использует блокировки Java для сохранения сериализации доступа. Таким образом, если 100 потоков имеют один экземпляр базы данных, вызовы действительной базы данных на диске сериализуются.
Итак, один помощник, одно соединение с БД, которое сериализовано в коде Java. Один поток, 1000 потоков, если вы используете один экземпляр помощника, совместно используемый ими, весь ваш код доступа к БД является последовательным. И жизнь хороша (иш).
Если вы попытаетесь одновременно выполнить запись в базу данных из разных соединений, произойдет сбой. Это не будет ждать, пока первое будет сделано, а затем напишите. Это просто не будет писать ваши изменения. Хуже того, если вы не вызовете правильную версию вставки / обновления для базы данных SQLiteD, вы не получите исключение. Вы просто получите сообщение в своем LogCat, и это будет оно.
Итак, несколько потоков? Используйте один помощник. Период. Если вы ЗНАЕТЕ, что будет писать только один поток, вы МОЖЕТЕ использовать несколько соединений, и ваше чтение будет быстрее, но покупатель остерегается. Я не так много тестировал.
Вот блог с гораздо более подробной информацией и примером приложения.
- Блокировка Android Sqlite (обновленная ссылка 18.06.2012)
- Пример Android-базы данных-блокировки-столкновений от touchlab на GitHub
Грей и я на самом деле завершили работу над инструментом ORM, основанным на его Ormlite, который изначально работает с реализациями баз данных Android и придерживается структуры безопасного создания / вызова, которую я описал в посте блога. Это должно быть очень скоро. Взглянуть.
А пока есть пост в блоге:
Также проверьте ответвление по 2point0 из ранее упомянутого примера блокировки:
Параллельный доступ к базе данных
Та же статья в моем блоге (мне больше нравится форматирование)
Я написал небольшую статью, в которой описывается, как сделать доступ к вашей базе данных Android безопасным.
Предполагая, что у вас есть свой собственный SQLiteOpenHelper.
public class DatabaseHelper extends SQLiteOpenHelper { ... }
Теперь вы хотите записать данные в базу данных в отдельных потоках.
// Thread 1
Context context = getApplicationContext();
DatabaseHelper helper = new DatabaseHelper(context);
SQLiteDatabase database = helper.getWritableDatabase();
database.insert(…);
database.close();
// Thread 2
Context context = getApplicationContext();
DatabaseHelper helper = new DatabaseHelper(context);
SQLiteDatabase database = helper.getWritableDatabase();
database.insert(…);
database.close();
Вы получите следующее сообщение в своем logcat, и одно из ваших изменений не будет записано.
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
Это происходит потому, что каждый раз, когда вы создаете новый объект SQLiteOpenHelper, вы фактически устанавливаете новое соединение с базой данных. Если вы попытаетесь одновременно выполнить запись в базу данных из разных соединений, произойдет сбой. (из ответа выше)
Чтобы использовать базу данных с несколькими потоками, мы должны убедиться, что мы используем одно соединение с базой данных.
Давайте создадим одноэлементный класс Database Manager, который будет содержать и возвращать один объект SQLiteOpenHelper.
public class DatabaseManager {
private static DatabaseManager instance;
private static SQLiteOpenHelper mDatabaseHelper;
public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
if (instance == null) {
instance = new DatabaseManager();
mDatabaseHelper = helper;
}
}
public static synchronized DatabaseManager getInstance() {
if (instance == null) {
throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
" is not initialized, call initialize(..) method first.");
}
return instance;
}
public SQLiteDatabase getDatabase() {
return new mDatabaseHelper.getWritableDatabase();
}
}
Обновленный код, который записывает данные в базу данных в отдельных потоках, будет выглядеть следующим образом.
// In your application class
DatabaseManager.initializeInstance(new MySQLiteOpenHelper());
// Thread 1
DatabaseManager manager = DatabaseManager.getInstance();
SQLiteDatabase database = manager.getDatabase()
database.insert(…);
database.close();
// Thread 2
DatabaseManager manager = DatabaseManager.getInstance();
SQLiteDatabase database = manager.getDatabase()
database.insert(…);
database.close();
Это принесет вам еще одну аварию.
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase
Поскольку мы используем только одно соединение с базой данных, метод getDatabase() возвращает один и тот же экземпляр объекта SQLiteDatabase для Thread1 и Thread2. Что происходит, Thread1 может закрыть базу данных, в то время как Thread2 все еще использует ее. Вот почему у нас происходит сбой IllegalStateException.
Мы должны убедиться, что никто не использует базу данных, и только потом закрывать ее. Некоторые люди в stackoveflow рекомендуют никогда не закрывать вашу базу данных SQLite. Это не только звучит глупо, но и чтит вас следующим сообщением logcat.
Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed
Рабочий образец
public class DatabaseManager {
private int mOpenCounter;
private static DatabaseManager instance;
private static SQLiteOpenHelper mDatabaseHelper;
private SQLiteDatabase mDatabase;
public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
if (instance == null) {
instance = new DatabaseManager();
mDatabaseHelper = helper;
}
}
public static synchronized DatabaseManager getInstance() {
if (instance == null) {
throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
" is not initialized, call initializeInstance(..) method first.");
}
return instance;
}
public synchronized SQLiteDatabase openDatabase() {
mOpenCounter++;
if(mOpenCounter == 1) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return mDatabase;
}
public synchronized void closeDatabase() {
mOpenCounter--;
if(mOpenCounter == 0) {
// Closing database
mDatabase.close();
}
}
}
Используйте это следующим образом.
SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way
Каждый раз, когда вам нужна база данных, вы должны вызывать метод openDatabase() класса DatabaseManager. Внутри этого метода у нас есть счетчик, который показывает, сколько раз открывается база данных. Если он равен единице, это означает, что нам нужно создать новое соединение с базой данных, если нет, соединение с базой данных уже создано.
То же самое происходит в методе closeDatabase(). Каждый раз, когда мы вызываем этот метод, счетчик уменьшается, когда он равен нулю, мы закрываем соединение с базой данных.
Теперь вы должны быть в состоянии использовать вашу базу данных и быть уверенным, что она безопасна для потоков.
- Использовать
Thread
или жеAsyncTask
для длительных операций (50 мс +). Протестируйте свое приложение, чтобы увидеть, где это. Большинство операций (вероятно) не требуют потока, потому что большинство операций (вероятно) включают только несколько строк. Используйте поток для массовых операций. - Поделиться одним
SQLiteDatabase
экземпляр для каждой БД на диске между потоками и реализовать систему подсчета для отслеживания открытых соединений.
Есть ли лучшие практики для этих сценариев?
Разделите статическое поле между всеми вашими классами. Я имел обыкновение держать синглтон для этого и других вещей, которыми нужно поделиться. Следует также использовать схему подсчета (обычно с использованием AtomicInteger), чтобы вы никогда не закрывали базу данных раньше времени и не оставляли ее открытой.
Мое решение:
Самую свежую версию смотрите на https://github.com/JakarCo/databasemanager, но я постараюсь и здесь обновлять код. Если вы хотите понять мое решение, посмотрите код и прочитайте мои заметки. Мои заметки обычно очень полезны.
- скопируйте / вставьте код в новый файл с именем
DatabaseManager
, (или загрузите его с github) - простираться
DatabaseManager
и реализоватьonCreate
а такжеonUpgrade
как обычно. Вы можете создать несколько подклассов одногоDatabaseManager
класс для того, чтобы иметь разные базы данных на диске. - Создайте свой подкласс и позвоните
getDb()
использоватьSQLiteDatabase
учебный класс. - Вызов
close()
для каждого созданного вами подкласса
Код для копирования / вставки:
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import java.util.concurrent.ConcurrentHashMap;
/** Extend this class and use it as an SQLiteOpenHelper class
*
* DO NOT distribute, sell, or present this code as your own.
* for any distributing/selling, or whatever, see the info at the link below
*
* Distribution, attribution, legal stuff,
* See https://github.com/JakarCo/databasemanager
*
* If you ever need help with this code, contact me at support@androidsqlitelibrary.com (or support@jakar.co )
*
* Do not sell this. but use it as much as you want. There are no implied or express warranties with this code.
*
* This is a simple database manager class which makes threading/synchronization super easy.
*
* Extend this class and use it like an SQLiteOpenHelper, but use it as follows:
* Instantiate this class once in each thread that uses the database.
* Make sure to call {@link #close()} on every opened instance of this class
* If it is closed, then call {@link #open()} before using again.
*
* Call {@link #getDb()} to get an instance of the underlying SQLiteDatabse class (which is synchronized)
*
* I also implement this system (well, it's very similar) in my <a href="http://androidslitelibrary.com">Android SQLite Libray</a> at http://androidslitelibrary.com
*
*
*/
abstract public class DatabaseManager {
/**See SQLiteOpenHelper documentation
*/
abstract public void onCreate(SQLiteDatabase db);
/**See SQLiteOpenHelper documentation
*/
abstract public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
/**Optional.
* *
*/
public void onOpen(SQLiteDatabase db){}
/**Optional.
*
*/
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
/**Optional
*
*/
public void onConfigure(SQLiteDatabase db){}
/** The SQLiteOpenHelper class is not actually used by your application.
*
*/
static private class DBSQLiteOpenHelper extends SQLiteOpenHelper {
DatabaseManager databaseManager;
private AtomicInteger counter = new AtomicInteger(0);
public DBSQLiteOpenHelper(Context context, String name, int version, DatabaseManager databaseManager) {
super(context, name, null, version);
this.databaseManager = databaseManager;
}
public void addConnection(){
counter.incrementAndGet();
}
public void removeConnection(){
counter.decrementAndGet();
}
public int getCounter() {
return counter.get();
}
@Override
public void onCreate(SQLiteDatabase db) {
databaseManager.onCreate(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
databaseManager.onUpgrade(db, oldVersion, newVersion);
}
@Override
public void onOpen(SQLiteDatabase db) {
databaseManager.onOpen(db);
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
databaseManager.onDowngrade(db, oldVersion, newVersion);
}
@Override
public void onConfigure(SQLiteDatabase db) {
databaseManager.onConfigure(db);
}
}
private static final ConcurrentHashMap<String,DBSQLiteOpenHelper> dbMap = new ConcurrentHashMap<String, DBSQLiteOpenHelper>();
private static final Object lockObject = new Object();
private DBSQLiteOpenHelper sqLiteOpenHelper;
private SQLiteDatabase db;
private Context context;
/** Instantiate a new DB Helper.
* <br> SQLiteOpenHelpers are statically cached so they (and their internally cached SQLiteDatabases) will be reused for concurrency
*
* @param context Any {@link android.content.Context} belonging to your package.
* @param name The database name. This may be anything you like. Adding a file extension is not required and any file extension you would like to use is fine.
* @param version the database version.
*/
public DatabaseManager(Context context, String name, int version) {
String dbPath = context.getApplicationContext().getDatabasePath(name).getAbsolutePath();
synchronized (lockObject) {
sqLiteOpenHelper = dbMap.get(dbPath);
if (sqLiteOpenHelper==null) {
sqLiteOpenHelper = new DBSQLiteOpenHelper(context, name, version, this);
dbMap.put(dbPath,sqLiteOpenHelper);
}
//SQLiteOpenHelper class caches the SQLiteDatabase, so this will be the same SQLiteDatabase object every time
db = sqLiteOpenHelper.getWritableDatabase();
}
this.context = context.getApplicationContext();
}
/**Get the writable SQLiteDatabase
*/
public SQLiteDatabase getDb(){
return db;
}
/** Check if the underlying SQLiteDatabase is open
*
* @return whether the DB is open or not
*/
public boolean isOpen(){
return (db!=null&&db.isOpen());
}
/** Lowers the DB counter by 1 for any {@link DatabaseManager}s referencing the same DB on disk
* <br />If the new counter is 0, then the database will be closed.
* <br /><br />This needs to be called before application exit.
* <br />If the counter is 0, then the underlying SQLiteDatabase is <b>null</b> until another DatabaseManager is instantiated or you call {@link #open()}
*
* @return true if the underlying {@link android.database.sqlite.SQLiteDatabase} is closed (counter is 0), and false otherwise (counter > 0)
*/
public boolean close(){
sqLiteOpenHelper.removeConnection();
if (sqLiteOpenHelper.getCounter()==0){
synchronized (lockObject){
if (db.inTransaction())db.endTransaction();
if (db.isOpen())db.close();
db = null;
}
return true;
}
return false;
}
/** Increments the internal db counter by one and opens the db if needed
*
*/
public void open(){
sqLiteOpenHelper.addConnection();
if (db==null||!db.isOpen()){
synchronized (lockObject){
db = sqLiteOpenHelper.getWritableDatabase();
}
}
}
}
База данных очень гибкая с многопоточностью. Мои приложения попадают в их базы данных из разных потоков одновременно, и это нормально. В некоторых случаях у меня есть несколько процессов, одновременно работающих с БД, и это тоже отлично работает.
Ваши асинхронные задачи - используйте одно и то же соединение, когда можете, но если нужно, все нормально, чтобы получить доступ к БД из разных задач.
После нескольких часов борьбы с этим я обнаружил, что вы можете использовать только один вспомогательный объект БД на выполнение БД. Например,
for(int x = 0; x < someMaxValue; x++)
{
db = new DBAdapter(this);
try
{
db.addRow
(
NamesStringArray[i].toString(),
StartTimeStringArray[i].toString(),
EndTimeStringArray[i].toString()
);
}
catch (Exception e)
{
Log.e("Add Error", e.toString());
e.printStackTrace();
}
db.close();
}
по отношению к:
db = new DBAdapter(this);
for(int x = 0; x < someMaxValue; x++)
{
try
{
// ask the database manager to add a row given the two strings
db.addRow
(
NamesStringArray[i].toString(),
StartTimeStringArray[i].toString(),
EndTimeStringArray[i].toString()
);
}
catch (Exception e)
{
Log.e("Add Error", e.toString());
e.printStackTrace();
}
}
db.close();
создание нового DBAdapter каждый раз при повторении цикла было единственным способом, которым я мог получить свои строки в базу данных через мой вспомогательный класс.
Ответ Дмитрия отлично подходит для моего случая. Я думаю, что лучше объявить функцию синхронизированной. по крайней мере, в моем случае это вызвало бы исключение нулевого указателя в противном случае, например, getWritableDatabase еще не возвращен в одном потоке, а openDatabse вызывается в другом потоке.
public synchronized SQLiteDatabase openDatabase() {
if(mOpenCounter.incrementAndGet() == 1) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return mDatabase;
}
Вы можете попробовать применить новый подход к архитектуре, объявленный на Google I/O 2017.
Он также включает новую библиотеку ORM под названием Room
Он содержит три основных компонента: @Entity, @Dao и @Database
User.java
@Entity
public class User {
@PrimaryKey
private int uid;
@ColumnInfo(name = "first_name")
private String firstName;
@ColumnInfo(name = "last_name")
private String lastName;
// Getters and setters are ignored for brevity,
// but they're required for Room to work.
}
UserDao.java
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND "
+ "last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}
AppDatabase.java
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
Насколько я понимаю API-интерфейсы SQLiteDatabase, если у вас многопоточное приложение, вы не можете позволить себе иметь более одного объекта SQLiteDatabase, указывающего на одну базу данных.
Объект определенно может быть создан, но вставка / обновление завершаются неудачно, если разные потоки / процессы (тоже) начинают использовать разные объекты SQLiteDatabase (например, как мы используем в JDBC Connection).
Единственное решение здесь - придерживаться 1 объекта SQLiteDatabase, и всякий раз, когда startTransaction() используется в более чем 1 потоке, Android управляет блокировкой между различными потоками и позволяет только одному потоку одновременно иметь эксклюзивный доступ к обновлениям.
Также вы можете выполнять "Чтение" из базы данных и использовать один и тот же объект SQLiteDatabase в другом потоке (в то время как другой поток записывает), и никогда не произойдет повреждение базы данных, то есть "чтение потока" не будет считывать данные из базы данных до тех пор, пока " запись потока "фиксирует данные, хотя оба используют один и тот же объект SQLiteDatabase.
Это отличается от того, как объект соединения находится в JDBC, где, если вы передадите (используете то же самое) объект соединения между потоками чтения и записи, то мы, вероятно, также будем печатать незафиксированные данные.
В моем корпоративном приложении я пытаюсь использовать условные проверки, чтобы потоку пользовательского интерфейса никогда не приходилось ждать, в то время как поток BG содержит объект SQLiteDatabase (исключительно). Я пытаюсь предсказать действия пользовательского интерфейса и отложить запуск потока BG на "х" секунд. Также можно поддерживать PriorityQueue для управления раздачей объектов Соединения SQLiteDatabase, чтобы поток пользовательского интерфейса получал его первым.
После некоторых проблем, я думаю, я понял, почему я иду не так, как надо.
Я написал класс обертки базы данных, который включал close()
который называется помощник близко, как зеркало open()
который вызвал getWriteableDatabase, а затем мигрировал в ContentProvider
, Модель для ContentProvider
не использует SQLiteDatabase.close()
который я считаю большой подсказкой, поскольку код использует getWriteableDatabase
В некоторых случаях я все еще делал прямой доступ (запросы проверки экрана в основном, поэтому я перешел на модель getWriteableDatabase/rawQuery.
Я использую синглтон, и в закрытой документации есть немного зловещий комментарий
Закройте любой открытый объект базы данных
(мой смелый).
Поэтому у меня периодически возникали сбои, когда я использовал фоновые потоки для доступа к базе данных, и они выполнялись одновременно с передним планом.
Я так думаю close()
заставляет базу данных закрываться независимо от любых других потоков, содержащих ссылки - так close()
само по себе не просто отмена соответствия getWriteableDatabase
но принудительно закрывайте любые открытые запросы. В большинстве случаев это не проблема, поскольку код является однопоточным, но в многопоточных случаях всегда есть вероятность открытия и закрытия из-за синхронизации.
Прочитав комментарии в другом месте, в которых объясняется, что экземпляр кода SqLiteDatabaseHelper имеет значение, единственный раз, когда вам нужно закрытие, - это ситуация, когда вы хотите сделать резервную копию, и вы хотите принудительно закрыть все соединения и заставить SqLite выполнить запишите все кэшированные данные, которые могли бы слоняться - другими словами, остановите всю активность базы данных приложения, закройте на случай, если Помощник потерял отслеживание, выполните любое действие на уровне файлов (резервное копирование / восстановление), затем начните все заново.
Несмотря на то, что это хорошая идея, чтобы попытаться закрыть контролируемым образом, реальность такова, что Android оставляет за собой право уничтожить вашу ВМ, поэтому любое закрытие снижает риск того, что кэшированные обновления не будут записаны, но это не может быть гарантировано, если устройство подчеркнуто, и если вы правильно освободили свои курсоры и ссылки на базы данных (которые не должны быть статическими членами), то помощник все равно закроет базу данных.
Итак, я считаю, что подход заключается в следующем:
Используйте getWriteableDatabase для открытия из одноэлементной оболочки. (Я использовал производный класс приложения для предоставления контекста приложения из статического кода, чтобы удовлетворить потребность в контексте).
Никогда не звоните напрямую.
Никогда не храните результирующую базу данных ни в каком объекте, который не имеет очевидной области видимости и использует подсчет ссылок для запуска неявного close().
Если вы выполняете обработку на уровне файлов, остановите все действия с базой данных, а затем вызовите close на тот случай, если существует неконтролируемый поток, исходя из предположения, что вы пишете правильные транзакции, поэтому сбойный поток завершится сбоем, и закрытая база данных, по крайней мере, будет иметь правильные транзакции. чем потенциально файловая копия частичной транзакции.
Я знаю, что ответ запаздывает, но лучший способ выполнить sqlite-запросы в Android - через собственного провайдера контента. Таким образом, пользовательский интерфейс не связан с классом базы данных (класс, который расширяет класс SQLiteOpenHelper). Также запросы выполняются в фоновом потоке (Cursor Loader).