UPBReader – Persistente Datenspeicherung in Android mit SQLite3

12 Jan

Wer meine letzten Beiträge zu meiner Anwendung etwas verfolgt hat weiß, dass ich die Daten der Annotationen an einem ePub Dokument in einer Objektstruktur gespeichert habe. Da ich aber diese Daten auch benötige, wenn die Anwendung das nächste mal gestartet wird, stand eine persistente Speicherung der Daten auf dem Programm. Hier lag direkt die Verwendung einer Datenbank nahe. Ich verwende dafür SQLite3, welches von Android mitgeliefert wird. Ich möchte in diesem Beitrag etwas über meine Erfahrungen mit SQLite3 in Android mitteilen. Komplette Codeausschnitte lasse ich dieses mal weg, da das einfach zu viel würde. Dafür gibt es am Ende einen Link, in dem ein komplettes Beispiel behandelt wird und auch weiter geht, als ich es getan habe.

SQLite3 unterstützt einen Großteil der SQL Befehle, verwendet zur Laufzeit sehr wenig Speicher und benötigt keinen Server. Es verwendet Locks auf Datei Ebene, wodurch der Zugriff serialisiert durchgeführt wird. Im Normalfall bedeutet das, dass man ohne Bedenken auf der Datenbank arbeiten kann. Ich sage „im Normalfall“, da es durchaus auch zu Problemen kommen kann, obwohl eigentlich SQLite3 korrekt verwendet wird. Aber dazu später. Zunächst möchte ich ein paar wichtige Begriffe aufzeigen, die für das Einrichten einer Datenbank in Android wichtig sind.

Um eine neue Datenbank zu erzeugen verwendet man in Android die Klasse SQLiteOpenHelper. SQLiteOpenHelper hält ein paar Methoden bereit, die für die Verwaltung einer Datenbank hilfreich sind. Z.B. kann man durch getReadableDatabase() oder getWriteableDatabase() eine Verbindung zu einem SQLiteDatabase Objekt aufbauen. Die Klasse SQLiteDatabase hält die Methoden bereit, die ähnlich den SQL Befehlen sind. Hier ein paar wichtige als Beispiel:

  • insert()
  • update()
  • remove()
  • query()

Wenn man SQLiteOpenHelper erweitert, muss man die Methoden onCreate() und onUpgrade() implementieren. Die Methode onCreate() wird automatisch aufgerufen, wenn eine neue Datenbank erzeugt wird. Hier wird per execSQL() eine SQL Anweisung zum erzeugen einer Tabelle ausgeführt („CREATE TABLE Annotations(_id INTEGER PRIMARY KEY, name TEXT)„). Die Methode onUpgrade() hingegen wird aufgerufen, wenn sich die Versionsnummer der Datenbank verändert hat. Wenn ich also als Entwickler eine neue Version der Anwendung veröffentlichen möchte aber Änderungen an der Datenbank vorgenommen habe, so verändere ich die Versionsnummer der Datenbank und führe in onUpgrade() eine Migration von der früheren Datenbankversion zur neuen Version durch. Beispielsweise könnte die Migration durch entfernen einer Spalte geschehen.

An dieser Stelle muss ich erwähnen, dass SQLite3 nicht alle ALTER TABLE Funktionalitäten mit bringt. Es bietet nur Add und Rename an, nicht aber Drop oder Remove. Somit ist das entfernen einer Spalte etwas komplizierter. Drop oder Remove wird in SQLite3 durch das neu Erzeugen der Tabelle gewährleistet. Um Datenverlust zu vermeiden müssten dann die Daten zunächst zwischengespeichert werden. Eine gute Beschreibung ist im folgenden Link in der ersten Antwort zu finden.

http://stackoverflow.com/questions/3505900/sqliteopenhelper-onupgrade-confusion-android

Nun aber weiter. Um die selbst erstellte Unterklasse von SQLiteOpenHelper möglichst übersichtlich zu halten, sollte man für jede Tabelle der Datenbank eine eigene Klasse anlegen die onCreate() und onUpgrade() bereit hält. Das ist bei großen Tabelle sehr hilfreich. Somit haben wir bisher mindestens zwei Klassen, die Unterklasse von SQLiteOpenHelper („AnnotationsDatabaseHelper„) und eine Klasse zum Erzeugen der Tabellen („AnnotationsTables“). Ich habe mich nämlich gegen den Tipp, eine Tabelle eine Klasse, entschieden da ich gerade mal 4 Tabellen benötige die nicht sonderlich viele Spalten haben.

Die dritte und für die Verwendung der Datenbank zuständige ist die „AnnotationsDbAdapter“ Klasse. Sie kann z.B. wie in meinem Fall folgende Methoden halten:

  • open() – Erzeugt ein AnnotationsDatabaseHelper Objekt und öffnet mit getWritableDatabase() eine Verbindung zur Datenbank
  • close() – Wichtig. Nach fertiger Interaktion sollte die Datenbank wieder geschlossen werden.
  • createAnnotation(daten) – Hier werden die Daten zunächst in ein ContentValues Objekt gesteckt und anschließend per db.insert() als neue Zeile der Tabelle in die Datenbank eingefügt.
  • updateAnnotation(daten) – Genau wie letzteres außer dass am Schluss db.update() aufgerufen wird um eine Zeile der Tabelle zu verändert.
  • deleteAnnotation(id) – Hier wird ein db.delete(id) aufgerufen, um eine Zeile der Tabelle zu löschen.
  • fetchAllAnnotations() – Ein db.query() gibt alle Annotationen der Datenbank zurück.

Natürlich ist das nur ein grober Überblick. Es könnten weitere speziellere Methoden geschrieben werden. Z.B. wenn man alle Annotationen haben möchte, die von einem Author verfasst wurden usw.

Wenn also die drei Klassen stehen hat man alles was man für das Auslesen aus der Datenbank und das Schreiben in die Datenbank benötigt. Hier ein kurzer Ausschnitt, wie eine Interaktion mit der Datenbank nun aussehen würde.

AnnotationsDbAdapter dbHelper = new AnnotationsDbAdapter(c);
dbHelper.open();
Cursor cursor = dbHelper.fetchAllAnnotations();
...
// Hier die Daten in cursor verarbeiten oder speichern
...
cursor.close();
dbHelper.close();

Ohne die beiden close() Aufrufe gibt es schnell mal eine Exception. Der Cursor hält immer eine einzige Zeile. Das heißt auch wenn durch fetchAllAnnotations() mehrere Zeilen geholt wurden, Cursor zu einem Zeitpunkt immer nur auf eine Zeile zeigt. Für die Verschiebung des Cursors bietet er die Methoden moveToFirst() oder moveToNext(). Für Schleifen ist auch die Methode isAfterLast() sehr hilfreich. Jedenfalls war es das erstmal. Hier noch ein Link, auf dessen Seite man ein gutes Beispiel zur Erstellung einer Datenbank in Android bekommt.

http://www.vogella.de/articles/AndroidSQLite/article.html

Nun muss ich aber noch einmal auf das zurück kommen, das ich am Anfang gesagt habe. Dass man im Normalfall ohne Bedenken damit arbeiten kann. Genau genommen stimmt das, solange man nicht von verschiedenen Threads aus je ein neues SQLiteOpenHelper Objekt erzeugt und damit arbeitet. Wenn das geschieht und zwei Threads gleichzeitig in die Datenbank schreiben wollen, werden nämlich nicht beide hintereinander abgearbeitet sondern nur einer von beiden. Der andere bekommt eine Exception und läuft munter weiter. Deshalb ist dieser Fehler auch nicht leicht zu erkennen.

Die einzige wirkliche Lösung für das Problem ist das verwenden einer einzigen SQLiteOpenHelper Instanz in allen Threads. Um diese Instanz in der gesamten Anwendung zugreifbar zu machen gibt es mehrere Ansätze. Von den verschiedenen Ansätzen werden in Android am meisten die ContentProvider verwendet. Wenn man es genau nimmt, sind ContentProvider eigentlich dafür da, Daten anhand einer URI auch für andere Anwendungen auf dem lokalen System oder aus dem Internet bereit zu stellen. Ob also ein ContentProvider für interne Datenhaltung das optimale ist, weiß ich nicht. Aus diesem Grund versuche ich bisher noch ohne auszukommen.

Schreibe einen Kommentar

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: