Accès simultané aux bases de données
Même article sur mon blog(j'aime plus le formatage)
J'ai écrit un petit article qui décrit comment rendre l'accès à votre base de données Android thread safe.
En supposant que vous ayez votre propre SQLiteOpenHelper .
public class DatabaseHelper extends SQLiteOpenHelper { ... }
Maintenant, vous voulez écrire des données dans la base de données dans des threads séparés.
// 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();
Vous obtiendrez le message suivant dans votre logcat et une de vos modifications ne sera pas écrite.
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
Cela se produit parce qu'à chaque fois que vous créez un nouveau SQLiteOpenHelper vous établissez en fait une nouvelle connexion à la base de données. Si vous essayez d'écrire dans la base de données à partir de connexions distinctes réelles en même temps, l'une d'entre elles échouera. (d'après la réponse ci-dessus)
Pour utiliser une base de données avec plusieurs threads, nous devons nous assurer que nous utilisons une seule connexion à la base de données.
Faisons une classe singleton Gestionnaire de base de données qui contiendra et renverra un seul SQLiteOpenHelper objet.
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();
}
}
Le code mis à jour qui écrit les données dans la base de données dans des fils séparés ressemblera à ceci.
// 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();
Cela vous apportera un autre crash.
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase
Puisque nous n'utilisons qu'une seule connexion à la base de données, la méthode getDatabase() retourner la même instance de SQLiteDatabase pour Thread1 y Thread2 . Ce qui se passe, Thread1 peut fermer la base de données, tandis que Thread2 l'utilise toujours. C'est pourquoi nous avons IllegalStateException crash.
Nous devons nous assurer que personne n'utilise la base de données et seulement ensuite la fermer. Certaines personnes sur stackoveflow recommandent de ne jamais fermer la base de données. SQLiteDatabase . Il en résultera le message logcat suivant.
Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed
Echantillon de travail
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();
}
}
}
Utilisez-le comme suit.
SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way
Chaque fois que vous avez besoin d'une base de données, vous devez appeler openDatabase() méthode de DatabaseManager la classe. Dans cette méthode, nous avons un compteur, qui indique combien de fois la base de données est ouverte. S'il est égal à un, cela signifie que nous devons créer une nouvelle connexion à la base de données, sinon, la connexion à la base de données est déjà créée.
La même chose se produit dans closeDatabase() méthode. Chaque fois que nous appelons cette méthode, le compteur diminue, et s'il arrive à zéro, nous fermons la connexion à la base de données.
Maintenant, vous devriez être en mesure d'utiliser votre base de données et d'être sûr qu'elle est thread safe.
12 votes
Quoi qu'il en soit, n'oubliez pas de désinfecter vos entrées si votre fournisseur de contenu (ou votre interface SQLite) est accessible au public !
43 votes
Je peux vous dire que vous ne devez absolument pas accéder aux données à partir du thread de l'interface utilisateur.
0 votes
@EdwardFalk Pourquoi pas ? Il y a sûrement des cas d'utilisation où il est valable de le faire ?
8 votes
Si vous effectuez des entrées/sorties, des accès au réseau, etc. à partir du thread de l'interface utilisateur, l'ensemble du dispositif se fige jusqu'à ce que l'opération soit terminée. Si l'opération se termine en 1/20e de seconde, c'est parfait. Si cela prend plus de temps, l'expérience de l'utilisateur est mauvaise.