Quelles sont les meilleures pratiques pour SQLite sur Android?

694

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?

Vidar Vestnes
la source
10
Quoi que vous fassiez, n'oubliez pas de nettoyer vos entrées si votre fournisseur de contenu (ou interface SQLite) est ouvert au public!
Kristopher Micinski
37
Vous ne devriez certainement PAS faire des accès db à partir du thread d'interface utilisateur, je peux vous dire cela.
Edward Falk
@EdwardFalk Pourquoi pas? Il y a sûrement des cas d'utilisation où il est valable de le faire?
Michael
4
Si vous effectuez des E / S, des accès réseau, etc. à partir du thread d'interface utilisateur, l'ensemble du périphérique se bloque jusqu'à la fin de l'opération. Si elle se termine en 1/20 seconde, alors très bien. Si cela prend plus de temps, vous avez une mauvaise expérience utilisateur.
Edward Falk

Réponses:

631

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:

Kevin Galligan
la source
2
En passant, le support Android d'Ormlite peut être trouvé à ormlite.sourceforge.net/sqlite_java_android_orm.html . Il existe des exemples de projets, de documentation et de fichiers jar.
Gris
1
Une seconde de côté. Le code ormlite possède des classes d'assistance qui peuvent être utilisées pour gérer les instances de dbhelper. Vous pouvez utiliser la substance ormlite, mais ce n'est pas obligatoire. Vous pouvez utiliser les classes d'assistance uniquement pour gérer la connexion.
Kevin Galligan
31
Kāgii, merci pour l'explication détaillée. Pourriez-vous clarifier une chose - je comprends que vous devriez avoir UN assistant, mais devriez-vous également n'avoir qu'une seule connexion (c'est-à-dire un objet SqliteDatabase)? En d'autres termes, à quelle fréquence devez-vous appeler getWritableDatabase? Et tout aussi important quand appelez-vous close ()?
Artem
3
J'ai mis à jour le code. L'original a été perdu lorsque j'ai changé d'hôte de blog, mais j'ai ajouté un exemple de code allégé qui illustrera le problème. De plus, comment gérez-vous la connexion unique? J'avais initialement une solution beaucoup plus compliquée, mais j'ai depuis amendé cela. Jetez un œil ici: touchlab.co/uncategorized/single-sqlite-connection
Kevin Galligan
est-il nécessaire de supprimer la connexion et où le faire?
tugce
188

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 .

public class DatabaseHelper extends SQLiteOpenHelper { ... }

Vous souhaitez maintenant é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 l'une de vos modifications ne sera pas écrite.

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)

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 .

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 des données dans la base de données dans des threads 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 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.

Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed

Échantillon 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 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.

Dmytro Danylyk
la source
10
Je fais partie de ces personnes qui vous suggèrent de ne jamais fermer la base de données. Vous obtenez une erreur "Leak found" uniquement si vous ouvrez la base de données, ne la fermez pas, puis essayez de l'ouvrir à nouveau. Si vous n'utilisez qu'un seul assistant ouvert et ne fermez jamais la base de données, vous n'obtenez pas cette erreur. Si vous trouvez le contraire, merci de me le faire savoir (avec code). J'avais un article plus long à ce sujet quelque part, mais je ne le trouve pas. Réponse à la question par commonsware ici, qui nous dépasse en quelque sorte dans le département des points SO: stackoverflow.com/questions/7211941/…
Kevin Galligan
4
Plus de pensées. # 1, je créerais l'aide dans votre manager. Demander des problèmes pour l'avoir à l'extérieur. Le nouveau développeur peut appeler directement l'assistant pour une raison folle. De plus, si vous allez avoir besoin d'une méthode init, lancez une exception si l'instance existe déjà. Les applications multi-db échoueront évidemment telles quelles. # 2, pourquoi le champ mDatabase? Son disponible auprès de l'assistant. # 3, comme première étape vers "ne jamais fermer", qu'arrive-t-il à votre base de données lorsque votre application se bloque et n'est pas "fermée"? Un indice, rien. C'est très bien, car SQLite est super stable. C'était l'étape 1 pour comprendre pourquoi vous n'avez pas besoin de le fermer.
Kevin Galligan
1
Une raison pour laquelle vous utilisez une méthode d'initialisation publique pour appeler avant d'obtenir l'instance? Pourquoi ne pas avoir un constructeur privé qui s'appelle 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.?
ChiefTwoPencils
1
initializeInstance()a un paramètre de type SQLiteOpenHelper, mais dans votre commentaire, vous avez indiqué d'utiliser DatabaseManager.initializeInstance(getApplicationContext());. Que se passe-t-il? Comment cela peut-il fonctionner?
faizal
2
@DmytroDanylyk "DatabaseManager est un singleton thread-safe, donc il pourrait être utilisé n'importe où" ce n'est pas vrai en réponse à la question de virsir. Les objets ne sont pas des processus croisés partagés. Votre DatabaseManager n'aura aucun état dans un processus différent comme dans un adaptateur de synchronisation (android: process = ": sync")
sifflez le
17
  • Utilisez un Threadou AsyncTaskpour 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.
  • Partagez une SQLiteDatabaseinstance 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.

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

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.

Ma solution:

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.

  1. copiez / collez le code dans un nouveau fichier nommé DatabaseManager. (ou téléchargez-le depuis github)
  2. étendre DatabaseManageret mettre en œuvre onCreateet onUpgradecomme vous le feriez normalement. Vous pouvez créer plusieurs sous-classes de la même DatabaseManagerclasse afin d'avoir différentes bases de données sur le disque.
  3. Instanciez votre sous-classe et appelez getDb()pour utiliser la SQLiteDatabaseclasse.
  4. Appelez close()pour chaque sous-classe que vous avez instanciée

Le code à copier / coller :

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;

import java.util.concurrent.ConcurrentHashMap;

/** Extend this class and use it as an SQLiteOpenHelper class
 *
 * DO NOT distribute, sell, or present this code as your own. 
 * for any distributing/selling, or whatever, see the info at the link below
 *
 * Distribution, attribution, legal stuff,
 * See https://github.com/JakarCo/databasemanager
 * 
 * If you ever need help with this code, contact me at [email protected] (or [email protected] )
 * 
 * Do not sell this. but use it as much as you want. There are no implied or express warranties with this code. 
 *
 * This is a simple database manager class which makes threading/synchronization super easy.
 *
 * Extend this class and use it like an SQLiteOpenHelper, but use it as follows:
 *  Instantiate this class once in each thread that uses the database. 
 *  Make sure to call {@link #close()} on every opened instance of this class
 *  If it is closed, then call {@link #open()} before using again.
 * 
 * Call {@link #getDb()} to get an instance of the underlying SQLiteDatabse class (which is synchronized)
 *
 * I also implement this system (well, it's very similar) in my <a href="http://androidslitelibrary.com">Android SQLite Libray</a> at http://androidslitelibrary.com
 * 
 *
 */
abstract public class DatabaseManager {

    /**See SQLiteOpenHelper documentation
    */
    abstract public void onCreate(SQLiteDatabase db);
    /**See SQLiteOpenHelper documentation
     */
    abstract public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
    /**Optional.
     * *
     */
    public void onOpen(SQLiteDatabase db){}
    /**Optional.
     * 
     */
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
    /**Optional
     * 
     */
    public void onConfigure(SQLiteDatabase db){}



    /** The SQLiteOpenHelper class is not actually used by your application.
     *
     */
    static private class DBSQLiteOpenHelper extends SQLiteOpenHelper {

        DatabaseManager databaseManager;
        private AtomicInteger counter = new AtomicInteger(0);

        public DBSQLiteOpenHelper(Context context, String name, int version, DatabaseManager databaseManager) {
            super(context, name, null, version);
            this.databaseManager = databaseManager;
        }

        public void addConnection(){
            counter.incrementAndGet();
        }
        public void removeConnection(){
            counter.decrementAndGet();
        }
        public int getCounter() {
            return counter.get();
        }
        @Override
        public void onCreate(SQLiteDatabase db) {
            databaseManager.onCreate(db);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            databaseManager.onUpgrade(db, oldVersion, newVersion);
        }

        @Override
        public void onOpen(SQLiteDatabase db) {
            databaseManager.onOpen(db);
        }

        @Override
        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            databaseManager.onDowngrade(db, oldVersion, newVersion);
        }

        @Override
        public void onConfigure(SQLiteDatabase db) {
            databaseManager.onConfigure(db);
        }
    }

    private static final ConcurrentHashMap<String,DBSQLiteOpenHelper> dbMap = new ConcurrentHashMap<String, DBSQLiteOpenHelper>();

    private static final Object lockObject = new Object();


    private DBSQLiteOpenHelper sqLiteOpenHelper;
    private SQLiteDatabase db;
    private Context context;

    /** Instantiate a new DB Helper. 
     * <br> SQLiteOpenHelpers are statically cached so they (and their internally cached SQLiteDatabases) will be reused for concurrency
     *
     * @param context Any {@link android.content.Context} belonging to your package.
     * @param name The database name. This may be anything you like. Adding a file extension is not required and any file extension you would like to use is fine.
     * @param version the database version.
     */
    public DatabaseManager(Context context, String name, int version) {
        String dbPath = context.getApplicationContext().getDatabasePath(name).getAbsolutePath();
        synchronized (lockObject) {
            sqLiteOpenHelper = dbMap.get(dbPath);
            if (sqLiteOpenHelper==null) {
                sqLiteOpenHelper = new DBSQLiteOpenHelper(context, name, version, this);
                dbMap.put(dbPath,sqLiteOpenHelper);
            }
            //SQLiteOpenHelper class caches the SQLiteDatabase, so this will be the same SQLiteDatabase object every time
            db = sqLiteOpenHelper.getWritableDatabase();
        }
        this.context = context.getApplicationContext();
    }
    /**Get the writable SQLiteDatabase
     */
    public SQLiteDatabase getDb(){
        return db;
    }

    /** Check if the underlying SQLiteDatabase is open
     *
     * @return whether the DB is open or not
     */
    public boolean isOpen(){
        return (db!=null&&db.isOpen());
    }


    /** Lowers the DB counter by 1 for any {@link DatabaseManager}s referencing the same DB on disk
     *  <br />If the new counter is 0, then the database will be closed.
     *  <br /><br />This needs to be called before application exit.
     * <br />If the counter is 0, then the underlying SQLiteDatabase is <b>null</b> until another DatabaseManager is instantiated or you call {@link #open()}
     *
     * @return true if the underlying {@link android.database.sqlite.SQLiteDatabase} is closed (counter is 0), and false otherwise (counter > 0)
     */
    public boolean close(){
        sqLiteOpenHelper.removeConnection();
        if (sqLiteOpenHelper.getCounter()==0){
            synchronized (lockObject){
                if (db.inTransaction())db.endTransaction();
                if (db.isOpen())db.close();
                db = null;
            }
            return true;
        }
        return false;
    }
    /** Increments the internal db counter by one and opens the db if needed
    *
    */
    public void open(){
        sqLiteOpenHelper.addConnection();
        if (db==null||!db.isOpen()){
                synchronized (lockObject){
                    db = sqLiteOpenHelper.getWritableDatabase();
                }
        } 
    }
}
Roseau
la source
1
Que se passe-t-il lorsque vous appelez «fermer» et essayez ensuite de réutiliser la classe? ça va planter? ou va-t-il se réinitialiser automatiquement pour pouvoir réutiliser la base de données?
développeur Android
1
@androiddeveloper, Si vous appelez close, vous devez appeler à opennouveau avant d'utiliser la même instance de la classe OU vous pouvez créer une nouvelle instance. Étant donné que dans le closecode, j'ai défini db=null, vous ne seriez pas en mesure d'utiliser la valeur de retour de getDb(car elle serait nulle), vous obtiendrez donc un NullPointerExceptionsi vous faisiez quelque chose commemyInstance.close(); myInstance.getDb().query(...);
Reed
Pourquoi ne pas combiner getDb()et open()en une seule méthode?
Alex Burdusel
@Burdu, Un mélange de fournir un contrôle supplémentaire sur le compteur de base de données et une mauvaise conception. Ce n'est certainement pas la meilleure façon de le faire, cependant. Je le mettrai à jour dans quelques jours.
Reed
@Burdu, je viens de le mettre à jour. Vous pouvez obtenir le nouveau code à partir d' ici . Je ne l'ai pas testé, alors faites-moi savoir si je dois valider les modifications.
Reed
11

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.

Brad Hein
la source
De plus, avez-vous des lecteurs et des écrivains dans différentes connexions ou devraient-ils partager une seule connexion? Merci.
Gray
@Gray - correct, j'aurais dû le mentionner explicitement. En ce qui concerne les connexions, j'utiliserais autant que possible la même connexion, 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 Android sqlite est très flexible et indulgente.
Brad Hein
3
@Gray, je voulais juste publier une information mise à jour pour les personnes qui pourraient utiliser cette méthode. La documentation dit: Cette méthode ne fait plus rien. Ne pas utiliser.
Pijusn
3
J'ai trouvé que cette méthode échouait horriblement, nous passons à ContentProvider pour accéder à partir de plusieurs applications. Nous devrons faire un peu de concurrence sur nos méthodes mais cela devrait résoudre tous les problèmes avec les processus accédant tous aux données en même temps.
JPM
2
Je sais que c'est vieux, mais c'est incorrect. Cela peut fonctionner correctement pour accéder à la base de données à partir de différents SQLiteDatabaseobjets sur différents AsyncTasks / Threads, mais cela entraînera parfois des erreurs, c'est pourquoi SQLiteDatabase (ligne 1297) utilise Locks
Reed
7

La 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.

public synchronized SQLiteDatabase openDatabase() {
    if(mOpenCounter.incrementAndGet() == 1) {
        // Opening new database
        mDatabase = mDatabaseHelper.getWritableDatabase();
    }
    return mDatabase;
}
gonglong
la source
mDatabaseHelper.getWritableDatabase (); Cela ne créera pas de nouvel objet de base de données
périphérique
5

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,

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

comme apposé sur:

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 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.

dell116
la source
4

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.

Swaroop
la source
Et que mettez-vous dans ce PriorityQueue - écouteurs (qui veulent obtenir l'objet de base de données) ou requêtes SQL?
Pijusn
Je n'ai pas utilisé l'approche de file d'attente prioritaire, mais essentiellement les threads "appelant".
Swaroop
@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 "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.
Amit Vikram Singh
4

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

@Entity
public class User {
  @PrimaryKey
  private int uid;

  @ColumnInfo(name = "first_name")
  private String firstName;

  @ColumnInfo(name = "last_name")
  private String lastName;

  // Getters and setters are ignored for brevity,
  // but they're required for Room to work.
}

UserDao.java

@Dao
public interface UserDao {
  @Query("SELECT * FROM user")
  List<User> getAll();

  @Query("SELECT * FROM user WHERE uid IN (:userIds)")
  List<User> loadAllByIds(int[] userIds);

  @Query("SELECT * FROM user WHERE first_name LIKE :first AND "
       + "last_name LIKE :last LIMIT 1")
  User findByName(String first, String last);

  @Insert
  void insertAll(User... users);

  @Delete
  void delete(User user);
}

AppDatabase.java

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
  public abstract UserDao userDao();
}
Zimbo Rodger
la source
Je ne suggère pas d'espace à une base de données avec plusieurs relations N-à-N, car il ne gère pas bien cette approche et vous devez écrire beaucoup de code pour trouver une solution de contournement pour ces relations.
AlexPad
3

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 de open()ce qui s'appelait getWriteableDatabase, puis j'ai migré vers un ContentProvider. Le modèle pour ContentProvidern'utilise pas SQLiteDatabase.close()ce qui, à mon avis, est un indice important car le code l'utilise getWriteableDatabaseDans 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

Fermez tout objet de base de données ouvert

(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 - donc close()elle - même ne défaisons simplement la mise en correspondance , getWriteableDatabasemais 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.

Ian Spencer
la source
0

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).

Théo
la source