Room ist eine Persistence Library, die einen abstrakten Layer über SQLite zur Verfügung stellt. Damit ist ein einfacher und sicherer Zugriff möglich, ohne auf die Funktion von SQLite zu verzichten. In diesem Tutorial wird eine Einführung, wie man die Datenbank erstellt und Daten von der Datenbank abfragt und bearbeitet.
Warum sollte man Room verwenden und nicht einfach direkt SQLite?
Mit Room hat man eine Verifikation des SQL-Codes während dem Kompilieren. Dies ist aber nicht bei SQLite verfügbar. Außerdem müssen einer Änderung des Datenbank-Schemas alle betroffenen SQL-Queries auch manuell geändert und angepasst werden.
Room ist eine "Object Relational Mapping" Library, die Java-Objekte mit Datenbank-Objekte verbindet und damit ist der Zugriff viel schneller und einfacher. Die Hauptkomponenten von Room bestehen zum Beispiel aus "Entities", "DAO" und etc., die man auch zum Beispiel in Java-Programmen mit Hibernate findet. Die offizielle Dokumentation von Room befindet sich auf dieser Webseite: https://developer.android.com/topic/libraries/architecture/room.html
Für dieses Tutorial wird Android Studio mit einer installierten SDK (ab Android 5) benötigt. Es wird mit Java programmiert. Die Datenbank und die benötigten Methoden werden erstellt, um die Daten einer Aufgabenliste App zu speichern.
Vorraussetzungen einrichten
Um die Datenbank einzurichten werden neue Dependencies benötigt. Room und RxJava2 müssen nun installiert werden. Fügen Sie dies im Abschnitt "dependencies" in der Gradle-Datei der App (die Datei "build.gradle (Module: app)" - befindet sich unter dem Abschnitt Gradle Scripts) im neu erstellten Projekt. Die neueste Version von Room befindet sich auf dieser Webseite: https://developer.android.com/jetpack/androidx/releases/room
Die neueste Version von RxJava: https://github.com/ReactiveX/RxJava/tree/2.x
Diese Dependencies werden benötigt. In diesem Tutorial wird die "Room" Version 2.1.0-rc01, "RxJava" Version 2.1.0 und "RxAndroid" Version 2.0.1 verwendet:
implementation 'android.arch.persistence.room:runtime:2.1.0-rc01'
implementation 'android.arch.persistence.room:rxjava2:2.1.0-rc01'
annotationProcessor 'android.arch.persistence.room:compiler:2.1.0-rc01'
implementation 'io.reactivex.rxjava2:rxjava:2.1.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
Bitte starten Sie dann die Gradle-Synchronisation um die neuen Dependencies herunterzuladen.
Datenmodell erstellen
Nun werden die Tabellen für die Datenbank erstellt. Es wird ein neuer Package "model" erstellt und in diese werden die Tabellen gespeichert.
Eine Tabelle entspricht hier einer Java-Klasse und einer Entity, die aber Annotations beinhaltet. Die Java-Klasse ist "public".
Hier wird die Java-Klasse "Task" erstellt, die Aufgaben speichert.
@Entity(tableName = "task")
public class Task {
@NonNull
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name="id")
private int id;
@NonNull
@ColumnInfo(name="taskname")
private String taskName;
@ColumnInfo(name="duedate")
private Date dueDate;
@ColumnInfo(name="priority")
private int priority;
public Task(){
}
@Ignore
public Task(int id, @NonNull String taskName, Date dueDate, String category, int priority, int points) {
this.id = id;
this.taskName = taskName;
this.dueDate = dueDate;
this.priority = priority;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@NonNull
public String getTaskName() {
return taskName;
}
public void setTaskName(@NonNull String taskName) {
this.taskName = taskName;
}
public Date getDueDate() {
return dueDate;
}
public void setDueDate(Date dueDate) {
this.dueDate = dueDate;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
Spalten der Tabelle werden mittels Instanzvariablen definiert. Hier werden die Spalten "id", "taskname", "duedate" und "priority" erstellt.
Wichtig: Es werden get...() und set...() Methoden für alle Instanzvariablen benötigt.
Es muss auch ein leerer Konstruktor ( hier Task() ) erstellt werden. Weitere Konstruktoren können auch hinzugefügt werden. Hier wurde ein zweiter Konstruktor erstellt, der nur von uns und nicht von der "Room library" verwendet wird.
@Entity: Diese Java-Klasse als Tabelle definieren. Mit tableName wird auch ein Name der Tabelle definiert, der sich auch von dem Namen der Java-Klasse unterscheiden kann.
@ColumnInfo: Einen Namen für die Spalten hinzufügen. Kann auch ein ganz anderen Namen haben als die Instanzvariable.
@PrimaryKey: Primärschlüssel der Datenbank definieren. Mit autoGenerate wird automatisch ein Primärschlüssel für den Datensatz erstellt.
@NonNull: Wenn eine Spalte ein Wert hinzugefügt werden muss. Nicht vergessen dies bei dem Primärschlüssel hinzuzufügen.
@Ignore: Mit dieser Annotation makierter Code wird von Room, dann ignoriert und nicht verarbeitet. Dies ist nützlich zum Beispiel, wenn ein Objekt nicht in die Datenbank gespeichert werden soll.
Optional kann auch eine Methode toString() in diese Klasse hinzugefügt werden. Damit kann man die Instanzvariablen der Klasse ausgeben, wenn das Klassenobjekt direkt aufgerufen wird.
"DAO Interface" erstellen
Nun muss es eine Java Klasse vom Typ "Interface" erstellt werden. Die Klasse muss die Annotation "@Dao" haben. Die dann später implementierbaren Funktionalitäten (Daten aktualisieren, hinzufügen oder löschen) werden als Interface Methoden in diesem DAO Interface erstellt. Zudem werden hier die Methoden zum Abruf von Daten in der Datenbank definiert.
@Dao
public interface AppDAO {
@Query("Select * from task where id=:id")
Flowable<Task> getTaskById(int id);
@Query("Select * from task")
Flowable<List<Task>> getAllTasks();
@Insert
void insertTask(Task... tasks);
@Update
void updateTask(Task... tasks);
@Delete
void deleteTask(Task task);
@Query("Delete from task")
void deleteAllTasks();
}
Oberhalb der Interface Methode wird mittels Annotations, wie man auf die Datenbank zugreift, definiert.
@Dao - Klasse als DAO Interface definieren.
@Insert - Diese Methode wird Datensätze hinzufügen. Das bedeutet es wird ein Query vom Typ "Insert" ausgeführt.
@Update - Diese Methode wird Datensätze aktualisieren
@Delete - Diese Methode ein Datensatz löschen
@Query - Die auszuführenden Datenbank-Operationen direkt über SQL Befehle definieren.
Datenbank erstellen
Erstellen Sie nun eine abstrakte Klasse ("public abstract class"), die von der Klasse RoomDatabase ("extends", existierende Klasse von Room) erben muss. Diese abstrakte Klasse erstellt die Datenbank dieser App.
@Database(version = 1, entities = {Task.class})
public abstract class AppDatabase extends RoomDatabase {
//Name der zu erstellenden Datenbank
public static final String DATABASE_NAME ="Tasks-App-Database";
public abstract AppDAO appDAO();
//Die Instanz der Datenbank
private static AppDatabase dbInstance;
public static AppDatabase getInstance(Context context){
if (dbInstance == null){
dbInstance = Room.databaseBuilder(context,AppDatabase.class,DATABASE_NAME).
fallbackToDestructiveMigration().
build();
}
return dbInstance;
}
}
Die Klasse muss eine Annotation @Database haben und in dieser Annotation werden die Entitätsklassen, die einer Tabelle entsprechen, hinzugefügt. Es können natürlich auch hier direkt mehrere Entitätsklassen hinzugefügt werden, die aber durch Beistriche getrennt werden.
fallbackToDestructiveMigration() - Diese Funktion bewirkt, dass die Datenbank neuerstellt wird, wenn es ein neues Datenbankschema erstellt worden ist.
Funktionen (Methoden) für Datenbank erstellen
Die Methoden die im DAO Interface erstellt wurden, werden nun implementiert. Mit diesen Methoden ist dann der Zugriff über Java Code auf die Datenbank möglich. Es wird nun ein Interface und zwei Klassen die dieses Interface implementiert ("implements") erstellt.
1. Es muss eine Interface Klasse mit den Variablen und Interface Methoden aus der DAO Interface Klasse AppDAO erstellt werden.
public interface AppDataSourceInterface {
Flowable<Task> getTaskById(int id);
Flowable<List<Task>> getAllTasks();
void insertTask(Task... tasks);
void updateTask(Task... tasks);
void deleteTask(Task task);
void deleteAllTasks();
}
2. Nun wird eine Klasse erstellt, welche die Methoden von der zuvor erstellten Interface Klasse implementiert.
public class AppDataSource implements AppDataSourceInterface {
private AppDAO appDAO;
private static AppDataSource dbInstance;
@Override
public Flowable<Task> getTaskById(int id) {
return appDAO.getTaskById(id);
}
@Override
public Flowable<List<Task>> getAllTasks() {
return appDAO.getAllTasks();
}
@Override
public void insertTask(Task... tasks) {
appDAO.insertTask(tasks);
}
@Override
public void updateTask(Task... tasks) {
appDAO.updateTask(tasks);
}
@Override
public void deleteTask(Task task) {
appDAO.deleteTask(task);
}
@Override
public void deleteAllTasks() {
appDAO.deleteAllTasks();
}
}
In den implementierten Methoden werden die dazugehörige Methoden von dem DAO Interface appDAO aufgerufen.
3. Nun wird eine Klasse erstellt, die als Repository verwendet wird. Diese neu erstellte Klasse implementiert auch das Interface "AppDataSourceInterface".
public class AppRepository implements AppDataSourceInterface {
private AppDataSourceInterface dbLocalDataSource;
private static AppRepository dbInstance;
public AppRepository(AppDataSourceInterface dbLocalDataSource){
this.dbLocalDataSource = dbLocalDataSource
}
public static AppRepository getInstance(AppDataSourceInterface dbLocalDataSource){
if (dbInstance == null){
dbInstance = new AppRepository(dbLocalDataSource);
}
return dbInstance;
}
@Override
public Flowable<Task> getTaskById(int id) {
return dbLocalDataSource.getTaskById(id);
}
@Override
public Flowable<List<Task>> getAllTasks() {
return dbLocalDataSource.getAllTasks();
}
@Override
public void insertTask(Task... tasks) {
dbLocalDataSource.insertTask(tasks);
}
@Override
public void updateTask(Task... tasks) {
dbLocalDataSource.updateTask(tasks);
}
@Override
public void deleteTask(Task task) {
dbLocalDataSource.deleteTask(task);
}
@Override
public void deleteAllTasks() {
dbLocalDataSource.deleteAllTasks();
}
}
Mit dieser Klasse wird dann die Datenbearbeitung der Datenbank über Java Code (z.B.: in der Klasse MainActivity.java) durchgeführt.
Bearbeitung der Daten in der Datenbank
Die zuvor erstellten Methoden können nun verwendet werden, um zum Beispiel Daten hinzuzufügen, zu bearbeiten, usw.
In der MainActivity.Java wird dann eine List und ein ArrayAdapter erstellt. Die List besteht aus Klassenobjekte der Klasse "Task" und speichert alle Aufgaben in einer "Array List". Mit diesen Java Objekten werden die Daten Datenbank abgefragt und bearbeitet.
List<Task> taskList;
ArrayAdapter adapter;
private CompositeDisposable compositeDisposable;
private AppRepository appRepository;
Das Laden von Daten aus der Datenbank erfolgt über ein Objekt vom Typ Disposable. Ein "Disposable" sendet Daten hinaus, die dann von "Subscribers" verarbeitet werden. Ein "Disposable" Objekt besteht aus einem bis mehreren "Subscribers". "Disposable" wird auch für alle anderen Tätigkeiten (Hinzufügen von Daten in der Datenbank, Löschen von Daten in der Datenbank, ...) an der Datenbank verwendet.
Für das Abrufen von Datenbankdaten muss eine anonyme innere Klasse für das Interface Consumer erstellt werden.
Disposable disposable = appRepository.getAllTasks()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<Task>>() {
@Override
public void accept(List<Task> tasks) throws Exception {
onGetAllTaskSucess(tasks);
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
//Hier kommen Funktionen, die ausgeführt werden sollen, wenn die Datenbank erfolgreich aufgerufen wurde. Kann auch leer sein oder nur eine Meldung in einem "Toast" ausgeben.
}
});
compositeDisposable.add(disposable);
Im Interface "Consumer" wird die Methode accept() (die sich in einer inneren anonymen Klasse befindet) überschrieben. Die Methode accept() bekommt als Argument (List<Task> tasks) die Tabelle "Task", die von der Datenbank in einer List konvertiert wurde.
In der Methode onGetAllTaskSucess(tasks) in der Hauptklasse MainActivity.java werden alle Datensätze von der Datenbank geladen und in eine List gespeichert.
private void onGetAllTaskSucess(List<Task> tasks) {
taskList.clear();
taskList.addAll(tasks);
adapter.notifyDataSetChanged();
}
Es werden Daten in die Datenbank (eine neue Aufgabe - neuer Datensatz in Tabelle Task) auch über ein Disposable-Objekt hinzugefügt. Für das Bearbeiten von Datenbankdaten muss eine anonyme innere Klasse für das Interface ObservableOnSubscribe erstellt werden.
addButton = (FloatingActionButton) findViewById(R.id.addTaskButon);
addButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Disposable disposable = io.reactivex.Observable.create(new ObservableOnSubscribe<Object>(
) {
@Override
public void subscribe(ObservableEmitter<Object> emitter) throws Exception {
//Befehle um Daten in der Datenbank zu speichern oder zu löschen
Task task = new Task();
task.setTaskName(textfield1.getText());
appRepository.insertTask(task);
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new Consumer() {
@Override
public void accept(Object o) throws Exception {
//Erfolgreiche Meldung ausgeben
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
//Die Fehlermeldungen von Variable throwable ausgeben
}
}, new Action() {
@Override
public void run() throws Exception {
//Daten aus der Datenbank nochmal laden.
loadDatabaseData();
}
}
);
}
});
Wenn auf dem Hinzufügen Button ("addButton") geklickt wird, dann wird der vom Benutzer eingegebene Text im Textfeld "textfield1" in die Datenbank in der Spalte "taskname" gespeichert. Zum Bearbeiten der Daten in der Datenbank wird das Interface "ObservableOnSubscribe" verwendet. In diesem Interface wird die einzige Methode "subscribe()" mit den auszuführenden Datenbankbefehlen überschrieben. Zuletzt wird dann die Methode subscribe() aufgerufen, wo programmiert wird, was nach dem Bearbeiten der Datenbankdaten passieren soll.
Die Daten von der Datenbank können dann mit diesen Befehlen in einer Activity (hier: MainActivity.java) bearbeitet werden. Am besten eignet sich für die Darstellung der Datenbank-Daten eine ListView, die dann alle Werte eines Datensatz der Datenbank in einer Liste ausgibt.