Android-Programmierung SQL
Willemers Informatik-Ecke
<< Android-Hauptseite

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:

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:
  1. In der build.gradle des Projekts sollte sichergestellt sein, dass das Google-Repository verwendet wird.
    allprojects {
      repositories {
        jcenter()
        google()
      }
    }
    
  2. 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(List 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);
}
Für die Datenbankoperationen gibt es wiederum Annotationen.

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));
List personen = personDB.personDAO().selectAll();
Für das Auslesen wird die Annotation @Query verwendet:
@Query("SELECT * FROM person")
public LiveData<List<Person>> observePersonen()