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ûr d'exécuter des insertions, des suppressions et des requêtes select à partir d'une AsyncTask est doInBackground? Ou devrais-je utiliser le fil UI? Je suppose que les requêtes de base de données peuvent être "lourdes" et ne devraient pas utiliser le thread de L'UI car il peut verrouiller l'application - résultant en une Application ne répondant pas (ANR).

si j'ai plusieurs AsyncTasks, devraient-ils partager une connexion ou doivent-ils ouvrir une connexion de chaque?

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

640
demandé sur Peter Mortensen 2010-03-22 18:13:32

10 réponses

les Insertions, mises à jour, suppressions et les lectures sont généralement OK à partir de plusieurs threads, mais Brad réponse n'est pas correct. Vous devez faire attention à 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 basique.

L'objet SqliteOpenHelper conserve une connexion de base de données. Il il semble vous offrir une connexion de lecture et d'écriture, mais ce n'est vraiment pas le cas. Appelez la lecture seule, et vous obtiendrez la connexion de base de données d'écriture indépendamment.

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

donc, un assistant, une connexion db, qui est sérialisée en code java. Un thread, 1000 threads, si vous utilisez une instance helper 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 à la base de données à partir de connexions distinctes réelles en même temps, l'une d'elles échouera. Il n'attendra pas que le premier soit fait et ensuite écrire. Il sera tout simplement pas écrire votre changement. Pire, si vous n'appelez pas la bonne version d'insert/update sur le SQLiteDatabase, vous n'obtiendrez pas une exception. Vous aurez juste un message dans votre LogCat, et ce sera tout.

donc, plusieurs fils? Utiliser un helper. Période. Si vous savez qu'un seul thread va être écrit, Vous pouvez être en mesure d'utiliser plusieurs connexions, et vos lectures seront plus rapides, mais attention acheteur. Je n'ai pas testé plus que ça.

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

Gray et moi sommes en train d'emballer 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écuritaire que je décris dans le billet de blog. Qui devrait sortir très bientôt. Prendre un coup d'oeil.


dans l'intervalle, il ya un billet de blog de suivi:

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

601
répondu Kevin Galligan 2018-04-17 06:39:23

Accès Simultané À La Base De Données

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

j'ai écrit un petit article qui décrit comment rendre l'accès à votre base de données android thread sûr.


en supposant que vous ayez votre propre SQLiteOpenHelper .

public class DatabaseHelper extends SQLiteOpenHelper { ... }

Maintenant, vous voulez écrire des données à 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 se produit parce que chaque fois que vous créez un nouvel objet SQLiteOpenHelper vous créez une nouvelle connexion de 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'elles é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 connexion de base de données.

nous allons faire classe singleton Gestionnaire de Base de données qui sera en attente et revenir 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();
    }

}

code mis à jour qui écrivent des données à 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();

ce sera apporter un autre crash.

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

puisque nous n'utilisons qu'une seule connexion de base de données, la méthode getDatabase () retourner la 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 est encore à l'utiliser. C'est pourquoi nous avons IllegalStateException crash.

nous devons nous assurer que personne n'utilise la base de données et seulement ensuite la fermer. Certaines personnes sur stackoveflow recommandé de ne jamais fermer votre SQLiteDatabase . Cela semble non seulement stupide, mais aussi vous honorer en suivant logcat message.

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 vous avez besoin de la base de données vous devriez appeler openDatabase() méthode de DatabaseManager classe. Dans cette méthode, nous avons un compteur, qui indique combien de fois la base de données est ouverte. Si elle est égale à un, cela signifie que nous devons créer une nouvelle connexion de base de données, sinon, la connexion de base de données est déjà créée.

la même chose se produit dans closeDatabase () méthode. Chaque fois que nous appelons cette méthode, le compteur est diminué, à chaque fois qu'il va à zéro, nous fermons la connexion de base de données.


Maintenant, vous devriez être en mesure d'utiliser votre base de données et être sûr qu'il est thread sûr.

177
répondu Dmytro Danylyk 2018-07-05 09:22:24
  • utilisez un Thread ou AsyncTask pour les opérations de longue durée (50ms+). Testez votre application pour voir où c'est. La plupart des opérations (probablement) ne nécessitent pas de thread, parce que la plupart des opérations (probablement) impliquent seulement quelques lignes. Utilisez un thread pour les opérations en vrac.
  • partager une SQLiteDatabase instance pour chaque DB sur le disque entre les threads et mettre en œuvre un système de comptage pour garder la trace des connexions ouvertes.

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

partagez un champ statique entre toutes vos classes. J'ai utilisé pour garder un singleton autour de cela et d'autres choses qui doivent être partagées. Un système de comptage (utilisant généralement AtomicInteger) devrait également être utilisé pour s'assurer que vous ne fermez jamais la base de données en avance ou la laisser ouverte.

ma solution:

pour les plus récents version, 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 le télécharger à partir de github)
  2. étendre DatabaseManager et mettre en œuvre onCreate et onUpgrade comme vous normalement serait. Vous pouvez créer plusieurs sous-classes de la classe DatabaseManager afin d'avoir différentes bases de données sur disque.
  3. instanciez votre sous-classe et appelez getDb() pour utiliser la classe SQLiteDatabase .
  4. appel close() pour chaque sous-classe que vous instanciez

le code de 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();
                }
        } 
    }
}
16
répondu Jakar 2014-11-03 23:09:17

la base de données est très flexible avec multi-threading. Mes applications frappent leur DBs à partir de nombreux fils différents simultanément et il fait très bien. Dans certains cas, j'ai plusieurs processus qui frappent 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, c'est OK pour accéder à la base de données à partir de différentes tâches.

10
répondu Brad Hein 2010-03-22 16:14:17

la réponse de Dmytro fonctionne très 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 l'exception du pointeur null sinon, par exemple getawritabledatabase non encore retourné dans un thread et openDatabse appelé dans un autre thread entretemps.

public synchronized SQLiteDatabase openDatabase() {
        if(mOpenCounter.incrementAndGet() == 1) {
            // Opening new database
            mDatabase = mDatabaseHelper.getWritableDatabase();
        }
        return mDatabase;
    }
6
répondu gonglong 2014-01-07 09:01:27

ma compréhension des API SQLiteDatabase est que dans le cas où vous avez une application multi-threadée, vous ne pouvez pas vous permettre d'avoir plus d'un objet SQLiteDatabase pointant vers une base de données unique.

l'objet peut certainement être créé mais les inserts/mises à jour échouent si différents threads/processus (aussi) commencent à utiliser différents objets SQLiteDatabase (comme la façon dont nous utilisons la connexion JDBC).

la seule solution ici est de coller avec 1 SQLiteDatabase objects et chaque fois qu'un startTransaction() est utilisé dans plus d'un thread, Android gère le verrouillage entre différents threads et ne permet qu'un thread à la fois pour avoir un accès exclusif à la mise à jour.

aussi, vous pouvez faire" lit " de la base de données et utiliser le même objet SQLiteDatabase dans un fil différent (tandis qu'un autre fil écrit) et il n'y aurait jamais de corruption de base de données I. e "read thread" ne lirait pas les données de la base de données jusqu'à ce que le "write thread"" engage les données bien que les deux utilisent le même objet SQLiteDatabase.

c'est différent de la façon dont l'objet de connexion se trouve dans JDBC où si vous passez autour (utilisez le même) l'objet de connexion entre les threads de lecture et d'écriture, alors nous imprimerions probablement des données non engagées aussi.

dans mon application enterprise, j'essaie d'utiliser des contrôles conditionnels pour que le Thread UI n'ait jamais à attendre, alors que le thread BG contient L'objet SQLiteDatabase (exclusivement). Je essayez de prédire les Actions de L'UI et de différer le fil BG de l'exécution pendant 'x' secondes. On peut aussi maintenir PriorityQueue pour gérer la distribution des objets de connexion SQLiteDatabase de sorte que le Thread de L'interface utilisateur l'obtienne en premier.

4
répondu Swaroop 2011-02-03 09:35:07

après quelques heures de lutte, j'ai découvert que vous ne pouvez utiliser qu'un seul objet DB helper par exécution db. Par exemple,

for(int x = 0; x < someMaxValue; x++)
{
    db = new DBAdapter(this);
    try
    {

        db.addRow
        (
                NamesStringArray[i].toString(), 
                StartTimeStringArray[i].toString(),
                EndTimeStringArray[i].toString()
        );

    }
    catch (Exception e)
    {
        Log.e("Add Error", e.toString());
        e.printStackTrace();
    }
    db.close();
}

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 que je pouvais obtenir mes chaînes dans une base de données par le biais de ma classe helper.

4
répondu dell116 2011-07-01 05:50:19

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 comprenait un close() qui a appelé le helper close comme un miroir de open() qui a appelé getWriteableDatabase et ont ensuite migré à un ContentProvider . Le modèle pour ContentProvider n'utilise pas SQLiteDatabase.close() ce qui est à mon avis Un gros indice car le code utilise getWriteableDatabase dans certains cas je faisais encore l'accès direct (les requêtes de validation d'écran dans la main, j'ai donc migré vers un modèle getawriteabledatabase/rawQuery.

j'utilise un singleton, et il est légèrement inquiétant commentaire dans la proximité de la documentation

Fermer tout ouverte objet de base de données

(mon gras).

donc j'ai eu des accidents intermittents où j'utilise des fils de fond pour accéder à la base de données et ils exécuter en même temps que le premier plan.

donc je pense que close() force la base de données à se fermer indépendamment de tout autre threads tenant des références - donc close() lui-même n'est pas simplement défaire la correspondance getWriteableDatabase mais force fermeture n'importe demandes ouvertes. La plupart du temps, ce n'est pas un problème car le code est un threading simple, mais dans les cas de threads multiples, Il ya toujours la possibilité d'ouvrir et de fermer hors de synchronisation.

après avoir lu des commentaires ailleurs qui expliquent que L'instance de code SqLiteDatabaseHelper compte, alors le seul moment où vous voulez une fermeture est là où vous voulez la situation où vous voulez faire une copie de sauvegarde, et vous voulez forcer toutes les connexions à être fermées et forcer SqLite à écrire loin toutes les choses cachées qui pourraient être loitering environ - en d'autres termes arrêter toute activité de base de données d'application, fermer juste au cas où L'Assistant a perdu la piste, faire n'importe quelle activité de niveau de dossier (sauvegarde/restauration) puis tout recommencer.

bien que cela ressemble à une bonne idée d'essayer de fermer de manière contrôlée, la réalité est que Android se réserve le droit de détruire votre VM de sorte que toute fermeture est de réduire le risque de mises à jour en cache n'étant pas écrit, mais il ne peut pas être garanti si l'appareil est souligné, et si vous avez correctement libéré vos curseurs et les références aux bases de données (qui ne devraient pas être des membres statiques), alors l'Assistant aura fermé la base de données de toute façon.

donc mon point de vue est que l'approche est:

utilisez la base de données getewriteabledatabase pour ouvrir à partir d'un simple wrapper. (J'ai utilisé une classe d'application dérivée pour fournir le contexte d'application à partir d'un statique pour résoudre le besoin d'un contexte).

N'appelle jamais directement close.

ne stocke jamais la base de données résultante dans un objet qui n'a pas une portée évidente et s'appuie sur un comptage de référence pour déclencher une implicite close().

si vous effectuez le traitement au niveau du fichier, arrêtez toute activité de la base de données, puis appelez close juste au cas où il y aurait un fil de fuite sur la supposition que vous écrivez des transactions correctes de sorte que le fil de fuite échouera et que la base de données fermée aura au moins des transactions correctes plutôt que potentiellement une copie au niveau du fichier d'une transaction partielle.

3
répondu Ian Spencer 2017-03-03 21:47:44

vous pouvez essayer d'appliquer une nouvelle approche d'architecture rebondi sur Google I/O 2017.

comprend également la nouvelle bibliothèque ORM appelée Room

contient trois composants principaux: @Entity, @Dao et @Database

de l'Utilisateur.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();
}
3
répondu Zimbo Rodger 2017-05-23 19:17:59

je sais que la réponse est tardive, mais la meilleure façon d'exécuter sqlite requêtes dans android est par l'intermédiaire d'un fournisseur de contenu personnalisé. De cette façon, L'interface utilisateur est découplée avec la classe de la base de données(la classe qui étend la classe SQLiteOpenHelper). Les requêtes sont également exécutées dans un thread de fond(curseur Loader).

0
répondu Theo 2017-11-24 06:13:11