721 votes

Comment éviter les problèmes de concurrence lors de l'utilisation de SQLite sur Android ?

Quelles seraient les meilleures pratiques pour exécuter des requêtes sur une base de données SQLite dans une application Android ?

Est-il sûr d'exécuter des insertions, des suppressions et des requêtes de sélection à partir du doInBackground d'une AsyncTask ? Ou dois-je utiliser le thread de l'interface utilisateur ? Je suppose que les requêtes de base de données peuvent être "lourdes" et qu'elles ne devraient pas utiliser le thread de l'interface utilisateur, car elles peuvent bloquer l'application - ce qui entraînerait une erreur d'exécution. L'application ne répond pas (ANR).

Si j'ai plusieurs AsyncTasks, doivent-elles partager une connexion ou doivent-elles ouvrir une connexion chacune ?

Existe-t-il des pratiques exemplaires pour ces scénarios ?

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 ?

643voto

Kevin Galligan Points 6101

Les insertions, les mises à jour, les suppressions et les lectures sont généralement acceptables à partir de plusieurs threads, mais Brad's respuesta n'est pas correct. Vous devez faire attention à la façon dont vous créez vos connexions et les utilisez. Dans certaines situations, vos appels de mise à jour échoueront, même si votre base de données n'est pas corrompue.

La réponse de base.

L'objet SqliteOpenHelper conserve une connexion à la base de données. Il semble vous offrir une connexion en lecture et écriture, mais ce n'est pas le cas. Appelez le read-only, et vous obtiendrez la connexion en écriture à la base de données.

Donc, une instance d'aide, une connexion à la base de données. Même si vous l'utilisez depuis plusieurs threads, une seule connexion à la fois. L'objet SqliteDatabase utilise des verrous java pour garder l'accès sérialisé. Ainsi, si 100 threads ont une instance db, les appels à la base de données réelle sur disque sont sérialisés.

Donc, une aide, une connexion à la base de données, qui est sérialisée en code Java. Un thread, 1000 threads, si vous utilisez une instance de helper partagée entre eux, tout votre code d'accès à la base de données est sérialisé. Et la vie est belle (ou presque).

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. Elle n'attendra pas que la première soit terminée pour écrire. Il n'écrira tout simplement pas votre modification. Pire encore, si vous n'appelez pas la bonne version de insert/update sur la SQLiteDatabase, vous ne recevrez pas d'exception. Vous obtiendrez simplement un message dans votre LogCat, et ce sera tout.

Alors, plusieurs fils ? Utilisez une seule aide. Point final. Si vous savez qu'un seul thread va écrire, vous pouvez peut-être utiliser des connexions multiples et vos lectures seront plus rapides, mais attention à l'acheteur. Je n'ai pas beaucoup testé cela.

Voici un article de blog avec beaucoup plus de détails et un exemple d'application.

Gray et moi sommes en train de finaliser un outil ORM, basé sur son Ormlite, qui fonctionne nativement avec les implémentations de bases de données Android, et suit la structure de création/appel sécurisée que je décris dans l'article de blog. Il devrait sortir très bientôt. Jetez-y un coup d'œil.


En attendant, il y a un billet de blog de suivi :

Découvrez également la fourche de 2point0 de l'exemple de verrouillage mentionné précédemment :

3 votes

Par ailleurs, le support Android d'Ormlite est disponible à l'adresse suivante ormlite.sourceforge.net/sqlite_java_android_orm.html . Il existe des exemples de projets, de la documentation et des bocaux.

1 votes

Une deuxième parenthèse. Le code ormlite a des classes d'aide qui peuvent être utilisées pour gérer les instances dbhelper. Vous pouvez utiliser le code ormlite, mais ce n'est pas obligatoire. Vous pouvez utiliser les classes d'aide juste pour faire la gestion des connexions.

32 votes

Kagii, merci pour cette explication détaillée. Pourriez-vous clarifier une chose : je comprends que vous ne devez avoir qu'UNE seule aide, mais devez-vous également n'avoir qu'une seule connexion (c'est-à-dire un seul objet SqliteDatabase) ? En d'autres termes, combien de fois devriez-vous appeler getWritableDatabase ? Et de manière tout aussi importante, quand devez-vous appeler close() ?

196voto

Dmytro Danylyk Points 6911

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.

0 votes

Que dois-je faire si la base de données est utilisée par activites et SyncService qui s'exécute dans son processus séparé ? <service Android:name=".SyncService" Android:exported="true" Android:process=":sync"> Si je comprends bien, le chargeur de classe pour un processus séparé est différent de celui des activites, donc votre approche ne fonctionnera pas ici ?

0 votes

@ievgen DatabaseManager est un singleton thread safe, il peut donc être utilisé partout. N'oubliez pas de l'initialiser la première fois dans votre classe d'application. DatabaseManager.initializeInstance(getApplicationContext());

11 votes

Je fais partie de ces gens qui suggèrent de ne jamais fermer le db. Vous n'obtiendrez une erreur "Leak found" que si vous ouvrez la base de données, ne la fermez pas, puis essayez de l'ouvrir à nouveau. Si vous n'utilisez qu'une seule aide à l'ouverture, et que jamais fermer le db, vous n'aurez pas cette erreur. Si vous trouvez autre chose, merci de me le faire savoir (avec le code). J'avais un post plus long à ce sujet quelque part, mais je ne le trouve pas. La question a été répondue par commonsware ici, qui nous dépasse tous les deux dans le département des points SO : stackoverflow.com/questions/7211941/

14voto

Brad Hein Points 4874

La base de données est très flexible avec le multi-threading. Mes applications utilisent leur base de données à partir de nombreux threads différents et cela fonctionne très bien. Dans certains cas, j'ai plusieurs processus qui accèdent à la base de données simultanément et cela fonctionne bien aussi.

Vos tâches asynchrones - utilisez la même connexion quand vous le pouvez, mais si vous devez le faire, vous pouvez accéder à la base de données à partir de différentes tâches.

0 votes

Par ailleurs, les lecteurs et les écrivains doivent-ils avoir des connexions différentes ou doivent-ils partager une seule connexion ? Merci.

1 votes

@Gray - correct, j'aurais dû le mentionner explicitement. En ce qui concerne les connexions, j'utiliserais la même connexion autant que possible, mais comme le verrouillage est géré au niveau du système de fichiers, vous pouvez l'ouvrir plusieurs fois dans le code, mais j'utiliserais une seule connexion autant que possible. La base de données sqlite d'Android est très flexible et indulgente.

4 votes

@Gray, je voulais juste poster une information mise à jour pour les personnes qui pourraient utiliser cette méthode. La documentation dit : Cette méthode ne fait maintenant rien. Ne pas utiliser.

9voto

dell116 Points 1614

Après avoir lutté avec cela pendant quelques heures, j'ai découvert que vous ne pouvez utiliser qu'un seul objet db helper par exécution db. Par exemple,

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();
}

par opposition à :

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();

La création d'un nouveau DBAdapter à chaque itération de la boucle était le seul moyen de faire entrer mes chaînes de caractères dans une base de données via ma classe d'aide.

5voto

Swaroop Points 444

D'après ce que je comprends des API SQLiteDatabase, si vous avez une application multithread, vous ne pouvez pas vous permettre d'avoir plus d'un objet SQLiteDatabase pointant vers une seule base de données.

L'objet peut certainement être créé mais les insertions/mises à jour échouent si différents threads/processus (aussi) commencent à utiliser différents objets SQLiteDatabase (comme nous utilisons dans JDBC Connection).

La seule solution ici est de s'en tenir à 1 objet SQLiteDatabase et chaque fois qu'un startTransaction() est utilisé dans plus d'un thread, Android gère le verrouillage entre les différents threads et permet à un seul thread à la fois d'avoir un accès exclusif aux mises à jour.

Vous pouvez également effectuer des "lectures" de la base de données et utiliser le même objet SQLiteDatabase dans un thread différent (pendant qu'un autre thread écrit) et il n'y aura jamais de corruption de la base de données, c'est-à-dire que le "thread de lecture" ne lira pas les données de la base de données tant que le "thread d'écriture" n'aura pas validé les données, même si les deux utilisent le même objet SQLiteDatabase.

C'est différent de la façon dont l'objet de connexion est utilisé dans JDBC où si vous faites circuler (utiliser le même) l'objet de connexion entre les threads de lecture et d'écriture, il est probable que nous imprimions également des données non engagées.

Dans mon application d'entreprise, j'essaie d'utiliser des contrôles conditionnels afin que le thread de l'interface utilisateur ne doive jamais attendre, tandis que le thread BG détient l'objet SQLiteDatabase (exclusivement). J'essaie de prévoir les actions de l'interface utilisateur et de différer l'exécution du thread BG pendant "x" secondes. On peut aussi utiliser PriorityQueue pour gérer la distribution des objets de connexion SQLiteDatabase afin que le thread de l'interface utilisateur les obtienne en premier.

0 votes

Et que mettez-vous dans cette PriorityQueue - des listeners (qui veulent obtenir l'objet de la base de données) ou des requêtes SQL ?

0 votes

Je n'ai pas utilisé l'approche de la file d'attente prioritaire, mais essentiellement les fils "appelants".

0 votes

@Swaroop : PCMIIW, "read thread" wouldn't read the data from the database till the "write thread" commits the data although both use the same SQLiteDatabase object . Ce n'est pas toujours vrai, si vous démarrez le "read thread" juste après le "write thread", vous risquez de ne pas obtenir les données nouvellement mises à jour (insérées ou mises à jour dans le "write thread"). Le thread de lecture peut lire les données avant le démarrage du thread d'écriture. Cela se produit parce que l'opération d'écriture active initialement le verrou réservé au lieu du verrou exclusif.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X