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?
Antworten:
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:
quelle
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 .
Jetzt möchten Sie Daten in separaten Threads in die Datenbank schreiben.
In Ihrem Logcat wird die folgende Meldung angezeigt, und eine Ihrer Änderungen wird nicht geschrieben.
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 .
Der aktualisierte Code, der Daten in separaten Threads in die Datenbank schreibt, sieht folgendermaßen aus.
Dies wird Ihnen einen weiteren Absturz bringen.
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.
Arbeitsprobe
Verwenden Sie es wie folgt.
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.
quelle
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?initializeInstance()
hat einen Parameter vom TypSQLiteOpenHelper
, aber in Ihrem Kommentar haben Sie erwähnt, zu verwendenDatabaseManager.initializeInstance(getApplicationContext());
. Was ist los? Wie kann das möglicherweise funktionieren?Thread
oderAsyncTask
fü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.SQLiteDatabase
Instanz für jede Datenbank auf der Festplatte zwischen Threads und implementieren Sie ein Zählsystem, um offene Verbindungen zu verfolgen.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.
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.
DatabaseManager
. (oder von github herunterladen)DatabaseManager
und implementierenonCreate
undonUpgrade
wie Sie es normalerweise tun würden. Sie können mehrere Unterklassen einerDatabaseManager
Klasse erstellen , um unterschiedliche Datenbanken auf der Festplatte zu haben.getDb()
auf, um dieSQLiteDatabase
Klasse zu verwenden.close()
für jede von Ihnen instanziierte Unterklasse aufDer Code zum Kopieren / Einfügen :
quelle
close
, müssen Sieopen
erneut aufrufen , bevor Sie dieselbe Instanz der Klasse verwenden, ODER Sie können eine neue Instanz erstellen. Da imclose
Code, den ich festgelegt habedb=null
, Sie den Rückgabewert von nicht verwenden könnengetDb
(da er null wäre), würden Sie einen erhalten,NullPointerException
wenn Sie so etwas tun würdenmyInstance.close(); myInstance.getDb().query(...);
getDb()
undopen()
zu einer einzigen Methode?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.
quelle
SQLiteDatabase
Objekten auf verschiedenenAsyncTask
S /Thread
S auf DB zuzugreifen , aber es führt manchmal zu Fehlern, weshalb SQLiteDatabase (Zeile 1297)Lock
sDie 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.
quelle
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,
wie angefügt an:
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.
quelle
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.
quelle
"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.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
UserDao.java
AppDatabase.java
quelle
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 vonopen()
getWriteableDatabase nannte, und war dann zu einer migriertContentProvider
. Das Modell fürContentProvider
wird 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
(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. Daherclose()
wird das Abgleichen nicht einfach rückgängig gemacht,getWriteableDatabase
sondern 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.
quelle
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.
quelle