Quali sono le migliori pratiche per SQLite su Android?

Quali sarebbero considerate le migliori pratiche durante l’esecuzione di query su un database SQLite all’interno di un’app Android?

È sicuro eseguire gli inserimenti, eliminare e selezionare le query da un doInBackground di AsyncTask? O dovrei usare il thread UI? Suppongo che le query sul database possano essere “pesanti” e non dovrebbero usare il thread dell’interfaccia utente poiché può bloccare l’app – risultando in un’applicazione non rispondente (ANR).

Se ho diversi AsyncTasks, dovrebbero condividere una connessione o dovrebbero aprire una connessione ciascuno?

Esistono buone pratiche per questi scenari?

Inserti, aggiornamenti, cancellazioni e letture sono generalmente OK da più thread, ma la risposta di Brad non è corretta. Devi stare attento a come crei le tue connessioni e ad usarle. Vi sono situazioni in cui le chiamate di aggiornamento non riusciranno, anche se il database non viene danneggiato.

La risposta di base

L’object SqliteOpenHelper mantiene una connessione al database. Sembra offrirti una connessione in lettura e scrittura, ma in realtà non lo è. Chiama la sola lettura e otterrai la connessione al database di scrittura a prescindere.

Quindi, una istanza di supporto, una connessione db. Anche se lo usi da più thread, una connessione alla volta. L’object SqliteDatabase utilizza i blocchi java per mantenere l’accesso serializzato. Quindi, se 100 thread hanno un’istanza db, le chiamate al database reale su disco sono serializzate.

Quindi, un helper, una connessione db, che viene serializzata in codice java. Un thread, 1000 thread, se si utilizza un’istanza helper condivisa tra di loro, tutto il codice di accesso db è seriale. E la vita è bella (ish).

Se provi a scrivere sul database da connessioni distinte allo stesso tempo, fallirai. Non aspetterà che il primo sia finito e poi scriva. Semplicemente non scriverà la tua modifica. Peggio ancora, se non si chiama la versione corretta di insert / update su SQLiteDatabase, non si otterrà un’eccezione. Riceverai un messaggio nel tuo LogCat, e sarà così.

Quindi, più thread? Usa un aiutante. Periodo. Se SAPETE solo un thread verrà scritto, POTREBBE essere in grado di utilizzare più connessioni, e le tue letture saranno più veloci, ma l’acquirente attenta. Non ho provato molto.

Ecco un post sul blog con molti più dettagli e un’app di esempio.

  • Blocco Android Sqlite (collegamento aggiornato 18/06/2012)
  • Database Android-Locking-Collisions-Esempio di touchlab su GitHub

Gray e I stanno completando uno strumento ORM, basato sul suo Ormlite, che funziona in modo nativo con le implementazioni di database Android, e segue la struttura di creazione / chiamata sicura che descrivo nel post del blog. Dovrebbe uscire molto presto. Guarda.


Nel frattempo, c’è un post sul blog di follow-up:

  • Singola connessione SQLite

Controlla anche il fork di 2point0 dell’esempio di blocco menzionato in precedenza:

  • Database Android: blocco delle collisioni: esempio di 2point0 su GitHub

Accesso simultaneo al database

Lo stesso articolo sul mio blog (mi piace la formattazione di più)

Ho scritto un piccolo articolo che descrive come rendere sicuro l’accesso al thread del tuo database Android.


Supponendo che tu abbia il tuo SQLiteOpenHelper .

public class DatabaseHelper extends SQLiteOpenHelper { ... } 

Ora vuoi scrivere i dati nel database in thread separati.

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

Riceverai il seguente messaggio nel tuo logcat e una delle tue modifiche non verrà scritta.

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

Questo sta accadendo perché ogni volta che crei un nuovo object SQLiteOpenHelper stai creando una nuova connessione al database. Se provi a scrivere sul database da connessioni distinte allo stesso tempo, fallirai. (dalla risposta sopra)

Per utilizzare il database con più thread, dobbiamo assicurarci di utilizzare una connessione al database.

Facciamo in modo che Database Manager della class singleton conservi e restituisca un singolo object 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(); } } 

Il codice aggiornato che scrive i dati nel database in thread separati sarà simile a questo.

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

Questo ti porterà un altro incidente.

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

Poiché utilizziamo una sola connessione al database, il metodo getDatabase () restituisce la stessa istanza dell’object SQLiteDatabase per Thread1 e Thread2 . Cosa sta succedendo, Thread1 potrebbe chiudere il database, mentre Thread2 lo sta ancora utilizzando. Ecco perché abbiamo il crash di IllegalStateException .

Dobbiamo accertarci che nessuno stia utilizzando il database e solo dopo chiuderlo. Alcuni utenti su stackoveflow consigliano di non chiudere mai SQLiteDatabase . Non solo suona stupido ma ti onora anche con il seguente messaggio di logcat.

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

Campione di lavoro

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

Usalo come segue.

 SQLiteDatabase database = DatabaseManager.getInstance().openDatabase(); database.insert(...); // database.close(); Don't close it directly! DatabaseManager.getInstance().closeDatabase(); // correct way 

Ogni volta che hai bisogno di un database dovresti chiamare il metodo openDatabase () della class DatabaseManager . All’interno di questo metodo, abbiamo un contatore, che indica quante volte il database è aperto. Se è uguale a uno, significa che dobbiamo creare una nuova connessione al database, in caso contrario, la connessione al database è già stata creata.

Lo stesso succede nel metodo closeDatabase () . Ogni volta che chiamiamo questo metodo, il contatore viene diminuito, ogni volta che si azzera, stiamo chiudendo la connessione al database.


Ora dovresti essere in grado di usare il tuo database ed essere sicuro che sia thread-safe.

  • Utilizzare una Thread o AsyncTask per AsyncTask di lunga durata (50 ms +). Metti alla prova la tua app per vedere dove si trova. La maggior parte delle operazioni (probabilmente) non richiede un thread, perché la maggior parte delle operazioni (probabilmente) riguarda solo poche righe. Utilizzare un thread per operazioni di massa.
  • Condividi un’istanza SQLiteDatabase per ogni DB su disco tra i thread e implementa un sistema di conteggio per tenere traccia delle connessioni aperte.

Esistono buone pratiche per questi scenari?

Condividi un campo statico tra tutte le tue classi. Ho usato per tenere un singleton in giro per questo e altre cose che devono essere condivise. Anche uno schema di conteggio (generalmente utilizzando AtomicInteger) dovrebbe essere usato per assicurarsi di non chiudere mai il database in anticipo o lasciarlo aperto.

La mia soluzione:

Per la versione più recente, vedere https://github.com/JakarCo/databasemanager ma cercherò di mantenere il codice aggiornato anche qui. Se vuoi capire la mia soluzione, guarda il codice e leggi i miei appunti. I miei appunti sono di solito piuttosto utili.

  1. copia / incolla il codice in un nuovo file chiamato DatabaseManager . (o scaricarlo da github)
  2. estendere DatabaseManager e implementare onCreate e onUpgrade come faresti normalmente. È ansible creare più sottoclassi di una class DatabaseManager in modo da avere diversi database su disco.
  3. getDb() sottoclass e chiama getDb() per utilizzare la class SQLiteDatabase .
  4. Chiama close() per ogni sottoclass che hai istanziato

Il codice da copiare / incollare :

 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 Android SQLite Libray 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 dbMap = new ConcurrentHashMap(); private static final Object lockObject = new Object(); private DBSQLiteOpenHelper sqLiteOpenHelper; private SQLiteDatabase db; private Context context; /** Instantiate a new DB Helper. * 
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 *
If the new counter is 0, then the database will be closed. *

This needs to be called before application exit. *
If the counter is 0, then the underlying SQLiteDatabase is null 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(); } } } }

Il database è molto flessibile con multi-threading. Le mie app colpiscono i loro DB da molti thread diversi contemporaneamente e lo fa alla perfezione. In alcuni casi ho più processi che colpiscono simultaneamente il DB e anche questo funziona bene.

Le tue attività asincrone – usa la stessa connessione quando puoi, ma se necessario, è OK per accedere al DB da diverse attività.

La risposta di Dmytro funziona bene per il mio caso. Penso che sia meglio dichiarare la funzione come sincronizzata. almeno per il mio caso, invocerebbe altrimenti l’eccezione del puntatore nullo, ad esempio getWritableDatabase non è ancora stato restituito in un thread e openDatabse ha chiamato in un altro thread nel frattempo.

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

La mia comprensione delle API SQLiteDatabase è che nel caso in cui si disponga di un’applicazione multi-thread, non è ansible avere più di un object SQLiteDatabase che punta a un singolo database.

L’object può sicuramente essere creato ma gli inserimenti / gli aggiornamenti non riescono se diversi thread / processi (anche) iniziano a utilizzare oggetti SQLiteDatabase diversi (come il modo in cui utilizziamo in Connessione JDBC).

L’unica soluzione qui è quella di attenersi a 1 oggetti SQLiteDatabase e ogni volta che un startTransaction () viene utilizzato in più di 1 thread, Android gestisce il blocco su thread diversi e consente solo 1 thread alla volta di avere accesso esclusivo agli aggiornamenti.

Inoltre è ansible eseguire “Letture” dal database e utilizzare lo stesso object SQLiteDatabase in un thread diverso (mentre un altro thread scrive) e non ci sarebbe mai il danneggiamento del database, ad esempio “read thread” non leggerebbe i dati dal database fino al ” scrivi thread “esegue il commit dei dati sebbene entrambi utilizzino lo stesso object SQLiteDatabase.

Questo è diverso dal modo in cui l’object di connessione è in JDBC, dove se si passa (usa lo stesso) l’object di connessione tra thread di lettura e scrittura allora probabilmente stamperemo anche dati non salvati.

Nella mia applicazione aziendale, cerco di utilizzare i controlli condizionali in modo che il thread dell’interfaccia utente non debba mai attendere, mentre il thread BG contiene l’object SQLiteDatabase (esclusivamente). Provo a predire le azioni dell’interfaccia utente e rinvio dell’esecuzione di BG per secondi “x”. Inoltre, è ansible mantenere PriorityQueue per gestire la distribuzione degli oggetti SQLiteDatabase Connection in modo che il thread dell’interfaccia utente lo ottenga per primo.

dopo aver lottato con questo per un paio d’ore, ho scoperto che puoi usare solo un object db helper per esecuzione db. Per esempio,

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

come indicato a:

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

creare un nuovo DBAdapter ogni volta che il ciclo itera era l'unico modo per ottenere le mie stringhe in un database attraverso la mia class helper.

Avendo avuto alcuni problemi, penso di aver capito perché ho sbagliato.

Avevo scritto una class wrapper del database che includeva un close() che chiamava l’helper close come mirror di open() che chiamava getWriteableDatabase e quindi era migrato su ContentProvider . Il modello per ContentProvider non usa SQLiteDatabase.close() che penso sia un grosso indizio visto che il codice usa getWriteableDatabase In alcuni casi stavo ancora facendo l’accesso diretto (query di convalida dello schermo nel main così ho migrato a un modello getWriteableDatabase / rawQuery .

Io uso un singleton e c’è un commento un po ‘inquietante nella stretta documentazione

Chiudere qualsiasi object di database aperto

(il mio audace).

Quindi ho avuto arresti intermittenti in cui utilizzo thread in background per accedere al database e vengono eseguiti contemporaneamente ai foreground.

Quindi penso che close() costringa il database a chiudere indipendentemente da altri thread che contengono riferimenti – così close() non sta semplicemente annullando il corrispondente getWriteableDatabase ma forza la chiusura di eventuali richieste aperte. Il più delle volte questo non è un problema in quanto il codice è single threading, ma nei casi multi-thread c’è sempre la possibilità di aprire e chiudere la sincronizzazione.

Avendo letto commenti altrove che spiegano che l’istanza del codice SqLiteDatabaseHelper conta, l’unica volta in cui si desidera una chiusura è dove si desidera la situazione in cui si desidera eseguire una copia di backup e si desidera forzare tutte le connessioni a chiudersi e forzare SqLite a annota qualsiasi materiale memorizzato nella cache che potrebbe essere bighellonare – in altre parole, interrompe tutte le attività del database dell’applicazione, chiudi nel caso in cui l’helper abbia perso la traccia, esegua qualsiasi attività a livello di file (backup / ripristino) e ricominci da capo.

Anche se sembra una buona idea provare e chiudere in modo controllato, la realtà è che Android si riserva il diritto di cestinare la tua VM in modo che ogni chiusura riduca il rischio di non scrivere gli aggiornamenti nella cache, ma non può essere garantita se il dispositivo è stressato, e se hai correttamente liberato i tuoi cursori e riferimenti ai database (che non dovrebbero essere membri statici) allora l’helper avrà comunque chiuso il database.

Quindi la mia opinione è che l’approccio è:

Usa getWriteableDatabase per aprire da un wrapper singleton. (Ho usato una class di applicazione derivata per fornire il contesto dell’applicazione da una statica per risolvere la necessità di un contesto).

Mai chiamare direttamente vicino.

Non archiviare mai il database risultante in alcun object che non ha un ambito ovvio e si basano sul conteggio dei riferimenti per triggersre una chiusura implicita ().

Se si sta eseguendo la gestione a livello di file, arrestare tutte le attività del database e quindi chiamare close solo nel caso in cui ci sia un thread in fuga nel presupposto che si scrivano le transazioni corrette in modo che il thread in esecuzione fallisca e il database chiuso avrà almeno transazioni appropriate piuttosto che potenzialmente una copia a livello di file di una transazione parziale.

Puoi provare ad applicare un nuovo approccio all’architettura aggiunto a Google I / O 2017.

Include anche una nuova libreria ORM chiamata Room

Contiene tre componenti principali: @Entity, @Dao e @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 getAll(); @Query("SELECT * FROM user WHERE uid IN (:userIds)") List 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(); } 

So che la risposta è in ritardo, ma il modo migliore per eseguire query sqlite in Android è attraverso un fornitore di contenuti personalizzati. In questo modo l’interfaccia utente è disaccoppiata con la class del database (la class che estende la class SQLiteOpenHelper). Anche le query vengono eseguite in un thread in background (Cursor Loader).