Was sind die Best Practices für SQLite unter Android?

694

Was wäre die beste Vorgehensweise beim Ausführen von Abfragen in einer SQLite-Datenbank in einer Android-App?

Ist es sicher, Einfügungen auszuführen, zu löschen und Abfragen aus dem doInBackground einer AsyncTask auszuwählen? Oder sollte ich den UI-Thread verwenden? Ich nehme an, dass Datenbankabfragen "schwer" sein können und nicht den UI-Thread verwenden sollten, da er die App blockieren kann - was zu einer ANR ( Application Not Responding ) führt.

Wenn ich mehrere AsyncTasks habe, sollten sie eine Verbindung gemeinsam nutzen oder jeweils eine Verbindung öffnen?

Gibt es Best Practices für diese Szenarien?

Vidar Vestnes
quelle
10
Was auch immer Sie tun, denken Sie daran, Ihre Eingaben zu bereinigen, wenn Ihr Inhaltsanbieter (oder Ihre SQLite-Oberfläche) öffentlich konfrontiert ist!
Kristopher Micinski
37
Sie sollten auf keinen Fall Datenbankzugriffe über den UI-Thread ausführen, das kann ich Ihnen sagen.
Edward Falk
@ EdwardFalk Warum nicht? Sicher gibt es Anwendungsfälle, in denen dies gültig ist?
Michael
4
Wenn Sie über den UI-Thread E / A, Netzwerkzugriffe usw. ausführen, friert das gesamte Gerät ein, bis der Vorgang abgeschlossen ist. Wenn es in 1/20 Sekunde fertig ist, dann gut. Wenn es länger dauert, haben Sie eine schlechte Benutzererfahrung.
Edward Falk

Antworten:

632

Einfügungen, Aktualisierungen, Löschungen und Lesevorgänge von mehreren Threads sind im Allgemeinen in Ordnung, aber Brads Antwort ist nicht korrekt. Sie müssen vorsichtig sein, wie Sie Ihre Verbindungen erstellen und verwenden. Es gibt Situationen, in denen Ihre Update-Aufrufe fehlschlagen, auch wenn Ihre Datenbank nicht beschädigt wird.

Die grundlegende Antwort.

Das SqliteOpenHelper-Objekt behält eine Datenbankverbindung bei. Es scheint Ihnen eine Lese- und Schreibverbindung zu bieten, tut es aber wirklich nicht. Rufen Sie die schreibgeschützte Verbindung auf, und Sie erhalten unabhängig davon die Verbindung zur Schreibdatenbank.

Also eine Hilfsinstanz, eine Datenbankverbindung. Selbst wenn Sie es von mehreren Threads aus verwenden, jeweils eine Verbindung. Das SqliteDatabase-Objekt verwendet Java-Sperren, um den Zugriff serialisiert zu halten. Wenn also 100 Threads eine Datenbankinstanz haben, werden Aufrufe der eigentlichen On-Disk-Datenbank serialisiert.

Also ein Helfer, eine Datenbankverbindung, die in Java-Code serialisiert ist. Ein Thread, 1000 Threads. Wenn Sie eine zwischen ihnen gemeinsam genutzte Hilfsinstanz verwenden, ist Ihr gesamter Datenbankzugriffscode seriell. Und das Leben ist gut (ish).

Wenn Sie versuchen, gleichzeitig von tatsächlich unterschiedlichen Verbindungen in die Datenbank zu schreiben, schlägt eine fehl. Es wird nicht warten, bis der erste fertig ist und dann schreiben. Es wird einfach nicht Ihre Änderung schreiben. Schlimmer noch, wenn Sie nicht die richtige Version von insert / update in der SQLiteDatabase aufrufen, erhalten Sie keine Ausnahme. Sie erhalten nur eine Nachricht in Ihrem LogCat, und das wird es sein.

Also mehrere Threads? Verwenden Sie einen Helfer. Zeitraum. Wenn Sie wissen, dass nur ein Thread schreibt, können Sie möglicherweise mehrere Verbindungen verwenden, und Ihre Lesevorgänge sind schneller, aber der Käufer muss aufpassen. Ich habe nicht so viel getestet.

Hier ist ein Blog-Beitrag mit viel mehr Details und einer Beispiel-App.

Gray und ich arbeiten gerade an einem ORM-Tool, das auf seiner Ormlite basiert und nativ mit Android-Datenbankimplementierungen funktioniert. Es folgt der sicheren Erstellungs- / Aufrufstruktur, die ich im Blogbeitrag beschrieben habe. Das sollte sehr bald herauskommen. Schau mal.


In der Zwischenzeit gibt es einen nachfolgenden Blog-Beitrag:

Überprüfen Sie auch die Gabel um 2point0 des zuvor erwähnten Verriegelungsbeispiels:

Kevin Galligan
quelle
2
Abgesehen davon finden Sie die Android-Unterstützung von Ormlite unter ormlite.sourceforge.net/sqlite_java_android_orm.html . Es gibt Beispielprojekte, Dokumentationen und Gläser.
Grau
1
Eine Sekunde beiseite. Der ormlite-Code enthält Hilfsklassen, mit denen dbhelper-Instanzen verwaltet werden können. Sie können das ormlite-Zeug verwenden, aber es ist nicht erforderlich. Sie können die Hilfsklassen nur zum Verbindungsmanagement verwenden.
Kevin Galligan
31
Kāgii, danke für die ausführliche Erklärung. Könnten Sie eines klarstellen: Ich verstehe, Sie sollten EINEN Helfer haben, aber sollten Sie auch nur eine Verbindung haben (dh ein SqliteDatabase-Objekt)? Mit anderen Worten, wie oft sollten Sie getWritableDatabase aufrufen? Und genauso wichtig, wann rufen Sie close () auf?
Artem
3
Ich habe den Code aktualisiert. Das Original ging verloren, als ich den Blog-Host wechselte, aber ich fügte einen abgespeckten Beispielcode hinzu, der das Problem demonstriert. Wie verwalten Sie die einzelne Verbindung? Ich hatte anfangs eine viel kompliziertere Lösung, aber seitdem habe ich das geändert. Werfen
Kevin Galligan
Muss die Verbindung entsorgt werden und wo muss sie durchgeführt werden?
Tugce
187

Gleichzeitiger Datenbankzugriff

Gleicher Artikel in meinem Blog (ich formatiere gerne mehr)

Ich habe einen kleinen Artikel geschrieben, der beschreibt, wie Sie den Zugriff auf Ihren Android-Datenbank-Thread sicher machen können.


Angenommen, Sie haben Ihren eigenen SQLiteOpenHelper .

public class DatabaseHelper extends SQLiteOpenHelper { ... }

Jetzt möchten Sie Daten in separaten Threads in die Datenbank schreiben.

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

In Ihrem Logcat wird die folgende Meldung angezeigt, und eine Ihrer Änderungen wird nicht geschrieben.

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

Dies geschieht, weil jedes Mal, wenn Sie einen neuen SQLiteOpenHelper erstellen Objekt tatsächlich eine neue Datenbankverbindung herstellen. Wenn Sie versuchen, gleichzeitig von tatsächlich unterschiedlichen Verbindungen in die Datenbank zu schreiben, schlägt eine fehl. (aus der obigen Antwort)

Um eine Datenbank mit mehreren Threads zu verwenden, müssen wir sicherstellen, dass wir eine Datenbankverbindung verwenden.

Lassen Sie uns den Datenbankmanager der Singleton-Klasse erstellen, der ein einzelnes SQLiteOpenHelper- Objekt enthält und zurückgibt .

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

}

Der aktualisierte Code, der Daten in separaten Threads in die Datenbank schreibt, sieht folgendermaßen aus.

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

Dies wird Ihnen einen weiteren Absturz bringen.

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

Da wir nur eine Datenbankverbindung, Methode verwenden getDatabase () zurückgeben dieselbe Instanz von SQLiteDatabase Objekt für Thread1 und Thread2 . Was geschieht, Thread1 Datenbank schließen kann, während Thread2 es immer noch verwendet , ist. Deshalb haben wir IllegalStateException .

Wir müssen sicherstellen, dass niemand die Datenbank verwendet, und sie erst dann schließen. Einige Leute auf stackoveflow haben empfohlen, Ihre SQLiteDatabase niemals zu schließen . Dies führt zu der folgenden Logcat-Nachricht.

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

Arbeitsprobe

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

        }
    }

}

Verwenden Sie es wie folgt.

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

Jedes Mal, wenn Sie eine Datenbank benötigen, sollten Sie die openDatabase () -Methode von DatabaseManager aufrufen Klasse . Innerhalb dieser Methode haben wir einen Zähler, der angibt, wie oft die Datenbank geöffnet ist. Wenn es gleich eins ist, müssen wir eine neue Datenbankverbindung erstellen. Andernfalls wird bereits eine Datenbankverbindung erstellt.

Das gleiche passiert in der Methode closeDatabase () . Jedes Mal, wenn wir diese Methode aufrufen, wird der Zähler verringert. Immer wenn er auf Null geht, wird die Datenbankverbindung geschlossen.


Jetzt sollten Sie in der Lage sein, Ihre Datenbank zu verwenden und sicherzustellen, dass sie threadsicher ist.

Dmytro Danylyk
quelle
10
Ich bin einer von denen, die vorschlagen, dass Sie die Datenbank niemals schließen. Sie erhalten nur dann den Fehler "Leck gefunden", wenn Sie die Datenbank öffnen, nicht schließen und dann erneut versuchen, sie zu öffnen. Wenn Sie nur einen einzigen offenen Helfer verwenden und die Datenbank niemals schließen, wird dieser Fehler nicht angezeigt. Wenn Sie etwas anderes finden, lassen Sie es mich bitte wissen (mit Code). Ich hatte irgendwo einen längeren Beitrag darüber, kann ihn aber nicht finden. Frage von Commonsware hier beantwortet, die uns beide in der SO-Punkte-Abteilung irgendwie übertrifft
Kevin Galligan
4
Weitere Gedanken. # 1, ich würde den Helfer in Ihrem Manager erstellen. Fragen Sie nach Problemen, um es draußen zu haben. Ein neuer Entwickler könnte den Helfer aus irgendeinem verrückten Grund direkt anrufen. Wenn Sie eine Init-Methode benötigen, lösen Sie eine Ausnahme aus, wenn bereits eine Instanz vorhanden ist. Multi-DB-Apps werden offensichtlich so wie sie sind fehlschlagen. # 2, warum das mDatabase-Feld? Es ist vom Helfer erhältlich. # 3, als erster Schritt in Richtung "nie schließen", was passiert mit Ihrer Datenbank, wenn Ihre App abstürzt und nicht "geschlossen" ist? Hinweis, nichts. Es ist in Ordnung, weil SQLite super stabil ist. Das war Schritt 1, um herauszufinden, warum Sie es nicht schließen müssen.
Kevin Galligan
1
Gibt es einen Grund, warum Sie eine öffentliche Initialisierungsmethode verwenden, um vor dem Abrufen der Instanz aufzurufen? Warum nicht einen privaten Konstruktor haben, der heißt if(instance==null)? Sie haben keine andere Wahl, als jedes Mal die Initialisierung aufzurufen. Wie sonst würden Sie wissen, ob es in anderen Anwendungen usw. initialisiert wurde oder nicht?
ChiefTwoPencils
1
initializeInstance()hat einen Parameter vom Typ SQLiteOpenHelper, aber in Ihrem Kommentar haben Sie erwähnt, zu verwenden DatabaseManager.initializeInstance(getApplicationContext());. Was ist los? Wie kann das möglicherweise funktionieren?
Faizal
2
@DmytroDanylyk "DatabaseManager ist ein thread-sicherer Singleton, daher kann er überall verwendet werden", was auf die Frage von virsir nicht zutrifft. Objekte sind keine gemeinsamen prozessübergreifenden Objekte. Ihr DatabaseManager hat keinen Status in einem anderen Prozess als in einem Synchronisierungsadapter (android: process = ": sync")
Whizzle
17
  • Verwenden Sie ein Threadoder AsyncTaskfür Langzeitoperationen (50 ms +). Testen Sie Ihre App, um zu sehen, wo das ist. Die meisten Operationen erfordern (wahrscheinlich) keinen Thread, da die meisten Operationen (wahrscheinlich) nur wenige Zeilen umfassen. Verwenden Sie einen Thread für Massenoperationen.
  • Teilen Sie eine SQLiteDatabaseInstanz für jede Datenbank auf der Festplatte zwischen Threads und implementieren Sie ein Zählsystem, um offene Verbindungen zu verfolgen.

Gibt es Best Practices für diese Szenarien?

Teilen Sie ein statisches Feld zwischen all Ihren Klassen. Ich habe immer einen Singleton für das und andere Dinge behalten, die geteilt werden müssen. Ein Zählschema (im Allgemeinen mit AtomicInteger) sollte ebenfalls verwendet werden, um sicherzustellen, dass Sie die Datenbank niemals vorzeitig schließen oder offen lassen.

Meine Lösung:

Die aktuellste Version finden Sie unter https://github.com/JakarCo/databasemanager. Ich werde jedoch versuchen, den Code auch hier auf dem neuesten Stand zu halten. Wenn Sie meine Lösung verstehen möchten, schauen Sie sich den Code an und lesen Sie meine Notizen. Meine Notizen sind normalerweise ziemlich hilfreich.

  1. Kopieren Sie den Code und fügen Sie ihn in eine neue Datei mit dem Namen ein DatabaseManager. (oder von github herunterladen)
  2. erweitern DatabaseManagerund implementieren onCreateund onUpgradewie Sie es normalerweise tun würden. Sie können mehrere Unterklassen einer DatabaseManagerKlasse erstellen , um unterschiedliche Datenbanken auf der Festplatte zu haben.
  3. Instanziieren Sie Ihre Unterklasse und rufen Sie getDb()auf, um die SQLiteDatabaseKlasse zu verwenden.
  4. Rufen Sie close()für jede von Ihnen instanziierte Unterklasse auf

Der Code zum Kopieren / Einfügen :

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;

import java.util.concurrent.ConcurrentHashMap;

/** Extend this class and use it as an SQLiteOpenHelper class
 *
 * DO NOT distribute, sell, or present this code as your own. 
 * for any distributing/selling, or whatever, see the info at the link below
 *
 * Distribution, attribution, legal stuff,
 * See https://github.com/JakarCo/databasemanager
 * 
 * If you ever need help with this code, contact me at [email protected] (or [email protected] )
 * 
 * Do not sell this. but use it as much as you want. There are no implied or express warranties with this code. 
 *
 * This is a simple database manager class which makes threading/synchronization super easy.
 *
 * Extend this class and use it like an SQLiteOpenHelper, but use it as follows:
 *  Instantiate this class once in each thread that uses the database. 
 *  Make sure to call {@link #close()} on every opened instance of this class
 *  If it is closed, then call {@link #open()} before using again.
 * 
 * Call {@link #getDb()} to get an instance of the underlying SQLiteDatabse class (which is synchronized)
 *
 * I also implement this system (well, it's very similar) in my <a href="http://androidslitelibrary.com">Android SQLite Libray</a> at http://androidslitelibrary.com
 * 
 *
 */
abstract public class DatabaseManager {

    /**See SQLiteOpenHelper documentation
    */
    abstract public void onCreate(SQLiteDatabase db);
    /**See SQLiteOpenHelper documentation
     */
    abstract public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
    /**Optional.
     * *
     */
    public void onOpen(SQLiteDatabase db){}
    /**Optional.
     * 
     */
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
    /**Optional
     * 
     */
    public void onConfigure(SQLiteDatabase db){}



    /** The SQLiteOpenHelper class is not actually used by your application.
     *
     */
    static private class DBSQLiteOpenHelper extends SQLiteOpenHelper {

        DatabaseManager databaseManager;
        private AtomicInteger counter = new AtomicInteger(0);

        public DBSQLiteOpenHelper(Context context, String name, int version, DatabaseManager databaseManager) {
            super(context, name, null, version);
            this.databaseManager = databaseManager;
        }

        public void addConnection(){
            counter.incrementAndGet();
        }
        public void removeConnection(){
            counter.decrementAndGet();
        }
        public int getCounter() {
            return counter.get();
        }
        @Override
        public void onCreate(SQLiteDatabase db) {
            databaseManager.onCreate(db);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            databaseManager.onUpgrade(db, oldVersion, newVersion);
        }

        @Override
        public void onOpen(SQLiteDatabase db) {
            databaseManager.onOpen(db);
        }

        @Override
        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            databaseManager.onDowngrade(db, oldVersion, newVersion);
        }

        @Override
        public void onConfigure(SQLiteDatabase db) {
            databaseManager.onConfigure(db);
        }
    }

    private static final ConcurrentHashMap<String,DBSQLiteOpenHelper> dbMap = new ConcurrentHashMap<String, DBSQLiteOpenHelper>();

    private static final Object lockObject = new Object();


    private DBSQLiteOpenHelper sqLiteOpenHelper;
    private SQLiteDatabase db;
    private Context context;

    /** Instantiate a new DB Helper. 
     * <br> SQLiteOpenHelpers are statically cached so they (and their internally cached SQLiteDatabases) will be reused for concurrency
     *
     * @param context Any {@link android.content.Context} belonging to your package.
     * @param name The database name. This may be anything you like. Adding a file extension is not required and any file extension you would like to use is fine.
     * @param version the database version.
     */
    public DatabaseManager(Context context, String name, int version) {
        String dbPath = context.getApplicationContext().getDatabasePath(name).getAbsolutePath();
        synchronized (lockObject) {
            sqLiteOpenHelper = dbMap.get(dbPath);
            if (sqLiteOpenHelper==null) {
                sqLiteOpenHelper = new DBSQLiteOpenHelper(context, name, version, this);
                dbMap.put(dbPath,sqLiteOpenHelper);
            }
            //SQLiteOpenHelper class caches the SQLiteDatabase, so this will be the same SQLiteDatabase object every time
            db = sqLiteOpenHelper.getWritableDatabase();
        }
        this.context = context.getApplicationContext();
    }
    /**Get the writable SQLiteDatabase
     */
    public SQLiteDatabase getDb(){
        return db;
    }

    /** Check if the underlying SQLiteDatabase is open
     *
     * @return whether the DB is open or not
     */
    public boolean isOpen(){
        return (db!=null&&db.isOpen());
    }


    /** Lowers the DB counter by 1 for any {@link DatabaseManager}s referencing the same DB on disk
     *  <br />If the new counter is 0, then the database will be closed.
     *  <br /><br />This needs to be called before application exit.
     * <br />If the counter is 0, then the underlying SQLiteDatabase is <b>null</b> until another DatabaseManager is instantiated or you call {@link #open()}
     *
     * @return true if the underlying {@link android.database.sqlite.SQLiteDatabase} is closed (counter is 0), and false otherwise (counter > 0)
     */
    public boolean close(){
        sqLiteOpenHelper.removeConnection();
        if (sqLiteOpenHelper.getCounter()==0){
            synchronized (lockObject){
                if (db.inTransaction())db.endTransaction();
                if (db.isOpen())db.close();
                db = null;
            }
            return true;
        }
        return false;
    }
    /** Increments the internal db counter by one and opens the db if needed
    *
    */
    public void open(){
        sqLiteOpenHelper.addConnection();
        if (db==null||!db.isOpen()){
                synchronized (lockObject){
                    db = sqLiteOpenHelper.getWritableDatabase();
                }
        } 
    }
}
Schilf
quelle
1
Was passiert, wenn Sie "close" aufrufen und dann versuchen, die Klasse wiederzuverwenden? wird es abstürzen? oder wird es sich automatisch neu initialisieren, um die DB wieder verwenden zu können?
Android-Entwickler
1
@androiddeveloper, Wenn Sie aufrufen close, müssen Sie openerneut aufrufen , bevor Sie dieselbe Instanz der Klasse verwenden, ODER Sie können eine neue Instanz erstellen. Da im closeCode, den ich festgelegt habe db=null, Sie den Rückgabewert von nicht verwenden können getDb(da er null wäre), würden Sie einen erhalten, NullPointerExceptionwenn Sie so etwas tun würdenmyInstance.close(); myInstance.getDb().query(...);
Reed
Warum nicht kombinieren getDb()und open()zu einer einzigen Methode?
Alex Burdusel
@Burdu, Eine Mischung aus zusätzlicher Kontrolle über den Datenbankzähler und schlechtem Design. Es ist jedoch definitiv nicht der beste Weg, dies zu tun. Ich werde es in ein paar Tagen aktualisieren.
Reed
@ Burdu, ich habe es gerade aktualisiert. Den neuen Code erhalten Sie hier . Ich habe es nicht getestet. Bitte lassen Sie mich wissen, ob ich die Änderungen übernehmen soll.
Reed
11

Die Datenbank ist sehr flexibel mit Multithreading. Meine Apps haben ihre DBs von vielen verschiedenen Threads gleichzeitig aufgerufen, und das funktioniert einwandfrei. In einigen Fällen treffen mehrere Prozesse gleichzeitig auf die Datenbank, und das funktioniert auch einwandfrei.

Ihre asynchronen Aufgaben - verwenden Sie dieselbe Verbindung, wenn Sie können, aber wenn Sie müssen, ist es in Ordnung, von verschiedenen Aufgaben aus auf die Datenbank zuzugreifen.

Brad Hein
quelle
Haben Sie auch Leser und Autoren in unterschiedlichen Verbindungen oder sollten sie eine einzige Verbindung teilen? Vielen Dank.
Grau
@Gray - richtig, das hätte ich ausdrücklich erwähnen sollen. In Bezug auf Verbindungen würde ich dieselbe Verbindung so oft wie möglich verwenden. Da die Sperrung jedoch auf Dateisystemebene erfolgt, können Sie sie im Code mehrmals öffnen, aber ich würde so oft wie möglich eine einzelne Verbindung verwenden. Die Android SQLite DB ist sehr flexibel und verzeihend.
Brad Hein
3
@Gray, wollte nur aktualisierte Informationen für Personen veröffentlichen, die diese Methode möglicherweise verwenden. Die Dokumentation sagt: Diese Methode macht jetzt nichts. Verwende nicht.
Pijusn
3
Ich habe festgestellt, dass diese Methode schrecklich fehlschlägt. Wir wechseln zu ContentProvider, um von mehreren Anwendungen aus darauf zuzugreifen. Wir müssen einige Parallelen zu unseren Methoden herstellen, aber das sollte alle Probleme mit Prozessen beheben, die alle gleichzeitig auf die Daten zugreifen.
JPM
2
Ich weiß, das ist alt, aber es ist falsch. Es mag in Ordnung sein, von verschiedenen SQLiteDatabaseObjekten auf verschiedenen AsyncTaskS / ThreadS auf DB zuzugreifen , aber es führt manchmal zu Fehlern, weshalb SQLiteDatabase (Zeile 1297) Locks
Reed
7

Die Antwort von Dmytro funktioniert gut für meinen Fall. Ich denke, es ist besser, die Funktion als synchronisiert zu deklarieren. Zumindest für meinen Fall würde es sonst eine Nullzeigerausnahme aufrufen, z. B. getWritableDatabase, die noch nicht in einem Thread zurückgegeben wurde, und openDatabse, das in der Zwischenzeit in einem anderen Thread aufgerufen wurde.

public synchronized SQLiteDatabase openDatabase() {
    if(mOpenCounter.incrementAndGet() == 1) {
        // Opening new database
        mDatabase = mDatabaseHelper.getWritableDatabase();
    }
    return mDatabase;
}
Gonglong
quelle
mDatabaseHelper.getWritableDatabase (); Dies wird kein neues Datenbankobjekt erstellen
außerhalb des
5

Nachdem ich einige Stunden damit zu kämpfen hatte, stellte ich fest, dass Sie nur ein DB-Hilfsobjekt pro DB-Ausführung verwenden können. Zum Beispiel,

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

wie angefügt an:

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

Das Erstellen eines neuen DBA-Kapitels bei jeder Iteration der Schleife war die einzige Möglichkeit, meine Zeichenfolgen über meine Hilfsklasse in eine Datenbank zu übertragen.

dell116
quelle
4

Nach meinem Verständnis der SQLiteDatabase-APIs können Sie es sich bei einer Multithread-Anwendung nicht leisten, mehr als ein SQLiteDatabase-Objekt zu haben, das auf eine einzelne Datenbank verweist.

Das Objekt kann definitiv erstellt werden, aber die Einfügungen / Aktualisierungen schlagen fehl, wenn (auch) verschiedene Threads / Prozesse unterschiedliche SQLiteDatabase-Objekte verwenden (wie wir es in JDBC Connection verwenden).

Die einzige Lösung besteht darin, bei 1 SQLiteDatabase-Objekten zu bleiben. Wenn eine startTransaction () in mehr als einem Thread verwendet wird, verwaltet Android die Sperrung zwischen verschiedenen Threads und lässt jeweils nur 1 Thread exklusiven Update-Zugriff zu.

Sie können auch "Lesevorgänge" aus der Datenbank ausführen und dasselbe SQLiteDatabase-Objekt in einem anderen Thread verwenden (während ein anderer Thread schreibt), und es würde niemals zu einer Beschädigung der Datenbank kommen, dh "Thread lesen" würde die Daten aus der Datenbank erst lesen, wenn " write thread "schreibt die Daten fest, obwohl beide dasselbe SQLiteDatabase-Objekt verwenden.

Dies unterscheidet sich von dem Verbindungsobjekt in JDBC. Wenn Sie das Verbindungsobjekt zwischen Lese- und Schreibthreads übergeben (dasselbe verwenden), werden wahrscheinlich auch nicht festgeschriebene Daten gedruckt.

In meiner Unternehmensanwendung versuche ich, bedingte Überprüfungen zu verwenden, damit der UI-Thread nie warten muss, während der BG-Thread das SQLiteDatabase-Objekt (ausschließlich) enthält. Ich versuche, UI-Aktionen vorherzusagen und zu verhindern, dass der BG-Thread für 'x' Sekunden ausgeführt wird. Außerdem kann PriorityQueue verwaltet werden, um die Ausgabe von SQLiteDatabase Connection-Objekten so zu verwalten, dass der UI-Thread sie zuerst erhält.

Swaroop
quelle
Und was setzen Sie in diese PriorityQueue - Listener (die Datenbankobjekte erhalten möchten) oder SQL-Abfragen?
Pijusn
Ich habe nicht den Priority Queue-Ansatz verwendet, sondern im Wesentlichen die "Caller" -Threads.
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. Dies ist nicht immer der Fall, wenn Sie "Thread lesen" direkt nach "Thread schreiben" starten, erhalten Sie möglicherweise nicht die neu aktualisierten Daten (eingefügt oder im Schreibthread aktualisiert). Der Lesethread liest möglicherweise die Daten vor dem Start des Schreibthreads. Dies liegt daran, dass der Schreibvorgang zunächst die reservierte Sperre anstelle der exklusiven Sperre aktiviert.
Amit Vikram Singh
4

Sie können versuchen, einen neuen Architekturansatz anzuwenden, der bei Google I / O 2017 angekündigt wurde .

Es enthält auch eine neue ORM-Bibliothek namens Room

Es enthält drei Hauptkomponenten: @Entity, @Dao und @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();
}
Zimbo Rodger
quelle
Ich empfehle keinen Raum für eine Datenbank mit mehreren N-zu-N-Beziehungen, da dieser Ansatz nicht gut funktioniert und Sie viel Code schreiben müssen, um eine Problemumgehung für diese Beziehungen zu finden.
AlexPad
3

Nachdem ich einige Probleme hatte, denke ich, dass ich verstanden habe, warum ich falsch gelaufen bin.

Ich hatte eine Datenbank-Wrapper-Klasse geschrieben, die eine enthielt, close()die den Helfer close als Spiegel von open()getWriteableDatabase nannte, und war dann zu einer migriert ContentProvider. Das Modell für ContentProviderwird nicht verwendet, SQLiteDatabase.close()was meiner Meinung nach ein großer Hinweis ist, da der Code verwendet wirdgetWriteableDatabase In einigen Fällen habe ich immer noch direkten Zugriff ausgeführt (Abfragen zur Bildschirmüberprüfung im Wesentlichen, sodass ich auf ein getWriteableDatabase / rawQuery-Modell migriert bin.

Ich benutze einen Singleton und es gibt den etwas bedrohlichen Kommentar in der Abschlussdokumentation

Schließen Sie alle geöffneten Datenbankobjekte

(meine Kühnheit).

Ich hatte also zeitweise Abstürze, bei denen ich Hintergrundthreads für den Zugriff auf die Datenbank verwendete und diese gleichzeitig mit dem Vordergrund ausgeführt wurden.

Ich denke also close(), dass die Datenbank unabhängig von anderen Threads, die Referenzen enthalten, geschlossen werden muss. Daher close()wird das Abgleichen nicht einfach rückgängig gemacht, getWriteableDatabasesondern das Schließen erzwungen offenen Anfragen. Meistens ist dies kein Problem, da es sich bei dem Code um Single-Threading handelt. In Fällen mit mehreren Threads besteht jedoch immer die Möglichkeit, dass die Synchronisierung geöffnet und geschlossen wird.

Nachdem Sie an anderer Stelle Kommentare gelesen haben, die erklären, dass die SqLiteDatabaseHelper-Codeinstanz zählt, möchten Sie nur dann schließen, wenn Sie eine Sicherungskopie erstellen möchten, und Sie möchten das Schließen aller Verbindungen erzwingen und SqLite dazu zwingen Schreiben Sie alle zwischengespeicherten Inhalte weg, die möglicherweise herumlungern. Mit anderen Worten, stoppen Sie alle Aktivitäten der Anwendungsdatenbank, schließen Sie sie, falls der Helfer den Überblick verloren hat, führen Sie Aktivitäten auf Dateiebene (Sichern / Wiederherstellen) aus und beginnen Sie erneut.

Obwohl es sich nach einer guten Idee anhört, das Schließen auf kontrollierte Weise zu versuchen, behält sich Android das Recht vor, Ihre VM in den Papierkorb zu werfen, sodass durch das Schließen das Risiko verringert wird, dass zwischengespeicherte Updates nicht geschrieben werden. Dies kann jedoch nicht garantiert werden, wenn das Gerät vorhanden ist wird betont, und wenn Sie Ihre Cursor und Verweise auf Datenbanken (die keine statischen Mitglieder sein sollten) korrekt freigegeben haben, hat der Helfer die Datenbank trotzdem geschlossen.

Meiner Meinung nach lautet der Ansatz:

Verwenden Sie getWriteableDatabase, um von einem Singleton-Wrapper aus zu öffnen. (Ich habe eine abgeleitete Anwendungsklasse verwendet, um den Anwendungskontext aus einer statischen Datei bereitzustellen, um die Notwendigkeit eines Kontexts zu beheben.)

Rufen Sie niemals direkt in der Nähe an.

Speichern Sie die resultierende Datenbank niemals in einem Objekt, das keinen offensichtlichen Bereich hat, und verlassen Sie sich auf die Referenzzählung, um ein implizites close () auszulösen.

Wenn Sie die Behandlung auf Dateiebene durchführen, halten Sie alle Datenbankaktivitäten an und rufen Sie dann close auf, falls ein außer Kontrolle geratener Thread vorliegt, vorausgesetzt, Sie schreiben ordnungsgemäße Transaktionen, sodass der außer Kontrolle geratene Thread fehlschlägt und die geschlossene Datenbank zumindest über ordnungsgemäße Transaktionen verfügt als möglicherweise eine Kopie einer Teiltransaktion auf Dateiebene.

Ian Spencer
quelle
0

Ich weiß, dass die Antwort zu spät ist, aber der beste Weg, um SQLite-Abfragen in Android auszuführen, ist über einen benutzerdefinierten Inhaltsanbieter. Auf diese Weise wird die Benutzeroberfläche mit der Datenbankklasse (der Klasse, die die SQLiteOpenHelper-Klasse erweitert) entkoppelt. Auch die Abfragen werden in einem Hintergrundthread (Cursor Loader) ausgeführt.

Das Ö
quelle