Acesso simultâneo ao banco de dados
Mesmo artigo no meu blog (eu gosto de formatar mais)
Escrevi um pequeno artigo que descreve como tornar seguro o acesso ao seu thread de banco de dados do Android.
Supondo que você tenha seu próprio SQLiteOpenHelper .
public class DatabaseHelper extends SQLiteOpenHelper { ... }
Agora você deseja gravar dados no banco de dados em threads separados.
// 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();
Você receberá a seguinte mensagem no seu logcat e uma de suas alterações não será gravada.
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
Isso está acontecendo porque toda vez que você cria um novo objeto SQLiteOpenHelper, está realmente fazendo uma nova conexão com o banco de dados. Se você tentar gravar no banco de dados a partir de conexões distintas reais ao mesmo tempo, uma delas falhará. (da resposta acima)
Para usar o banco de dados com vários threads, precisamos garantir que estamos usando uma conexão com o banco de dados.
Vamos criar a classe singleton Database Manager, que manterá e retornará um único objeto 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();
}
}
O código atualizado que grava dados no banco de dados em threads separados terá a seguinte aparência.
// 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();
Isso trará a você outra falha.
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase
Como estamos usando apenas uma conexão com o banco de dados, o método getDatabase () retorna a mesma instância do objeto SQLiteDatabase para Thread1 e Thread2 . O que está acontecendo, o Thread1 pode fechar o banco de dados, enquanto o Thread2 ainda o está usando. É por isso que temos a falha IllegalStateException .
Precisamos garantir que ninguém esteja usando o banco de dados e só depois fechá-lo. Algumas pessoas no stackoveflow recomendam nunca fechar seu SQLiteDatabase . Isso resultará na seguinte mensagem de logcat.
Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed
Amostra de trabalho
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();
}
}
}
Use-o da seguinte maneira.
SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way
Toda vez que você precisar de banco de dados, chame o método openDatabase () da classe DatabaseManager . Dentro desse método, temos um contador, que indica quantas vezes o banco de dados é aberto. Se for igual a um, significa que precisamos criar uma nova conexão com o banco de dados; caso contrário, a conexão com o banco de dados já está criada.
O mesmo acontece no método closeDatabase () . Toda vez que chamamos esse método, o contador diminui; sempre que chega a zero, estamos fechando a conexão com o banco de dados.
Agora você deve poder usar seu banco de dados e ter certeza de que ele é seguro para threads.