SQLite bietet sich seit jeher als Datenbanksystem für Android an. Auch wenn mittlerweile potentere Alternativen wie die dokumentenbasierten Realm oder Firebase existieren, bildet SQLite nach wie vor den Unterboden vieler Apps. Der Zugriff auf diese SQLite-Datenbanken erfolgte über ContentValues, Cursor und Raw Queries, welche aufwendig in der Einrichtung und fehleranfällig in der Wartung sind. Aus dieser Not entstanden zahlreiche Drittanbieterbibliotheken für objektrelationale Abbildungen (Object Relational Mapping, kurz ORM) greenDao, ActiveAndroid oder SugarORM und bestehende Größen wie OrmLite wurden portiert.

Auf der diesjährigen Google I/O stellte Google mit der Room Persistence Library (Room) endlich einen hauseigenen ORM für Android vor. Durch den geschickten Einsatz von Annotationen verringert Room den Boilerplate Code auf ein absolutes Minimum und nimmt uns Entwicklern einen Heidenaufwand ab, um uns der eigentlichen Geschäftslogik unserer Apps zuzuwenden. Hierbei behalten wir den vollen Funktionsumfang von SQLite, da nach wie vor Raw Queries geschrieben werden. Diese werden im Gegensatz zum Plain SQLite zur Übersetzungszeit validiert und nicht erst zur Laufzeit, wodurch Fehler vermieden und Korrekturen vereinfacht werden. Weiterhin werden Datenbankabfragen im Background Thread forciert, um ein versehentliches Blockieren der UI zu verhindern.

Grundsätzlich setzt sich die Datenbankschicht mit Room aus drei Komponenten zusammen:

  1. Datenbank
  2. Entitäten
  3. Data Access Objects (Dao)

Die Datenbank wird mit @Database annotiert und leitet als abstrakte Klasse von RoomDatabase ab. Sie konfiguriert Datenbankversion und -namen sowie unterstützte Entitäten, Type Converter und benötigte Migrationspfade. Weiterhin dient sie als Sammelstelle für verwendete DAOs.

Entitäten werden mit @Entity annotiert und bestehen in ihrer schlanksten Form aus nicht mehr als public fields. Felder, die ignoriert werden sollen, kennzeichnen wir mit @Ignore. Für nicht-unterstützte Datentypen können Type Converter geschrieben werden, welche z. B. die DateTime aus JodaTime in ein Objekt vom Typ Long oder String umwandeln.

Data Access Objetcs werden mit @Dao annotiert und dienen als Zugriffsschicht auf die Datenbank. Für Standardaktionen wie @Insert, @Update oder @Delete muss nichts weiter als die entsprechende Annotation gesetzt werden. Abfragen werden mit @Query und einem Raw Statement definiert, welchem Parameter übergeben werden können.

Zu diesen drei Komponenten gesellt sich bei Bedarf noch das ViewModel hinzu, welches bei den neuen Lifecycle Aware Components eingesetzt wird.

Aus diesen Komponenten erstellt Room zur Übersetzungszeit die entsprechenden Implementierungen, womit der produzierte Code einsehbar aber nach wie vor komplett abstrahiert bleibt. Aktuell bereiten uns noch Generics Probleme, die wir z. B. für einen BaseDao verwenden, welcher Standardmethoden wie insert(), delete() oder findById() implementiert. Room verliert die Typparameter und kann mit dem unbekannten Typ T nichts mehr anfangen. Ironischerweise tippt uns der Compiler an der Stelle auf die Schulter und schlägt uns eine Basisklasse vor, da es scheint, als wollen wir einen BaseDao definieren. Dass unsere Implementierung exakt diesem Vorschlag entspricht, interessiert ihn nicht.

Kinderkrankheiten wie diese werden aber mit Sicherheit in den kommenden Iterationen des ansonsten bereits sehr vielversprechenden ORMs behoben. Aktuell befindet sich Room in der Alpha 1 und wird wie gewohnt, zusammen mit seinem Annotation Processor, über Gradle vertrieben:

compile "android.arch.persistence.room:runtime:1.0.0-alpha1"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha1"

 

Codebeispiel:

@Database(entities = {Example.class}, version = 1)
@TypeConverters({DateTimeConverter.class})
public abstract class AppDatabase extends RoomDatabase {

    private static AppDatabase instance;

    public static AppDatabase getDatabase(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(context, AppDatabase.class, "db_name").build();
        }
        return instance;
    }

    public abstract ExampleDao getExampleDao();
}
@Entity
public class Example {
    @PrimaryKey(autoGenerate = true)
    public int uid;
    public DateTime createdAt;
    public DateTime updatedAt;
    public String name;
}
@Dao
public interface ExampleDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insert(Example entity);
  
    @Delete
    public void delete(Example entity);
     
    @Query("SELECT * FROM example")
    List<Example> findAll();
}

Comment