Jednoczesny dostęp do bazy danych
Ten sam artykuł na moim blogu (lubię formatować więcej)
Napisałem mały artykuł opisujący, jak zapewnić bezpieczny dostęp do wątku bazy danych Androida.
Zakładając, że masz własny SQLiteOpenHelper .
public class DatabaseHelper extends SQLiteOpenHelper { ... }
Teraz chcesz zapisać dane w bazie danych w osobnych wątkach.
// 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();
Otrzymasz następujący komunikat w logcat i jedna ze zmian nie zostanie zapisana.
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
Dzieje się tak, ponieważ za każdym razem, gdy tworzysz nowy obiekt SQLiteOpenHelper , faktycznie tworzysz nowe połączenie z bazą danych. Jeśli spróbujesz pisać do bazy danych z rzeczywistych odrębnych połączeń w tym samym czasie, jedno zakończy się niepowodzeniem. (z odpowiedzi powyżej)
Aby korzystać z bazy danych z wieloma wątkami, musimy upewnić się, że korzystamy z jednego połączenia z bazą danych.
Stwórzmy menedżera bazy danych klasy singleton, który przechowa i zwróci pojedynczy obiekt 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();
}
}
Zaktualizowany kod, który zapisuje dane do bazy danych w osobnych wątkach, będzie wyglądał następująco.
// 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();
Spowoduje to kolejną awarię.
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase
Skoro jesteśmy przy użyciu tylko jednego połączenia z bazą danych, metoda getDatabase () zwrócić tę samą instancję SQLiteDatabase obiektu dla thread1 i thread2 . Co się dzieje, Thread1 może zamknąć bazę danych, podczas gdy Thread2 nadal z niej korzysta. Właśnie dlatego mamy awarię IllegalStateException .
Musimy upewnić się, że nikt nie korzysta z bazy danych, a dopiero potem ją zamknąć. Niektórzy ludzie na stackoveflow zalecają, aby nigdy nie zamykać SQLiteDatabase . Spowoduje to wyświetlenie następującego komunikatu logcat.
Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed
Próbka robocza
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();
}
}
}
Użyj go w następujący sposób.
SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way
Za każdym razem trzeba bazę danych należy zadzwonić openDatabase () metodę DatabaseManager klasie. Wewnątrz tej metody mamy licznik wskazujący, ile razy baza danych jest otwierana. Jeśli jest równy jeden, oznacza to, że musimy utworzyć nowe połączenie z bazą danych, jeśli nie, połączenie z bazą danych jest już utworzone.
To samo dzieje się w metodzie closeDatabase () . Za każdym razem, gdy wywołujemy tę metodę, licznik jest zmniejszany, za każdym razem, gdy osiąga zero, zamykamy połączenie z bazą danych.
Teraz powinieneś być w stanie korzystać z bazy danych i mieć pewność, że jest ona bezpieczna dla wątków.