Quali sono le migliori pratiche per SQLite su Android?


694

Quali sarebbero le migliori pratiche quando si eseguono query su un database SQLite all'interno di un'app Android?

È sicuro eseguire inserimenti, eliminazioni e selezionare query da doInBackground di un AsyncTask? O dovrei usare il thread dell'interfaccia utente? Suppongo che le query del database possano essere "pesanti" e non dovrebbero usare il thread dell'interfaccia utente in quanto può bloccare l'app, risultando in un'applicazione non rispondente (ANR).

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

Esistono buone pratiche per questi scenari?


10
Qualunque cosa tu faccia, ricorda di disinfettare i tuoi input se il tuo provider di contenuti (o interfaccia SQLite) è pubblicamente esposto!
Kristopher Micinski,

37
Sicuramente NON dovresti fare accessi in db dal thread dell'interfaccia utente, posso dirti così tanto.
Edward Falk,

@EdwardFalk Perché no? Sicuramente ci sono casi d'uso in cui è valido farlo?
Michael,

4
Se si eseguono operazioni di I / O, accessi alla rete, ecc. Dal thread dell'interfaccia utente, l'intero dispositivo si blocca fino al completamento dell'operazione. Se si completa in 1/20 di secondo, va bene. Se impiega più tempo, hai una brutta esperienza utente.
Edward Falk,

Risposte:


631

Inserimenti, aggiornamenti, eliminazioni 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 le usi. Ci sono situazioni in cui le chiamate di aggiornamento non riusciranno, anche se il database non viene danneggiato.

La risposta di base

L'oggetto SqliteOpenHelper mantiene una connessione al database. Sembra offrirti una connessione in lettura e scrittura, ma in realtà no. Chiama la sola lettura e otterrai la connessione al database di scrittura a prescindere.

Quindi, un'istanza di supporto, una connessione db. Anche se lo usi da più thread, una connessione alla volta. L'oggetto SqliteDatabase utilizza i blocchi Java per mantenere l'accesso serializzato. Pertanto, se 100 thread hanno un'istanza db, le chiamate al database su disco effettivo vengono serializzate.

Quindi, un aiutante, una connessione db, serializzata in codice java. Un thread, 1000 thread, se si utilizza un'istanza di supporto condivisa tra loro, tutto il codice di accesso al database è seriale. E la vita è buona (ish).

Se si tenta di scrivere nel database da connessioni distinte effettive contemporaneamente, si verificherà un errore. Non aspetterà fino a quando il primo non sarà terminato e poi scriverà. Semplicemente non scriverà la tua modifica. Peggio ancora, se non chiami la versione corretta di insert / update su SQLiteDatabase, non otterrai un'eccezione. Riceverai semplicemente un messaggio nel tuo LogCat e sarà quello.

Quindi, più thread? Usa un aiutante. Periodo. Se SAPETE che verrà scritto solo un thread, POTRETE essere in grado di utilizzare più connessioni e le vostre letture saranno più veloci, ma il compratore deve fare attenzione. Non ho provato così tanto.

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

Gray e io stiamo effettivamente avvolgendo uno strumento ORM, basato sul suo Ormlite, che funziona nativamente con le implementazioni del database Android e segue la struttura di creazione / chiamata sicura che descrivo nel post del blog. Dovrebbe uscire presto. Guarda.


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

Controlla anche la forcella di 2point0 dell'esempio di blocco precedentemente menzionato:


2
A parte questo, il supporto Android di Ormlite è disponibile all'indirizzo ormlite.sourceforge.net/sqlite_java_android_orm.html . Ci sono progetti di esempio, documentazione e vasetti.
Grigio,

1
Un secondo a parte. Il codice ormlite ha classi di supporto che possono essere utilizzate per gestire le istanze di dbhelper. Puoi usare le cose di ormlite, ma non è richiesto. Puoi usare le classi helper solo per fare la gestione della connessione.
Kevin Galligan,

31
Kāgii, grazie per la spiegazione dettagliata. Potresti chiarire una cosa: capisco che dovresti avere UN helper, ma dovresti anche avere solo una connessione (cioè un oggetto SqliteDatabase)? In altre parole, quanto spesso dovresti chiamare getWritableDatabase? E altrettanto importante quando chiami close ()?
Artem

3
Ho aggiornato il codice. L'originale è andato perso quando ho cambiato host di blog, ma ho aggiunto alcuni esempi di codice ridotto che dimostreranno il problema. Inoltre, come gestisci la singola connessione? Inizialmente avevo una soluzione molto più complicata, ma da allora l'ho modificata. Dai un'occhiata qui: touchlab.co/uncategorized/single-sqlite-connection
Kevin Galligan,

è necessario disporre di una connessione e dove farlo?
tugce

188

Accesso simultaneo al database

Stesso articolo sul mio blog (mi piace più la formattazione)

Ho scritto un piccolo articolo che descrive come rendere sicuro l'accesso al thread del 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();

Verrà visualizzato il seguente messaggio nel logcat e una delle modifiche non verrà scritta.

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

Questo accade perché ogni volta che crei un nuovo oggetto SQLiteOpenHelper stai effettivamente realizzando una nuova connessione al database. Se si tenta di scrivere nel database da connessioni distinte effettive contemporaneamente, si verificherà un errore. (dalla risposta sopra)

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

Facciamo Database Manager di classe singleton che conterrà e restituirà un singolo oggetto 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 al seguente.

 // 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é stiamo utilizzando una sola connessione al database, metodo getDatabase () restituire stessa istanza SQLiteDatabase oggetto per Filettatura1 e Thread2 . Quello che sta succedendo, Thread1 potrebbe chiudere il database, mentre Thread2 lo sta ancora usando. Ecco perché abbiamo crash IllegalStateException .

Dobbiamo assicurarci che nessuno stia usando il database e solo allora lo chiuda. Alcune persone su Stackoveflow consigliano di non chiudere mai il database SQLite . Ciò comporterà il seguente messaggio 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 è necessario il database, è necessario chiamare il metodo openDatabase () della classe DatabaseManager . All'interno di questo metodo, abbiamo un contatore, che indica quante volte viene aperto il database. Se è uguale a uno, significa che è necessario creare una nuova connessione al database, in caso contrario, la connessione al database è già stata creata.

Lo stesso accade nel metodo closeDatabase () . Ogni volta che chiamiamo questo metodo, il contatore viene ridotto, ogni volta che va a zero, chiudiamo la connessione al database.


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


10
Sono una di quelle persone che ti suggeriscono di non chiudere mai il db. Si ottiene un errore "Perdita rilevata" solo se si apre il db, non si chiude, quindi si tenta di aprire nuovamente. Se usi solo un singolo helper aperto e non chiudi mai il db, non ottieni questo errore. In caso contrario, fammi sapere (con il codice). Ho avuto un post più lungo al riguardo da qualche parte, ma non riesco a trovarlo. Domanda a cui ha risposto commonsware qui, che in qualche modo ci sorprende entrambi nel dipartimento dei punti SO: stackoverflow.com/questions/7211941/…
Kevin Galligan,

4
Più pensieri. # 1, creerei l'aiutante all'interno del tuo manager. Chiedere problemi per averlo fuori. Il nuovo sviluppatore potrebbe chiamare direttamente l'aiutante per qualche pazzo motivo. Inoltre, se hai bisogno di un metodo init, genera un'eccezione se l'istanza esiste già. Le app multi-db ovviamente falliranno così come sono. # 2, perché il campo mDatabase? È disponibile presso l'aiutante. # 3, come primo passo verso "non chiudere mai", cosa succede al tuo db quando l'app si blocca e non è "chiusa"? Suggerimento, niente. Va bene, perché SQLite è super stabile. Questo è stato il passaggio 1 per capire perché non è necessario chiuderlo.
Kevin Galligan,

1
Qual è il motivo per cui usi un metodo di inizializzazione pubblica per chiamare prima di ottenere l'istanza? Perché non avere un costruttore privato chiamato if(instance==null)? Non lasci altra scelta che chiamare ogni volta l'inizializzazione; in quale altro modo sapresti se è stato inizializzato o meno in altre applicazioni ecc.?
ChiefTwoPencils,

1
initializeInstance()ha un parametro di tipo SQLiteOpenHelper, ma nel tuo commento hai menzionato di usare DatabaseManager.initializeInstance(getApplicationContext());. Cosa sta succedendo? Come può funzionare?
faizal

2
@DmytroDanylyk "DatabaseManager è un thread singolo sicuro, quindi può essere utilizzato ovunque", il che non è vero in risposta alla domanda di Virsir. Gli oggetti non sono processi incrociati condivisi. Il tuo DatabaseManager non avrà uno stato in un processo diverso come in un adattatore di sincronizzazione (android: process = ": sync")
sfrigola il

17
  • Utilizzare a Threado AsyncTaskper operazioni di lunga durata (50ms +). Prova la tua app per vedere dove si trova. La maggior parte delle operazioni (probabilmente) non richiede un thread, poiché la maggior parte delle operazioni (probabilmente) coinvolge solo poche righe. Utilizzare un thread per operazioni in blocco.
  • Condividi SQLiteDatabaseun'istanza per ciascun DB sul disco tra 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. Tenevo un singleton in giro per quello e altre cose che devono essere condivise. È inoltre consigliabile utilizzare uno schema di conteggio (generalmente utilizzando AtomicInteger) per assicurarsi di non chiudere mai il database in anticipo o di lasciarlo aperto.

La mia soluzione:

Per la versione più recente, consultare https://github.com/JakarCo/databasemanager ma cercherò di mantenere aggiornato il codice anche qui. Se vuoi capire la mia soluzione, guarda il codice e leggi le mie note. Le mie note di solito sono molto utili.

  1. copia / incolla il codice in un nuovo file chiamato DatabaseManager. (o scaricalo da github)
  2. estendere DatabaseManagere implementare onCreatee onUpgradecome faresti normalmente. È possibile creare più sottoclassi di una DatabaseManagerclasse per avere diversi database sul disco.
  3. Crea un'istanza della sottoclasse e chiama getDb()per utilizzare la SQLiteDatabaseclasse.
  4. Chiama close()per ogni sottoclasse che hai creato un'istanza

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

1
Cosa succede quando chiami "chiudi" e poi provi a riutilizzare la classe? andrà in crash? o si reinizializzerà automaticamente per poter riutilizzare il DB?
sviluppatore Android il

1
@androiddeveloper, Se chiami close, devi chiamare di opennuovo prima di usare la stessa istanza della classe OPPURE puoi creare una nuova istanza. Dal momento che nel closecodice, ho impostato db=null, non saresti in grado di utilizzare il valore restituito da getDb(poiché sarebbe nullo), quindi otterrai un NullPointerExceptionse facessi qualcosa del generemyInstance.close(); myInstance.getDb().query(...);
Reed,

Perché non combinare getDb()e open()in un unico metodo?
Alex Burdusel,

@Burdu, un mix di fornire ulteriore controllo sul contatore del database e cattiva progettazione. Tuttavia, non è sicuramente il modo migliore per farlo. Lo aggiornerò tra qualche giorno.
Reed,

@Burdu, l'ho appena aggiornato. Puoi ottenere il nuovo codice da qui . Non l'ho provato, quindi per favore fatemi sapere se dovrei eseguire il commit delle modifiche.
Reed,

11

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

Attività asincrone: utilizzare la stessa connessione quando è possibile, ma se è necessario, è OK per accedere al DB da attività diverse.


Inoltre, hai lettori e scrittori in connessioni diverse o dovrebbero condividere una singola connessione? Grazie.
Grigio

@Gray: giusto, avrei dovuto menzionarlo esplicitamente. Per quanto riguarda le connessioni, utilizzerei la stessa connessione il più possibile, ma poiché il blocco è gestito a livello di filesystem, è possibile aprirlo più volte nel codice, ma utilizzerei una singola connessione il più possibile. Il DB sqlite di Android è molto flessibile e tollerante.
Brad Hein,

3
@Gray, volevo solo pubblicare informazioni aggiornate per le persone che potrebbero utilizzare questo metodo. La documentazione dice: questo metodo ora non fa nulla. Non usare.
Pijusn,

3
Ho scoperto che questo metodo fallisce in modo orribile, stiamo passando a ContentProvider per accedere da più applicazioni. Dovremo fare un po 'di concorrenza sui nostri metodi, ma ciò dovrebbe risolvere eventuali problemi con i processi che accedono tutti ai dati contemporaneamente.
JPM,

2
So che questo è vecchio, ma non è corretto. Si potrebbe lavorare bene per l'accesso ai database di diversi SQLiteDatabaseoggetti su diversi AsyncTasks / Threads, ma a volte portare a errori ed è per questo SQLiteDatabase (linea 1297) usi Locks
Reed

7

La risposta di Dmytro funziona bene per il mio caso. Penso che sia meglio dichiarare la funzione sincronizzata. almeno per il mio caso, invocherebbe un'eccezione puntatore null altrimenti, ad esempio getWritableDatabase non ancora restituito in un thread e openDatabse chiamato nel frattempo in un altro thread.

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

mDatabaseHelper.getWritableDatabase (); Questo non creerà un nuovo oggetto di database
periferico

5

dopo aver lottato con questo per un paio d'ore, ho scoperto che puoi usare solo un oggetto helper db per esecuzione di 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 apposto 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();

la creazione di un nuovo DBAdapter ogni volta che il ciclo viene ripetuto è stato l'unico modo per ottenere le mie stringhe in un database tramite la mia classe helper.


4

La mia comprensione delle API di SQLiteDatabase è che nel caso in cui si disponga di un'applicazione multi thread, non è possibile avere più di 1 oggetto SQLiteDatabase che punta a un singolo database.

L'oggetto può sicuramente essere creato ma gli inserimenti / gli aggiornamenti falliscono se anche diversi thread / processi (anche) iniziano a usare diversi oggetti SQLiteDatabase (come il modo in cui utilizziamo in JDBC Connection).

L'unica soluzione qui è quella di rimanere con 1 oggetti SQLiteDatabase e ogni volta che viene utilizzato startTransaction () in più di 1 thread, Android gestisce il blocco tra thread diversi e consente a 1 solo thread alla volta di avere accesso esclusivo all'aggiornamento.

Inoltre puoi fare "Letture" dal database e usare lo stesso oggetto SQLiteDatabase in un thread diverso (mentre un altro thread scrive) e non ci sarebbe mai la corruzione del database, ad esempio "leggi thread" non leggerebbe i dati dal database fino al " write thread "esegue il commit dei dati sebbene entrambi utilizzino lo stesso oggetto SQLiteDatabase.

Questo è diverso dal modo in cui l'oggetto connessione è in JDBC dove se si passa (usa lo stesso) l'oggetto connessione tra i thread di lettura e scrittura, probabilmente stamperemmo anche i dati senza commit.

Nella mia applicazione aziendale, provo a utilizzare i controlli condizionali in modo che il thread dell'interfaccia utente non debba mai attendere, mentre il thread BG contiene l'oggetto SQLiteDatabase (esclusivamente). Provo a prevedere le azioni dell'interfaccia utente e rinviare l'esecuzione del thread BG per 'x' secondi. Inoltre è possibile mantenere PriorityQueue per gestire la distribuzione di oggetti SQLiteDatabase Connection in modo che il thread dell'interfaccia utente ottenga per primo.


E cosa metti in PriorityQueue: listener (che vogliono ottenere oggetti di database) o query SQL?
Pijusn,

Non ho usato l'approccio della coda di priorità, ma essenzialmente i thread "chiamante".
Swaroop,

@Swaroop: PCMIIW, "read thread" wouldn't read the data from the database till the "write thread" commits the data although both use the same SQLiteDatabase object. Questo non è sempre vero, se avvii "leggi thread" subito dopo "scrivi thread" potresti non ottenere i dati appena aggiornati (inseriti o aggiornati nel thread di scrittura). Leggi thread potrebbe leggere i dati prima dell'inizio del thread di scrittura. Ciò accade perché inizialmente l'operazione di scrittura abilita il blocco riservato invece del blocco esclusivo.
Amit Vikram Singh,

4

Puoi provare ad applicare il nuovo approccio di architettura annunciato 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<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();
}

Non suggerisco spazio a un database con più relazioni da N a N, perché non gestisce bene questo approccio e devi scrivere molto codice per trovare una soluzione per queste relazioni.
AlexPad,

3

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

Avevo scritto una classe wrapper di database che includeva una close()che chiamava helper vicino come un mirror di open()cui si chiamava getWriteableDatabase e quindi sono migrata in a ContentProvider. Il modello per ContentProvidernon usa, SQLiteDatabase.close()che penso sia un grande indizio come il codice usagetWriteableDatabase In alcuni casi stavo ancora facendo l'accesso diretto (query di convalida dello schermo nel principale, quindi sono migrato a un modello getWriteableDatabase / rawQuery.

Uso un singleton e c'è un commento un po 'inquietante nella stretta documentazione

Chiudi qualsiasi oggetto di database aperto

(il mio coraggio).

Quindi ho avuto crash intermittenti in cui utilizzo i thread in background per accedere al database e vengono eseguiti contemporaneamente al primo piano.

Quindi penso che close()forza la chiusura del database indipendentemente da qualsiasi altro thread che close()contiene riferimenti, quindi non si sta semplicemente annullando la corrispondenza, getWriteableDatabasema forza la chiusura eventuali richieste aperte. Il più delle volte questo non è un problema poiché il codice è single threading, ma in casi multi-thread c'è sempre la possibilità di aprire e chiudere fuori sincrono.

Dopo aver letto i commenti altrove che spiegano che l'istanza del codice SqLiteDatabaseHelper conta, l'unica volta che si desidera una chiusura è dove si desidera la situazione in cui si desidera eseguire una copia di backup e si desidera forzare la chiusura di tutte le connessioni e forzare SqLite a annota tutte le cose memorizzate nella cache che potrebbero indugiare: in altre parole, interrompi tutte le attività del database dell'applicazione, chiudi nel caso in cui l'helper abbia perso traccia, esegui qualsiasi attività a livello di file (backup / ripristino), quindi ricomincia da capo.

Anche se sembra una buona idea provare a chiudere in modo controllato, la realtà è che Android si riserva il diritto di eliminare la VM in modo che qualsiasi chiusura stia riducendo il rischio che gli aggiornamenti memorizzati nella cache non vengano scritti, ma non può essere garantito se il dispositivo è stressato e se hai liberato correttamente i cursori e i riferimenti ai database (che non dovrebbero essere membri statici), l'helper avrà comunque chiuso il database.

Quindi la mia opinione è che l'approccio è:

Utilizzare getWriteableDatabase per aprire da un wrapper singleton. (Ho usato una classe di applicazione derivata per fornire il contesto dell'applicazione da uno statico per risolvere la necessità di un contesto).

Non chiamare mai direttamente vicino.

Non archiviare mai il database risultante in nessun oggetto che non ha un ambito ovvio e fare affidamento sul conteggio dei riferimenti per attivare una chiusura implicita ().

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


0

So che la risposta è in ritardo, ma il modo migliore per eseguire query sqlite in Android è attraverso un fornitore di contenuti personalizzati. In tal modo l'interfaccia utente viene disaccoppiata con la classe database (la classe che estende la classe SQLiteOpenHelper). Inoltre, le query vengono eseguite in un thread in background (Caricatore cursore).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.