SQLite mit Room-Annotations als Persistenz-Schicht Schritt für Schritt erklärt

Room ist ein, von der Forma Google entwickeltes Framework, dass auf der SQLite Datenbank aufsetzt, die in jedem Android Handy zur Verfügung steht. SQLite ist eine Relationale Datenbank, in der die Daten in Tabellen gespeichert werden und die Abfragesprache ein SQL Dialekt ist. Dank Room muss man sich nun (fast) gar nicht mehr mit den spezifischen Eigenschaften der SQLite Datenbank und der SQL Abfragesprache zu beschäftigen. Stattdessen programmiert man weiter in Java seine Entitäten, DataAccessObjects und ein paar andere Klassen und schon kann man Daten sehr einfach, schnell und sicher persistent speichern. Die folgende „Schritt für Schritt Anleitung“ erklärt wie man eine Datenbankschicht mit dem Room Framework realisiert. Alle Schritte sind ausführlich erklärt und die Snippets und können in jedes Android Studio Projekt übernommen werden.

Schritt 1:  Room zum Projekt hinzufügen

Um Room zu verwenden, muss man die entsprechenden Bibliotheken in das Projekt integrieren. Folgende 2 Abhängigkeiten müssen hier im build.gradle (app) File eingetragen werden. Sinnvoll ist es, sich vorher unter https://mvnrepository.com/artifact/android.arch.persistence.room/ zu informieren, welche Version aktuell verwendet werden soll. In meinem Fall ist es Version 1.1.1. Also ergänze ich folgende Zeilen im „dependencies“ Abschnitt der build.gradle Datei.

dependencies {
    ….
    implementation 'android.arch.persistence.room:runtime:$room_version'
    annotationProcessor 'android.arch.persistence.room:compiler:$room_version'
    ….
}

Nach man auf „Sync Now“ in Android Studio geklickt hat, sind die Dateien aus de, Repository geladen worden und es kann los gehen.

Hinweis: Room ist eines der Module, die im Zusammenhang mit den „Android Architeture Components“ nach androidx verschoben werden. Daher gibt es auch eine alternative Variante unter https://mvnrepository.com/artifact/androidx.room. Entscheidet man sich dafür, die Android Architeture Components zu verwenden, müssen folgende Zeilen in die build.gradle Datei eingetragen werden und die import Zeilen in den Java Klassen müssen entsprechend geändert werden (In diesem Tutorial wird nicht explizit mit der Android Architeture Components Variante von Room gearbeitet).

  implementation "androidx.room:room-runtime:$room_version"
  annotationProcessor "androidx.room:room-compiler:$room_version"

Ausserdem gibt es auch noch 4 weitere gradle Dependencies, die optional genutz werden können, auf die ich in diesem Artikel auch nicht weiter eingehen werde, da es hier erst mal nur um eine Einführung geht. Bei den 4 Modulen handelt es sich um:

  // optional - Kotlin Extensions und Coroutines Unterstützung für Room
  implementation "androidx.room:room-ktx:$room_version"

  // optional - RxJava Support für Room
  implementation "androidx.room:room-rxjava2:$room_version"

  // optional - Guava Support für Room
  implementation "androidx.room:room-guava:$room_version"

  // Test Hilfsklassen
  testImplementation "androidx.room:room-testing:$room_version"

Schritt 2: Entitäten erstellen

Die Entitäten sind die Klassen, die den Tabellen der Datenbank entsprechen. Es handelt sich hier um einfache Java Klassen, die über Variablen verfügen. Das Besondere daran ist, dass die Klasse mit der Annotation „@Entity“ versehen wird und die Variable, die der primäre Schlüssel der Tabelle werden soll mit der Annotation „@PrimaryKey“ versehen wird. Jede Entität muss mindestens einen primären Schlüssel besitzen, außer die Entität erbt einen primären Schlüssel von einer Superklasse. In dem Fall kann man den primären Schlüssel mit dem Annotation überschreiben, muss es aber nicht.

Eine einfach Entity-Klasse könnte z.B. folgendermaßen aussehen

@Entity
 public class Photo
 {
     @PrimaryKey
     private int id;
     private String name;
     private String filename;
 }

Beim Anlegen der Datenbank wird bekommt jede Entity Klasse eine eigene Tabelle und für jedes Attribut der Tabelle wird eine Spalte in der Tabelle angelegt. Der Name der Klasse entspricht dabei dem Tabellenname und die Namen der Attribute entsprechen den Spaltennamen. Optional kann man die Spaltennamen mit der Annotation @ColumnInfo(name = “SPALTENNAME “) anpassen.

Schritt 3: Data Access Objekte erstellen

Die sogenannten DAOs sind die Objekte, die den Zugriff auf die Daten ermöglichen. Sie enthalten typischerweise die CUD (Create, Update und Delete) Methoden und können noch weitere Methoden enthalten, die für den lesenden und schreibenden Zugriff auf die Datenbank nötig sein können.

Bei Room werden DAOs mit der „@Dao“ (ndroidx.room.Dao) Annotation gekennzeichnet. Zusätzlich muss man die enthaltenen Methoden mit den entsprechenden Annotationen versehen. Für DAOs sind die Annotationen vorgesehen.

import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;

Wie die Namen schon erahnen lassen handelt es sich hierbei um die die für SQL Datenbanken typischen Methoden. Ein typisches DAO-Interface könnte z.B. wie folgt aussehen.

@Dao
 public interface PhotoDao {
     @Query("SELECT * FROM photo")
     List<Photo> loadAll();
 
     @Query("SELECT * FROM photo WHERE id IN (:photoIds)")
     List<Photo> loadAllByPhotoId(int... photoIds);
 
     @Query("SELECT * FROM photo where name LIKE :name AND filename LIKE :filename LIMIT 1")
     Photo loadByNameAndFilename(String name, String filename);
 
     @Insert
     void insertAll(Photo... photos);
 
     @Update
     void updateAll(Photo... photos);
 
     @Delete
     void delete(Photo photo);
 }

Wie man am Beispiel sieht, ist es nicht nötig, die Insert, Update und Delete Methoden zu implementieren. Es ist ausreichend, die entsprechende Annotation anzugeben und Room kümmert sich um die Implementierung. Einzig bei den Methoden mit der Query Annotation muss das Select Statement mit angegeben werden.

In diesem Beispiel gibt es keine komplexen Abfragen, aber es sind auch Abfragen über mehrere Tabellen mit Inner-Joins, Left-Joins, und allem anderen was eine SQLite Datenbank unterstützt, möglich.

Das wirklich erstaunliche an der Query-Annotation ist, dass die SQL Statements auf ihre Gültigkeit hin überprüft werden. So erhält man Fehlermeldungen, wenn man Tabellen oder Spalten verwendet, die so nicht existieren oder andere Fehler im Select Statement entdeckt werden.

Das so definierte Room-DAO Interface kann nun dazu verwendet werden, Daten aus der Datenbank zu lesen oder in die Datenbank zu schreiben.

Optionaler Schritt 3b: Datenbank Views

Neben den DAO-Interfaces kennt Room noch eine weitere Annotation mit dem Namen @DatabaseView. Mit dieser Annotation kann man eine Klasse markieren, die dann wie ein Datenbank-View verwendet werden kann. Wie alle anderen DAO-Annotationen befindet sich der DatabaseView auch in dem Package androidx.room und ist ab der Room-Version 2.1.0 verfügbar. Ein solcher Datenbank View könnte dann z.B. wie folgt aussehen.

@DatabaseView("SELECT user.id, user.name, user.departmentId," +
         "department.name AS departmentName FROM user " +
         "INNER JOIN department ON user.departmentId = department.id")
 public class UserDetail {
     public long id;
     public String name;
     public long departmentId;
     public String departmentName;
 }

Dieses Beispiel habe ich der offiziellen Android Entwickler Webseite unter https://developer.android.com/reference/androidx/room/Database#views entnommen. Ich selbst habe keinerlei Erfahrung mit dieser Annotation und bin bisher immer sehr gut ohne ausgekommen.

Hinweis: Genau wie in einer SQL-Datenbank könne auch hier keine INSERT, UPDATE, oder DELETE Statements ausgeführt werden. Views lassen, wie der Name schon vermuten lässt, ausschließlich das Lesen von Daten zu.

Schritt 4: Datenbank verbinden

Um die Klassen und Interfaces verwenden können fehlt noch eine Klasse, die die Repräsentation der Datenbank enthält. Diese abstrakte Klasse enthält die abstrakten Methoden, mit denen man die Objekte der DAO-Interfaces erzeugen kann. Auch hier reicht es schon, die abstrakten Methoden-Definitionen anzugeben. Die Implementierung übernimmt der Annotation-Prozessor.

@Database(entities = {Photo.class}, version = 1)
 public abstract class AppDatabase extends RoomDatabase {
     public abstract PhotoDao userDao();
 }

In der „Database“-Annotation kann man 3 unterschiedliche Eigenschaften festlegen.

  1. Die Entitäten (Tabellen) werden in der „entities“-Liste durch Komma getrennt angegeben
  2. Die Version der Datenbank. Hier Version 1. Diese Version wird später noch wichtig, wenn man verschiedene Versionen migrieren möchte.
  3. Optional können die Views in der „views“ Eigenschaft der Annotation angegeben werden. Auch diese werden wie die Entitäten durch Komma getrennt angegeben.

Nun kann man von der AppDatabase Klasse eine Instanz erstellen und über die DAO-Methoden die Entitäten in die Datenbank schreiben oder aus der Datenbank lesen.

Nun kann man die Datenbank in der App benutzen. Da alle Daten auf dem lokalen Gerät innerhalb der App gespeichert werden ist noch nicht mal eine besondere Berechtigung nötig. Weder <uses-permission android:name=”android.permission.INTERNET”/> noch  <uses-permission android:name=”android.permission.READ_EXTERNAL_STORAGE”/> oder <uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE”/> werden benötigt (Außer man gibt der Room Datenbank einen Speicherort, der sich nicht innerhalb des App-Data Verzeichnisses befindet).

Schritt 5: Das Singleton (Irgendwie auch Optional) 🙂

Wenn ich eine Android App mit einer Room Datenbank erstelle, achte ich immer darauf, dass es nur eine Instanz der AppDatabase Klasse gibt. Um dies zu gewährleisten, erstelle ich zusätzlich noch eine Singleton Klasse, die dafür sorgt, dass es nur eine Instanz der AppDatabase Klasse gibt.

Diese Klasse hat einen privaten Konstruktor und kann daher nur von sich selbst instanziiert werden. Dies erfolgt mit Hilfe der statischen Methode instance(). Diese Methode prüft, ob es schon eine Instanz der AppDatabase Klasse gibt. Wenn nicht, wird einfach eine Instanz erstellt. Anderenfalls wird die existierende Instanz an die aufrufende Methode zurückgegeben,

public class DbSingleton {
 
     private static DbSingleton instance = null;
     private static final String DB_NAME = "database";
 
     private AppDatabase db;
 
     private DbSingleton (Context context) {
         db = Room.databaseBuilder(context.getApplicationContext(),
                                   AppDatabase.class,
                                   DB_NAME)
                 .allowMainThreadQueries()
                 .build();
     }
 
     public AppDatabase getAppDatabase()
     {
         return db;
     }
 
    public static synchronized DbSingleton instance(Context context) {
        if (instance == null) {
            instance = new DbSingleton(context);
        }
        return instance;
    }
 }

Datenbank verwenden

Damit sind alle Schritte getan, um in einer App eine Datenbank hinzuzufügen. Eine Instanz der Klasse bekommt man über den Aufruf:

appDatabase = DbSingleton.instance(context).getAppDatabase();

Über das appDatabase Objekt kann man die DAO-Objekte erreichen und so z.B. Entitäten in der Datenbank speichern.

Photo photo = new Photo(name, filename, System.currentTimeMillis());
 appDatabase.photoDao().insertAll(photo);

Weitere Infos

Da ich hier in der kurzen Einführung in Room nicht alle Details beschreiben möchte, hier noch mal ein Link auf die offizielle Dokumentation unter https://developer.android.com/training/data-storage/room. Hier werden noch viele Details angesprochen, die ich hier in dem Artikel unterschlagen habe.

Unter anderem:

  • enableMultiInstanceInvalidation() – Hilft bei Multi Prozess Apps mit mehreren AppDatabase Instanzen
  • Mehrere Primary Keys in einer Entität mit dem „primaryKeys“ Attribut in der „Entity“ Annotation
  • Vom Entwickler wählbare Tabellen- und Spaltennamen mit Hilfe von „tableName“ und ColumnInfo
  • Die @Ignore Annotation – Findet Verwendung, wenn Felder in einer Entität nicht persistiert werden sollen. (Kann auch mit dem Attribut „ignoredColumns“ der Entity Annotation erreicht werden – z.B. bei Vererbung sehr wichtig)
  • Support für Volltextsuche mit Hilfe der FTS3 und FTS4 SQLite Erweiterungen in Room möglich mit der „@Fts3“ oder „@Fts4“ Annotation.
  • Einsatz von Indizes zur Verbesserung der Query-Zeiten mit „@Index“.
  • Definition von Fremdschlüsseln mit „@ForeignKey“
  • Eingebettete Objekte mit „@Embedded“.
  • „m zu n“ Relationen werden mit @ForeignKey relaisiert.
  • Migration von einer Version zur nächsten mit Hilfe der Migration Klasse und entsprechenden SQL-Skripten (z.B. ALTER TABLE)
  • Und vieles mehr

Fazit

Dank Room und SQLite wird das persistente Speichern von Daten in einer App zum Kinderspiel. Ich persönlich nutze die Room Annotations sehr gerne in meinen Apps und habe noch keine Situation gefunden, in der ich an die Grenzen gestoßen bin. Selbst Volltextsuche in riesigen Datenmengen ist dank Fts3 und Fts4 ein Kinderspiel. Alles in allem hat Google hier ein sehr gutes SDK geschaffen, mit dem das App-Entwickeln noch schneller geht und man dank der SQL-Validation noch bessere Qualität liefern kann.

Auch die Dokumentation ist sehr umfangreich und leicht verständlich so dass auch ein Anfänger schnell einen Einstieg findet und die ersten Schritte mit Room ein Kinderspiel sind.