Question 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 dans une application Android?

Est-il sécuritaire d'exécuter des insertions, de supprimer et de sélectionner des requêtes à partir du doInBackground d'une AsyncTask? Ou devrais-je utiliser le fil de l'interface utilisateur? Je suppose que les requêtes de base de données peuvent être "lourdes" et ne doivent pas utiliser le thread de l'interface utilisateur car il peut verrouiller l'application - ce qui entraîne un L'application ne répond pas (ANR).

Si j'ai plusieurs AsyncTasks, devraient-ils partager une connexion ou devraient-ils ouvrir une connexion chacun?

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


631
2018-03-22 15:13


origine


Réponses:


Les insertions, les mises à jour, les suppressions et les lectures sont généralement OK à partir de plusieurs threads, mais Brad répondre n'est pas correcte. Vous devez être prudent avec la façon dont vous créez vos connexions et les utiliser. 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 à une base de données. Il semble vous offrir une connexion en lecture et en écriture, mais ce n'est pas le cas. Appelez la lecture seule, et vous obtiendrez la connexion de base de données d'écriture indépendamment.

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 db, les appels à la base de données sur disque réelle 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 en série. Et la vie est bonne (ish).

Si vous essayez d'écrire dans la base de données à partir de connexions distinctes en même temps, l'une échouera. Il n'attendra pas jusqu'à ce que le premier soit fait, puis écris. Il n'écrira simplement pas votre changement. Pire, si vous n'appelez pas la bonne version d'insert / update sur SQLiteDatabase, vous n'obtiendrez pas d'exception. Vous aurez juste un message dans votre LogCat, et ce sera tout.

Donc, plusieurs threads? Utilisez une aide. Période. Si vous CONNAISSEZ qu'un seul thread écrira, vous POUVEZ pouvoir utiliser plusieurs connexions, et vos lectures seront plus rapides, mais faites attention à l'acheteur. Je n'ai pas testé autant.

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 bases 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 post de blog de suivi:

Aussi vérifier la fourche par 2point0 de l'exemple de verrouillage mentionné précédemment:


594
2017-09-11 05:11



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

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

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


En supposant que vous avez votre propre SQLiteOpenHelper.

public class DatabaseHelper extends SQLiteOpenHelper { ... }

Vous voulez 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 recevrez le message suivant dans votre logcat et l'un de vos changements ne sera pas écrit.

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

Cela arrive parce que chaque fois que vous créez de nouvelles SQLiteOpenHelper objet que vous êtes en train de faire 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 échouera. (de la réponse ci-dessus)

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

Faisons la classe singleton Gestionnaire de base qui tiendra et retourner 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 des 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 accident.

java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase

Puisque nous utilisons seulement une connexion de base de données, la méthode getDatabase () retourner la même instance de SQLiteDatabase objet pour Thread1 et Thread2. Qu'est-ce qui se passe, Thread1 peut fermer la base de données, alors Thread2 l'utilise toujours. Voilà 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 recommandé de ne jamais fermer votre SQLiteDatabase. Cela semble non seulement stupide mais aussi vous honorer avec 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 de base de données, vous devriez appeler openDatabase () méthode de DatabaseManager classe. A 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 arrive dans closeDatabase () méthode. Chaque fois que nous appelons cette méthode, le compteur est diminué, à chaque fois qu'il passe à zéro, nous fermons la connexion à la base de données.


Maintenant, vous devriez être capable d'utiliser votre base de données et assurez-vous que le thread est sûr.


176
2017-11-15 08:50



  • Utiliser un Thread ou AsyncTask pour les opérations de longue durée (50ms +). 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) n'impliquent que quelques lignes. Utilisez un thread pour les opérations en bloc.
  • Partager un SQLiteDatabase instance pour chaque DB sur le disque entre les threads et mettre en œuvre 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 ça et d'autres choses qui ont besoin d'être partagées. Un schéma de comptage (utilisant généralement AtomicInteger) devrait également être utilisé pour s'assurer que vous ne fermez jamais la base de données au début ou 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 aussi. 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 depuis github)
  2. étendre DatabaseManager et mettre en œuvre onCreate et onUpgrade comme vous le feriez normalement. Vous pouvez créer plusieurs sous-classes de l'une DatabaseManager classe afin d'avoir différentes bases de données sur le disque.
  3. Instancier votre sous-classe et appeler getDb() utiliser le SQLiteDatabase classe.
  4. Appel close() pour chaque sous-classe vous instancié

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 support@androidsqlitelibrary.com (or support@jakar.co )
 * 
 * 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();
                }
        } 
    }
}

15
2017-10-29 23:14



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

Vos tâches asynchrones - utilisez la même connexion lorsque vous le pouvez, mais si vous le souhaitez, il est possible d'accéder à la base de données à partir de différentes tâches.


9
2018-03-22 16:14



La réponse de Dmytro fonctionne bien pour mon cas. Je pense qu'il est préférable de déclarer la fonction comme synchronisée. au moins pour 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 meantime.

public synchronized SQLiteDatabase openDatabase() {
        if(mOpenCounter.incrementAndGet() == 1) {
            // Opening new database
            mDatabase = mDatabaseHelper.getWritableDatabase();
        }
        return mDatabase;
    }

6
2018-01-07 09:01



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 1 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 (trop) commencent à utiliser des objets SQLiteDatabase différents (comme la façon dont nous utilisons la connexion JDBC).

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

Vous pouvez aussi faire "Reads" de la base de données et utiliser le même objet SQLiteDatabase dans un thread différent (alors qu'un autre thread écrit) et il n'y aurait jamais de corruption de base de données, ie "read thread" ne lirait pas les données 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 la même chose) l'objet de connexion entre les threads de lecture et d'écriture, nous imprimerons probablement des données non validées.

Dans mon application d'entreprise, j'essaie d'utiliser des contrôles conditionnels pour que le thread UI ne doive 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 différer l'exécution du thread BG pendant «x» secondes. En outre, on peut maintenir PriorityQueue pour gérer la distribution des objets de connexion SQLiteDatabase afin que le thread UI l'obtienne en premier.


4
2018-02-03 09:35



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

tel qu'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();

créer un nouvel DBAdapter chaque fois que la boucle itère était la seule façon d'obtenir mes chaînes dans une base de données via ma classe d'aide.


4
2018-06-29 07:22



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

J'avais écrit une classe wrapper de base de données qui comprenait un close() qui a appelé l'assistant proche comme un miroir de open() qui a appelé getWriteableDatabase, puis ont migré vers un ContentProvider. Le modèle pour ContentProvider n'utilise pas SQLiteDatabase.close() ce que je pense est un grand indice que le code utilise getWriteableDatabase Dans certains cas, je faisais toujours un accès direct (requêtes de validation d'écran dans le principal donc j'ai migré vers un modèle getWriteableDatabase / rawQuery.

J'utilise un singleton et il y a le commentaire un peu inquiétant dans la documentation close

Fermer tout objet de base de données ouvert

(mon bolding).

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.

Donc je pense close() force la base de données à fermer indépendamment de tous les autres threads contenant des références - donc close() lui-même ne défait pas simplement l'appariement getWriteableDatabase mais force de fermeture tout demandes ouvertes La plupart du temps, ce n'est pas un problème car le code est un thread unique, mais dans les cas multithread, il y a toujours un risque d'ouverture et de fermeture hors synchronisation.

Après avoir lu les commentaires qui expliquent que l'instance de code SqLiteDatabaseHelper compte, la seule fois où vous voulez une fermeture est l'endroit où vous voulez faire une copie de sauvegarde, et vous voulez forcer toutes les connexions à se fermer et forcer SqLite à écrivez toutes les données en cache qui pourraient être errantes - en d'autres termes, arrêtez toutes les activités de la base de données, fermez juste au cas où l'assistant a perdu la trace, effectuez une activité de sauvegarde de fichier puis recommencez.

Bien que cela semble une bonne idée d'essayer de fermer de manière contrôlée, Android se réserve le droit de jeter votre machine virtuelle de sorte que toute fermeture réduit le risque que les mises à jour en cache ne soient pas écrites, mais ne peut être garantie est souligné, et si vous avez correctement libéré vos curseurs et vos 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.

Donc, je pense 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).

Ne jamais appeler directement à proximité.

Ne jamais stocker la base de données résultante dans un objet qui n'a pas une portée évidente et compter sur le comptage des références pour déclencher un close () implicite.

Si vous manipulez un fichier, arrêtez toute l'activité de la base de données et appelez close juste au cas où il y aurait un thread d'emballement en supposant que vous écrivez correctement les transactions afin que le thread fugitif échoue et que la base fermée ait au moins les bonnes transactions que potentiellement une copie au niveau du fichier d'une transaction partielle.


3
2018-03-03 21:47



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 Chambre

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

2
2018-05-23 19:17



Je sais que la réponse est en retard, mais la meilleure façon d'exécuter des requêtes sqlite dans Android est par le biais d'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).


0
2017-11-24 06:13