- SQLiteOpenHelper
- Einfügen in die Datenbank
- Transaktionen
- Auslesen der Datenbank
- Datum und Uhrzeit
- Objekt Relation Mapping mit der Room Persistence Library
Android verwendet die Datenbank SQLite als Bibliothek zur Speicherung strukturierter Daten. Jede App verwaltet dabei ihre eigene Datenbank in einer lokalen Datei, die von außen nicht erreichbar ist.
Darüber hinaus bietet die Room Persistanz Library ein Objekt Relation Mapping. Damit ist es möglich, die Tabellen durch Klassen zu modellieren und so einen aus OOP-Sicht angenehmeren Umgang mit der Datenbank zu realisieren.
Unter Android sollten unstrukturierte Daten wie Bilder oder Audios separat gespeichert werden und nicht etwa als BLOBs in der Datenbank.
Eine Tabelle sollte unter Android immer eine autoinkrementierende Spalte als Index einzurichten. Typischerweise wird diese _ID genannt. Sie wird effektiv benötigt, wenn die Daten über einen Content-Provider verbreitet werden sollen.
SQLiteOpenHelper
Die abstrakte Klasse SQLiteOpenHelper veröffentlicht gegenüber der App nur die Schnittstelle für die Datenhaltung. Um eine eigene Datenbank zu pflegen, erweitern Sie diese Klasse.Die Methoden onCreate und onUpgrade sind abstrakt und müssen implementiert werden. Außerdem hat SQLiteOpenHelper nur Konstruktoren mit Parametern. Darum muss mindestens ein Konstruktor geschaffen werden, der per super einen Konstruktor von SQLiteOpenHelper aufruft.
import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; public class GewichtDB extends SQLiteOpenHelper { public GewichtDB(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { super(context, name, factory, version); } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { } }
Konstruktor
Der Konstruktor erhält den Context als Parameter, den er an den Basiskonstruktor weitergibt. Die anderen Parameter stellt man sinnvollerweise in der eigenen Klasse zur Verfügung, da sie außerhalb nicht benötigt werden.static final int dbversion = 1; public HighscoreSQLHelper(Context context) { super(context, "gewicht.db", null, dbversion); }
onCreate
Die Methode onCreate erhält als Parameter die SQLiteDatabase db. Hier kann die Datenbanktabelle durch den Aufruf der Methode db.execSQL erzeugt werden. Der Parameter ist der Befehl zur Erzeugung der Tabellen.@Override public void onCreate(SQLiteDatabase db) { final String CREATEDB = "create table "+TABELLE+ " (" + _ID+" integer primary key," + DATUM+" integer," + GEWICHT+" decimal);"; db.execSQL(CREATEDB); }
Zugriff
Der SQLiteOpenHelper liefert über die Aufrufe getReadableDatabase oder getWritableDatabase den Zugriff auf ein Datenbankobjekt. Der eine für lesende, der andere für schreibende Zugriffe.Einfügen in die Datenbank
Das Einfügen von Daten erfolgt über Objekte der Klasse ContentValues. Das ist eine Struktur, in der aus Spaltenname und Inhalt ein Paar gebildet wird, die mittels der Methode put gesetzt wird.Ein solches Objekt kann mit einem Rutsch durch die Methode insert in die Datenbanktabelle geschafft werden.
public long insertDatumGewicht(long tag, long monat, long jahr, double gewicht) { long zeile = -1; try { ContentValues values = new ContentValues(); values.put(DATUM, jahr*10000+monat*100+tag); values.put(GEWICHT, gewicht); SQLiteDatabase db = getWritableDatabase(); zeile = db.insert(TABELLE, null, values); } catch (SQLiteException e) { Log.e(this.getClass().getSimpleName(), "insert failed"); } return zeile; }
Transaktionen
Auf wenn SQLite sehr klein ist, unterstützt es schon Transaktionen Transaktionen werden benötigt, damit keine Inkonsistenzen entstehen, wenn zwei parallel arbeitende Prozesse auf demselben Datenbereich ändern. Ein solcher Parallelbetrieb ist eher bei einem Content-Provider erforderlich als in einer gewöhnlichen App.Um eine Aktion als Transaktion zu fahren, werden die folgenden Methoden für den SQLiteOpenHelper aufgerufen:
- db.beginTransaction();
Hier beginnt die Transaktion. Der aktuelle Zustand wird wieder hergestellt, wenn etwas schiefläuft. - db.setTransactionSuccessful();
Die Operation hat geklappt. Die Aktionen werden auf Dauer in der Datenbank gespeichert. - db.endTransaction();
Hier ist das Ende einer Transaktion. Typischerweise erfolgt dieser Aufruf im finaly-Zweig der SQLiteException.
Auslesen der Datenbank
Das Auslesen der Datenbank erfolgt über einen Cursor. Der Cursor ist das Ergebnis einer Query-Anfrage. Auch Querys können in der OpenHelper-Klasse vordefiniert werden. Immerhin sind immer nur bestimmte Anfragen an die Datenbank erforderlich.public Cursor query() { SQLiteDatabase db = getWritableDatabase(); return db.query(TABELLE, // Die Tabelle null, // Die Spalten, die ausgewählt werden, als String-Array null, // Die WHERE-Klausel null, // Wenn ? in der WHERE-Klausel stehen, werden sie hier aufgelöst null, // Die GROUP BY-Klausel null, // Die HAVING-Klausel DATUM+" DESC"); // Die ORDER-BY-Klausel }Der Cursor durchläuft Zeile für Zeile die Ergebnismenge des Select-Befehls. Aus dem Cursor können verschiedene Informationen gewonnen werden. Einerseits kann die Nummer einer Spalte über den Spaltennamen ermittelt werden. Andererseits kann beim Durchlaufen für jede Zeile der Wert des Feldes ausgelesen werden. Das folgende Listing liest aus einer Datenbanktabelle Gewichte und ermittelt den Durchschnitt und stellt diesen in einem TextView dar.
try { GewichtDB db = new GewichtDB(this); // SQLiteDatabase sdb = db.getWritableDatabase(); Cursor cursor = db.query(); int spalteGewicht = cursor.getColumnIndex(GewichtDB.GEWICHT); double summeGewicht = 0.0; int anzahl = 0; while (cursor.moveToNext()) { double gewicht = cursor.getDouble(spalteGewicht); summeGewicht += gewicht; anzahl++; } if (anzahl > 0) { tvDurchschnitt.setText("" + summeGewicht / anzahl); } else { tvDurchschnitt.setText("nix"); } cursor.close(); } catch (SQLiteException e) { }
Datum und Uhrzeit
Als Textfeld
SQLite besitzt kein Datums- und Uhrzeitfeld. Als Ausweg kann man das Datum als Text speichern. Dazu empfielt sich das Format JJJJMMTTHHMMSS. Auf diese Weise kann man die Daten sortieren.Besser ist das Format nach ISO860: YYYY-MM-DD HH:MM:SS.SSS. Das hat den Charme, dass man die DATETIME-Funktion von SQLite verwenden kann.
Als Zahl
Alternativ kann man Datum und Uhrzeit als Integer speichern. Das Standard-UNIX-Format zählt die Sekunden seit dem 1.1.1970. SQLite bietet eine Standardfunktion datetime_int zur Konvertierung an.
Objekt Relation Mapping mit der Room Persistence Library
Für die Synchronisation zwischen Tabellenspalten und Klassenattributen werden Annotationen angeboten. Für Abfragen stehen Query-Annotationen zur Verfügung, die dafür sorgen, dass Fehler bereits vom Compiler und nicht erst zur Laufzeit entdeckt werden.Einbindung ins Gradle
Einbinden in die Datei build.gradle:- In der build.gradle des Projekts sollte sichergestellt sein, dass
das Google-Repository verwendet wird.
allprojects { repositories { jcenter() google() } }
- In der build.gradle des Modul:app wird die Abhängigkeiten der
Room Persistance Library unter dependencies eintragen:
dependencies { ... implementation "android.arch.lifecycle:extensions:1.0.0" implementation "android.arch.persistence.room:runtime:1.0.0" annotationProcessor "android.arch.persistence.room:compiler:1.0.0" testImplementation "android.arch.persistence.room:testing:1.0.0" ... }
Wegen der Änderung der build.gradle mahnt Gradle einen Sync-Lauf an.
Die Entity
Jede Tabelle wird innerhalb des Programms durch eine Klasse repräsentiert, die mit der Annotation @Entity markiert wird:@Entity public class Person { @NonNull @PrimaryKey public String id; public String name; public int gebjahr; public boolean geschlecht; }
Alle public-Attribute entsprechen der Spaltendefinition. Attribute, die nicht in die Tabelle laufen sollen, werden mit @Ignore gekennzeichnet.
Statt public-Attribute können auch private-Attribute verwendet werden, sofern Getter und Setter vorhanden sind.
Data Access Object
Eine weitere Klasse definiert die Aktionen, die auf dem @Entity möglich sind. Diese Klasse ist die Basis für das Data Access Object (DAO) und wird durch die Annotation @Dao gekennzeichnet. Diese Klasse definiert die Speicher- und Abfragemethoden der Datenbank. Typischerweise existiert eine DAO-Klasse für jede Tabelle.@Dao public interface PersonDAO { @Insert public void insertPerson(Person person); @Insert(onConflict = OnConflictStrategy.REPLACE) public void insertPerson(ListFür die Datenbankoperationen gibt es wiederum Annotationen.personen); @Update public void updatePerson(Person person); @Delete public void deletePersonen(Person p1, Person p2); @Query("DELETE FROM person") public void deleteAll(); @Query("SELECT * FROM person") public List selectAll(); @Query("SELECT * FROM hoard WHERE name = :name") public Person selectByName(String name); }
Beim Einfügen (@Insert) entstehen Konflikte, wenn ein Eintrag mit gleichem Schlüsselwert bereits in der Tabelle angelegt wurde. Die zweite @Insert-Annotation gibt an, dass diese in solch einem Fall ersetzt werden soll.
Die Datenbank
Die Annotation @Database leitet eine abstrakte Klasse ein, die RoomDatabase erweitert. Sie muss eine abstrakte Methode enthalten, die das Data Access Object und die Liste der enthaltenen Entities zurückgibt.@Database(entities = {Person.class}, version = 1) public abstract class PersonDatabase extends RoomDatabase{ public abstract PersonDAO personDAO(); }Ein Objekt wird in die Tabelle auf die folgende Weise eingeführt:
PersonDatabase personDB = Room.databaseBuilder( getApplicationContext(), PersonDatabase.class, "persondb").build(); personDB.personDAO().insertPerson(new Person("Anton", 1960, true)); ListFür das Auslesen wird die Annotation @Query verwendet:personen = personDB.personDAO().selectAll();
@Query("SELECT * FROM person") public LiveData<List<Person>> observePersonen()