web-dev-qa-db-fra.com

Quelles sont les meilleures pratiques pour SQLite sur Android?

Quelles seraient les meilleures pratiques lors de l’exécution de requêtes sur une base de données SQLite au sein d’une application Android?

Est-il prudent d'exécuter des insertions, des suppressions et des requêtes dans DoInBackground d'un AsyncTask? Ou devrais-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 fil de l'interface utilisateur car elles peuvent verrouiller l'application, ce qui entraîne un application ne répondant pas (ANR).

Si j'ai plusieurs tâches asynchrones, doivent-ils partager une connexion ou doivent-ils ouvrir une connexion chacun?

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

671
Vidar Vestnes

Les insertions, mises à jour, suppressions et lectures sont généralement correctes pour plusieurs threads, mais Brad answer n'est pas correct. Vous devez faire attention à la manière dont vous créez vos relations et à les utiliser. Il existe 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 le lecteur en lecture seule et vous obtiendrez la connexion en écriture à la base de données, peu importe.

Donc, une instance d'assistance, une connexion à une base de données. Même si vous l'utilisez à partir de plusieurs threads, une connexion à la fois. L'objet SqliteDatabase utilise Java verrous 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 disque sont sérialisés.

Donc, un assistant, une connexion à une base de données, qui est sérialisée dans le code Java. Un thread, 1 000, si vous utilisez une instance d'assistance partagée entre eux, tout votre code d'accès à la base de données est série. Et la vie est belle (ish).

Si vous essayez d'écrire dans la base de données à partir de connexions distinctes en même temps, l'une d'entre elles échouera. Il n'attendra pas que le premier soit terminé puis écrit. Cela n'écrira simplement pas votre monnaie. Pire encore, si vous n’appelez pas la bonne version d’insertion/mise à jour sur la base de données SQLite, vous n’obtiendrez pas d’exception. Vous aurez juste un message dans votre LogCat, et ce sera tout.

Alors, plusieurs threads? Utilisez un assistant. Période. Si vous savez qu'un seul fil sera en cours d'écriture, vous pourrez peut-être utiliser plusieurs connexions et vos lectures seront plus rapides, mais soyez prudent. 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 en mode natif avec les implémentations de base de données Android et qui suit la structure de création/appel sécurisée que je décris dans l'article. Cela devrait être très bientôt. Regarde.


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

Vérifiez également le fork de 2point0 de l'exemple de verrouillage mentionné précédemment:

620
Kevin Galligan

Accès simultané à la base de données

Même article sur mon blog (j'aime plus formater)

J'ai écrit un petit article décrivant comment rendre l'accès à votre thread de base de données Android sécurisé.


En supposant que vous ayez votre propre SQLiteOpenHelper .

public class DatabaseHelper extends SQLiteOpenHelper { ... }

Vous souhaitez maintenant écrire des données dans une base de données dans des threads distincts.

 // 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 recevrez 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 une nouvelle connexion à la base de données. Si vous essayez d'écrire dans la base de données à partir de connexions distinctes en même temps, l'une d'entre elles échouera. (de la réponse ci-dessus)

Pour utiliser une base de données avec plusieurs threads, nous devons nous assurer que nous utilisons une connexion de base de données.

Créons la classe singleton du gestionnaire de base de données qui contiendra et renverra 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 les données dans la base de données dans des threads distincts 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

Comme nous n’utilisons qu’une seule connexion à une base de données, la méthode getDatabase () renvoie la même instance de . Objet SQLiteDatabase pour Thread1 et Thread2 . Ce qui se passe, Thread1 peut fermer la base de données, alors que Thread2 l’utilise toujours. C’est pourquoi nous avons un IllegalStateException crash.

Nous devons nous assurer que personne n'utilise la base de données et ensuite seulement la fermer. Certains utilisateurs de stackoveflow ont recommandé de ne jamais fermer votre SQLiteDatabase . Cela entraînera le message suivant de logcat.

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 . Dans cette méthode, nous avons un compteur, qui indique combien de fois la base de données est ouverte. S'il en vaut 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 thread-safe.

186
Dmytro Danylyk
  • Utilisez un Thread ou AsyncTask 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 fil, car la plupart des opérations (probablement) ne comportent que quelques lignes. Utilisez un fil pour les opérations en bloc.
  • Partagez une instance SQLiteDatabase pour chaque base de données sur le disque entre des threads et implémentez un système de comptage pour suivre les 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 à partager. 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 à l'avance ni 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. copier/coller le code dans un nouveau fichier nommé DatabaseManager. (ou téléchargez-le sur github)
  2. étendre DatabaseManager et implémenter onCreate et onUpgrade comme vous le feriez normalement. Vous pouvez créer plusieurs sous-classes de la classe DatabaseManager afin d’avoir différentes bases de données sur le disque.
  3. Instanciez votre sous-classe et appelez getDb() pour utiliser la classe SQLiteDatabase.
  4. Appelez close() pour chaque sous-classe que vous avez instanciée

Le code pour 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();
                }
        } 
    }
}
17
Reed

La base de données est très flexible avec le multi-threading. Mes applications utilisent simultanément leurs bases de données à partir de nombreux threads différents, et tout se passe bien. Dans certains cas, plusieurs processus touchent simultanément la base de données, ce qui fonctionne également très bien.

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

11
Brad Hein

La réponse de Dmytro fonctionne bien dans mon cas. Je pense qu'il est préférable de déclarer la fonction synchronisée. du moins dans mon cas, il invoquerait une exception de pointeur nul sinon, par ex. getWritableDatabase pas 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;
    }
7
gonglong

après avoir lutté pendant quelques heures avec cela, j'ai constaté que vous ne pouvez utiliser qu'un seul objet d'assistance de base de données par exécution de base de données. 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é à:

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 de faire entrer mes chaînes dans une base de données par le biais de ma classe d'assistance.

5
dell116

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 base de données unique.

L'objet peut définitivement être créé mais les insertions/mises à jour échouent si différents threads/processus commencent (aussi) à utiliser différents objets SQLiteDatabase (comme notre utilisation dans la connexion JDBC).

La seule solution consiste à ne conserver qu'un seul objet SQLiteDatabase. Chaque fois qu'une startTransaction () est utilisée dans plusieurs threads, Android gère le verrouillage sur différents threads et permet à un seul thread à la fois de bénéficier d'une mise à jour exclusive. accès.

Vous pouvez aussi faire des "lectures" dans la base de données et utiliser le même objet SQLiteDatabase dans un autre thread (pendant qu'un autre thread écrit) et il n'y aura jamais d'altération de la base de données, autrement dit "read thread" ne lira pas les données de la base de données avant le " write thread "valide les données bien que les deux utilisent le même objet SQLiteDatabase.

Cela diffère de la manière dont l'objet de connexion est dans JDBC, où si vous transmettez (utilisez le même) l'objet de connexion entre les threads de lecture et d'écriture, nous imprimerions probablement aussi des données non validées.

Dans mon application d'entreprise, j'essaie d'utiliser des contrôles conditionnels pour que le thread d'interface utilisateur n'ait jamais à attendre, alors que le thread BG contient l'objet SQLiteDatabase (exclusivement). J'essaie de prédire les actions de l'interface utilisateur et de différer l'exécution du fil d'exécution BG pendant 'x' secondes. Vous pouvez également gérer PriorityQueue pour gérer le traitement des objets de connexion SQLiteDatabase de sorte que le thread d'interface utilisateur le reçoive en premier.

4
Swaroop

Vous pouvez essayer d'appliquer une nouvelle approche de l'architecture annoncé à 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();
}
4
Zimbo Rodger

Ayant eu quelques problèmes, je pense avoir compris pourquoi je me suis trompé.

J'avais écrit une classe de wrapper de base de données qui contenait une close() qui appelait l'assistant comme un miroir de open() qui s'appelait getWriteableDatabase puis avait migré vers un ContentProvider. Le modèle pour ContentProvider n'utilise pas SQLiteDatabase.close(), ce qui me semble un indice important, car le code utilise getWriteableDatabase Dans certains cas, je faisais encore un accès direct (requêtes de validation d'écran dans le J'ai 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

Close any objet de base de données ouvert

(mon gras).

Donc, j'ai eu des plantages intermittents où j'utilisais des threads d'arrière-plan pour accéder à la base de données et ils fonctionnaient en même temps que l'avant-plan.

Donc, je pense que close() force la base de données à se fermer indépendamment des autres threads contenant des références - donc close() ne supprime pas simplement la correspondance getWriteableDatabase mais force la fermeture any demandes ouvertes. La plupart du temps, cela ne pose pas de problème car le code est à thread unique, mais dans les cas à plusieurs threads, il y a toujours une chance d'ouvrir et de fermer la synchronisation.

Après avoir lu des commentaires ailleurs expliquant que l'instance de code SqLiteDatabaseHelper est prise en compte, vous ne pouvez fermer que lorsque vous souhaitez que la situation dans laquelle vous souhaitez effectuer une copie de sauvegarde vous oblige à fermer toutes les connexions et à forcer SqLite à: écrivez tout ce qui est caché dans le cache - en d'autres termes, arrêtez toute activité de la base de données d'application, fermez-le au cas où le Helper aurait 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, le fait est que Android se réserve le droit de supprimer votre VM de sorte que toute fermeture réduise le risque de mises à jour en cache. pas en cours d'écriture, mais cela ne peut pas être garanti si le périphérique est soumis à une contrainte et si vous avez correctement libéré vos curseurs et vos références aux bases de données (qui ne doivent pas être des membres statiques), l'assistant aura malgré tout fermé la base de données.

Donc, ma conclusion est 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'un statique afin de résoudre le besoin d'un contexte).

Ne jamais appeler directement à proximité.

Ne stockez jamais la base de données résultante dans un objet sans étendue évidente et utilisez le décompte de références pour déclencher une fermeture implicite ().

Si vous gérez des fichiers, arrêtez toutes les activités de la base de données, puis appelez close au cas où il y aurait un thread en fuite en partant du principe que vous écrivez les transactions appropriées de sorte que le thread en fuite échoue et que la base de données fermée effectue au moins les transactions appropriées. que potentiellement une copie au niveau du fichier d’une transaction partielle.

3
Ian Spencer

Je sais que la réponse est en retard, mais le meilleur moyen d'exécuter des requêtes sqlite dans Android consiste à utiliser un fournisseur de contenu personnalisé. De cette manière, l'interface utilisateur est découplée de la classe de base de données (la classe qui étend la classe SQLiteOpenHelper). De plus, les requêtes sont exécutées dans un fil d’arrière-plan (Cursor Loader).

0
Theo