Quelles seraient les meilleures pratiques lors de l'exécution de 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 sélections de requêtes dans doInBackground d'un AsyncTask? Ou dois-je utiliser le fil d'interface utilisateur? Je suppose que les requêtes de base de données peuvent être "lourdes" et ne doivent pas utiliser le thread d'interface utilisateur car cela peut verrouiller l'application - résultant en une application ne répondant pas (ANR).
Si j'ai plusieurs AsyncTasks, doivent-ils partager une connexion ou doivent-ils ouvrir une connexion chacun?
Existe-t-il des meilleures pratiques pour ces scénarios?
Réponses:
Les insertions, mises à jour, suppressions et lectures sont généralement OK à partir de plusieurs threads, mais la réponse de Brad n'est pas correcte. Vous devez être prudent avec la façon dont vous créez vos connexions et les utilisez. Il y a des situations où 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 en écriture, mais ce n'est vraiment pas le cas. Appelez la lecture seule et vous obtiendrez la connexion à la base de données d'écriture.
Donc, une instance d'aide, une connexion db. Même si vous l'utilisez à partir de plusieurs threads, une connexion à la fois. L'objet SqliteDatabase utilise des verrous java pour conserver l'accès sérialisé. Ainsi, si 100 threads ont une instance de base de données, les appels à la base de données réelle sur le disque sont sérialisés.
Donc, une aide, une connexion db, qui est sérialisée en code java. Un thread, 1000 threads, si vous utilisez une instance d'assistance partagée entre eux, tout votre code d'accès db est série. Et la vie est belle (ish).
Si vous essayez d'écrire dans la base de données à partir de connexions distinctes réelles en même temps, l'une échouera. Il n'attendra pas que le premier soit terminé, puis rédigera. Il n'écrira tout simplement pas votre changement. Pire, si vous n'appelez pas la bonne version d'insertion / mise à jour sur la SQLiteDatabase, vous n'obtiendrez pas d'exception. Vous recevrez juste un message dans votre LogCat, et ce sera tout.
Donc, plusieurs threads? Utilisez un assistant. Période. Si vous SAVEZ qu'un seul thread écrira, vous POUVEZ être en mesure d'utiliser plusieurs connexions et vos lectures seront plus rapides, mais l'acheteur doit se méfier. Je n'ai pas beaucoup testé.
Voici un article de blog avec beaucoup plus de détails et un exemple d'application.
Gray et moi sommes en train de terminer un outil ORM, basé sur son Ormlite, qui fonctionne nativement avec les implémentations de base de données Android et suit la structure de création / appel sécurisée que je décris dans le blog. Cela devrait sortir très bientôt. Regarde.
En attendant, il y a un article de blog de suivi:
Vérifiez également la fourche par 2point0 de l'exemple de verrouillage mentionné précédemment:
la source
Accès simultané à la base de données
Même article sur mon blog (j'aime plus la mise en forme)
J'ai écrit un petit article qui décrit comment sécuriser l'accès à votre thread de base de données Android.
En supposant que vous ayez votre propre SQLiteOpenHelper .
Vous souhaitez maintenant écrire des données dans la base de données dans des threads séparés.
Vous obtiendrez le message suivant dans votre logcat et l'une de vos modifications ne sera pas écrite.
Cela se produit car chaque fois que vous créez un nouvel objet 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 échouera. (à partir de la réponse ci-dessus)
Pour utiliser la base de données avec plusieurs threads, nous devons nous assurer que nous utilisons une seule connexion à la base de données.
Faisons un gestionnaire de base de données de classe singleton qui contiendra et retournera un seul objet SQLiteOpenHelper .
Le code mis à jour qui écrit des données dans la base de données dans des threads séparés ressemblera à ceci.
Cela vous apportera un autre crash.
Puisque nous utilisons une seule connexion de base de données, méthode getDatabase () retour même instance de SQLiteDatabase objet pour Thread1 et Thread2 . Ce qui se passe, Thread1 peut fermer la base de données, tandis que Thread2 l' utilise toujours. C'est pourquoi nous avons un crash IllegalStateException .
Nous devons nous assurer que personne n'utilise la base de données et ensuite la fermer. Certaines personnes sur stackoveflow ont recommandé de ne jamais fermer votre SQLiteDatabase . Cela se traduira par le message logcat suivant.
Échantillon de travail
Utilisez-le comme suit.
Chaque fois que vous avez besoin d'une base de données, vous devez appeler la méthode openDatabase () de la classe DatabaseManager . À l'intérieur de 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 la méthode closeDatabase () . Chaque fois que nous appelons cette méthode, le compteur diminue, chaque fois qu'il passe à zéro, nous fermons la connexion à la base de données.
Vous devriez maintenant pouvoir utiliser votre base de données et vous assurer qu'elle est sécurisée pour les threads.
la source
if(instance==null)
? Vous ne laissez pas d'autre choix que d'appeler initialiser à chaque fois; Sinon, comment sauriez-vous s'il a été initialisé ou non dans d'autres applications, etc.?initializeInstance()
a un paramètre de typeSQLiteOpenHelper
, mais dans votre commentaire, vous avez indiqué d'utiliserDatabaseManager.initializeInstance(getApplicationContext());
. Que se passe-t-il? Comment cela peut-il fonctionner?Thread
ouAsyncTask
pour les opérations de longue durée (50 ms +). Testez votre application pour voir où cela se trouve. La plupart des opérations (probablement) ne nécessitent pas de thread, car la plupart des opérations (probablement) ne comportent que quelques lignes. Utilisez un fil pour les opérations en bloc.SQLiteDatabase
instance pour chaque base de données sur disque entre les threads et implémentez un système de comptage pour garder une trace des connexions ouvertes.Partagez un champ statique entre toutes vos classes. J'avais l'habitude de garder un singleton pour cela et d'autres choses qui doivent être partagées. Un schéma de comptage (utilisant généralement AtomicInteger) doit également être utilisé pour vous assurer de ne jamais fermer la base de données tôt ou de la laisser ouverte.
Pour la version la plus récente, voir https://github.com/JakarCo/databasemanager mais je vais essayer de garder le code à jour ici également. Si vous voulez comprendre ma solution, regardez le code et lisez mes notes. Mes notes sont généralement très utiles.
DatabaseManager
. (ou téléchargez-le depuis github)DatabaseManager
et mettre en œuvreonCreate
etonUpgrade
comme vous le feriez normalement. Vous pouvez créer plusieurs sous-classes de la mêmeDatabaseManager
classe afin d'avoir différentes bases de données sur le disque.getDb()
pour utiliser laSQLiteDatabase
classe.close()
pour chaque sous-classe que vous avez instanciéeLe code à copier / coller :
la source
close
, vous devez appeler àopen
nouveau avant d'utiliser la même instance de la classe OU vous pouvez créer une nouvelle instance. Étant donné que dans leclose
code, j'ai définidb=null
, vous ne seriez pas en mesure d'utiliser la valeur de retour degetDb
(car elle serait nulle), vous obtiendrez donc unNullPointerException
si vous faisiez quelque chose commemyInstance.close(); myInstance.getDb().query(...);
getDb()
etopen()
en une seule méthode?La base de données est très flexible avec plusieurs threads. Mes applications atteignent leurs bases de données à partir de nombreux threads simultanément et cela fonctionne très bien. Dans certains cas, j'ai plusieurs processus frappant la base de données simultanément et cela fonctionne très bien aussi.
Vos tâches asynchrones - utilisez la même connexion lorsque vous le pouvez, mais si vous le devez, vous pouvez accéder à la base de données à partir de différentes tâches.
la source
SQLiteDatabase
objets sur différentsAsyncTask
s /Thread
s, mais cela entraînera parfois des erreurs, c'est pourquoi SQLiteDatabase (ligne 1297) utiliseLock
sLa réponse de Dmytro fonctionne bien pour mon cas. Je pense qu'il vaut mieux déclarer la fonction synchronisée. au moins pour mon cas, il invoquerait une exception de pointeur nul dans le cas contraire, par exemple getWritableDatabase non encore retourné dans un thread et openDatabse appelé dans un autre thread entre-temps.
la source
après avoir lutté avec cela pendant quelques heures, j'ai constaté que vous ne pouvez utiliser qu'un seul objet d'assistance db par exécution db. Par exemple,
comme apposé sur:
la création d'un nouveau DBAdapter chaque fois que la boucle itère était le seul moyen pour moi de récupérer mes chaînes dans une base de données via ma classe d'assistance.
la source
Ma compréhension des API SQLiteDatabase est que dans le cas où vous avez une application multi-thread, 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 commencent (également) à utiliser différents objets SQLiteDatabase (comme la façon dont nous utilisons dans la connexion JDBC).
La seule solution ici est de s'en tenir à 1 objets SQLiteDatabase et chaque fois qu'une startTransaction () est utilisée dans plus d'un thread, Android gère le verrouillage sur différents threads et autorise un seul thread à la fois à avoir un accès exclusif aux mises à jour.
Vous pouvez également effectuer des "lectures" à partir de la base de données et utiliser le même objet SQLiteDatabase dans un thread différent (tandis qu'un autre thread écrit) et il n'y aurait jamais de corruption de base de données, c'est-à-dire que "le thread de lecture" ne lirait pas les données de la base de données jusqu'à ce que " write thread "valide les données bien que les deux utilisent le même objet SQLiteDatabase.
Ceci est différent de la façon dont l'objet de connexion est dans JDBC, où si vous passez (utilisez le même) l'objet de connexion entre les threads de lecture et d'écriture, nous imprimerons probablement également des données non validées.
Dans mon application d'entreprise, j'essaie d'utiliser des vérifications conditionnelles pour que le thread d'interface utilisateur n'ait jamais à attendre, tandis que le thread BG contient l'objet SQLiteDatabase (exclusivement). J'essaie de prédire les actions de l'interface utilisateur et de retarder l'exécution du thread BG pendant «x» secondes. On peut également maintenir PriorityQueue pour gérer la distribution des objets de connexion SQLiteDatabase afin que le thread d'interface utilisateur l'obtienne en premier.
la source
"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 "lire le fil" juste après "écrire le fil", vous risquez de ne pas obtenir les données nouvellement mises à jour (insérées ou mises à jour dans le fil d'écriture). Le thread de lecture peut lire les données avant le début du thread d'écriture. Cela se produit car l'opération d'écriture active initialement le verrouillage réservé au lieu du verrouillage exclusif.Vous pouvez essayer d'appliquer une nouvelle approche d'architecture annoncée lors de Google I / O 2017.
Il comprend également une nouvelle bibliothèque ORM appelée Room
Il contient trois composants principaux: @Entity, @Dao et @Database
User.java
UserDao.java
AppDatabase.java
la source
Ayant eu quelques problèmes, je pense avoir compris pourquoi je me trompais.
J'avais écrit une classe d'encapsuleur de base de données qui comprenait un
close()
qui appelait la fermeture de l'assistant en tant que miroir deopen()
ce qui s'appelait getWriteableDatabase, puis j'ai migré vers unContentProvider
. Le modèle pourContentProvider
n'utilise pasSQLiteDatabase.close()
ce qui, à mon avis, est un indice important car le code l'utilisegetWriteableDatabase
Dans certains cas, je faisais toujours un accès direct (requêtes de validation d'écran dans le principal, j'ai donc migré vers un modèle getWriteableDatabase / rawQuery.J'utilise un singleton et il y a le commentaire légèrement inquiétant dans la documentation proche
(mon audace).
J'ai donc eu des plantages intermittents où j'utilise des threads d'arrière-plan pour accéder à la base de données et ils s'exécutent en même temps que le premier plan.
Je pense donc que les
close()
forces de la base de données pour fermer indépendamment des autres threads possédant une référence - doncclose()
elle - même ne défaisons simplement la mise en correspondance ,getWriteableDatabase
mais la force de fermeture des demandes ouvertes. La plupart du temps, ce n'est pas un problème car le code est à thread unique, mais dans les cas à plusieurs threads, il y a toujours la possibilité d'ouvrir et de fermer la synchronisation.Après avoir lu des commentaires ailleurs qui expliquent que l'instance de code SqLiteDatabaseHelper compte, la seule fois où vous souhaitez une fermeture est l'endroit où vous souhaitez effectuer une copie de sauvegarde, et que vous souhaitez forcer la fermeture de toutes les connexions et forcer SqLite à supprimez toutes les informations en cache qui pourraient flâner - en d'autres termes, arrêtez toute activité de la base de données d'application, fermez juste au cas où le Helper a perdu la trace, effectuez une activité au niveau du fichier (sauvegarde / restauration) puis recommencez.
Bien que cela semble être une bonne idée d'essayer de fermer de manière contrôlée, la réalité est qu'Android se réserve le droit de jeter votre VM afin que toute fermeture réduise le risque que les mises à jour en cache ne soient pas écrites, mais cela ne peut pas être garanti si l'appareil est souligné, et si vous avez correctement libéré vos curseurs et références aux bases de données (qui ne devraient pas être des membres statiques), l'assistant aura quand même fermé la base de données.
Je pense donc que l'approche est la suivante:
Utilisez getWriteableDatabase pour ouvrir à partir d'un wrapper singleton. (J'ai utilisé une classe d'application dérivée pour fournir le contexte d'application à partir d'une statique pour résoudre le besoin d'un contexte).
N'appelez jamais directement près.
Ne stockez jamais la base de données résultante dans un objet qui n'a pas de portée évidente et comptez sur le comptage de références pour déclencher une fermeture implicite ().
Si vous effectuez la gestion au niveau des fichiers, arrêtez toutes les activités de la base de données, puis appelez close juste au cas où il y aurait un thread incontrôlé en supposant que vous écrivez les transactions appropriées afin que le thread incontrôlé échoue et que la base de données fermée ait au moins des transactions appropriées plutôt que potentiellement une copie de niveau de fichier d'une transaction partielle.
la source
Je sais que la réponse est tardive, mais la meilleure façon d'exécuter des requêtes sqlite dans Android est via un fournisseur de contenu personnalisé. De cette façon, l'interface utilisateur est découplée avec la classe de base de données (la classe qui étend la classe SQLiteOpenHelper). Les requêtes sont également exécutées dans un thread d'arrière-plan (Cursor Loader).
la source