Auf komplexe Daten mit Room verweisen

Room bietet Funktionen zum Konvertieren zwischen einfachen und Box-Typen, erlaubt jedoch keine Objektverweise zwischen Entitäten. In diesem Dokument wird erläutert, wie Typkonverter verwendet werden und warum Room keine Objektverweise unterstützt.

Typkonverter verwenden

Manchmal muss Ihre Anwendung einen benutzerdefinierten Datentyp in einer einzelnen Datenbankspalte speichern. Sie unterstützen benutzerdefinierte Typen, indem Sie Typkonverter angeben. Das sind Methoden, mit denen Room angewiesen wird, wie benutzerdefinierte Typen in bekannte und von bekannten Typen konvertiert werden sollen, die „Room“ beibehalten werden kann. Typkonverter werden mithilfe der Annotation @TypeConverter identifiziert.

Angenommen, Sie müssen Instanzen von Date in der Raumdatenbank dauerhaft speichern. Room weiß nicht, wie Date-Objekte beibehalten werden sollen. Daher müssen Sie Typkonverter definieren:

Kotlin

class Converters {
  @TypeConverter
  fun fromTimestamp(value: Long?): Date? {
    return value?.let { Date(it) }
  }

  @TypeConverter
  fun dateToTimestamp(date: Date?): Long? {
    return date?.time?.toLong()
  }
}

Java

public class Converters {
  @TypeConverter
  public static Date fromTimestamp(Long value) {
    return value == null ? null : new Date(value);
  }

  @TypeConverter
  public static Long dateToTimestamp(Date date) {
    return date == null ? null : date.getTime();
  }
}

In diesem Beispiel werden zwei Methoden zur Typkonverter definiert: eine, die ein Date-Objekt in ein Long-Objekt konvertiert, und eine, die die umgekehrte Konvertierung von Long in Date durchführt. Room weiß, wie Long-Objekte beibehalten werden sollen, und kann diese Konvertierungsprogramme verwenden, um Date-Objekte dauerhaft zu speichern.

Als Nächstes fügen Sie der AppDatabase-Klasse die Annotation @TypeConverters hinzu, damit Room die von Ihnen definierte Converter-Klasse kennt:

Kotlin

@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
  abstract fun userDao(): UserDao
}

Java

@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
  public abstract UserDao userDao();
}

Wenn diese Typkonverter definiert sind, können Sie den benutzerdefinierten Typ in Ihren Entitäten und DAOs genau wie primitive Typen verwenden:

Kotlin

@Entity
data class User(private val birthday: Date?)

@Dao
interface UserDao {
  @Query("SELECT * FROM user WHERE birthday = :targetDate")
  fun findUsersBornOnDate(targetDate: Date): List<User>
}

Java

@Entity
public class User {
  private Date birthday;
}

@Dao
public interface UserDao {
  @Query("SELECT * FROM user WHERE birthday = :targetDate")
  List<User> findUsersBornOnDate(Date targetDate);
}

In diesem Beispiel kann Room den definierten Typkonverter überall verwenden, da Sie AppDatabase mit @TypeConverters annotiert haben. Sie können jedoch auch Bereichstypkonverter in bestimmte Entitäten oder DAOs umwandeln. Dazu annotieren Sie die Klassen @Entity oder @Dao mit @TypeConverters.

Initialisierung des Steuerelementtypkonverters

Normalerweise übernimmt Room die Instanziierung der Typkonverter für Sie. Manchmal müssen Sie jedoch zusätzliche Abhängigkeiten an Ihre Typkonverterklassen übergeben. In diesem Fall muss Ihre Anwendung die Initialisierung der Typkonverter direkt steuern. Annotieren Sie in diesem Fall die Konvertierungsklasse mit @ProvidedTypeConverter:

Kotlin

@ProvidedTypeConverter
class ExampleConverter {
  @TypeConverter
  fun StringToExample(string: String?): ExampleType? {
    ...
  }

  @TypeConverter
  fun ExampleToString(example: ExampleType?): String? {
    ...
  }
}

Java

@ProvidedTypeConverter
public class ExampleConverter {
  @TypeConverter
  public Example StringToExample(String string) {
    ...
  }

  @TypeConverter
  public String ExampleToString(Example example) {
    ...
  }
}

Zusätzlich zur Deklaration der Konvertierungsklasse in @TypeConverters verwenden Sie die Methode RoomDatabase.Builder.addTypeConverter(), um eine Instanz Ihrer Konvertierungsklasse an den RoomDatabase-Builder zu übergeben:

Kotlin

val db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build()

Java

AppDatabase db = Room.databaseBuilder(...)
  .addTypeConverter(exampleConverterInstance)
  .build();

Warum in Room keine Objektverweise zulässig sind

Wichtigste Erkenntnis: Der Raum erlaubt keine Objektverweise zwischen Entitätsklassen. Stattdessen müssen Sie die Daten, die Ihre Anwendung benötigt, explizit anfordern.

Das Zuordnen von Beziehungen aus einer Datenbank zum jeweiligen Objektmodell ist eine gängige Praxis und funktioniert serverseitig sehr gut. Auch wenn Felder beim Zugriff durch das Programm geladen werden, funktioniert der Server weiterhin gut.

Auf Clientseite ist diese Art von Lazy Loading jedoch nicht möglich, da sie normalerweise im UI-Thread erfolgt und das Abfragen von Informationen auf dem Laufwerk im UI-Thread erhebliche Leistungsprobleme mit sich bringt. Der UI-Thread hat in der Regel etwa 16 ms, um das aktualisierte Layout einer Aktivität zu berechnen und zu zeichnen. Selbst wenn eine Abfrage nur 5 ms dauert, ist es also immer noch wahrscheinlich, dass der App die Zeit zum Zeichnen des Frames abläuft und es zu deutlichen visuellen Störungen kommt. Die Abfrage kann noch länger dauern, wenn eine separate Transaktion parallel ausgeführt wird oder das Gerät andere laufwerksintensive Aufgaben ausführt. Wenn Sie Lazy Loading nicht verwenden, ruft Ihre Anwendung jedoch mehr Daten ab, als erforderlich sind, was zu Problemen mit der Speicherauslastung führt.

Bei objektrelationalen Zuordnungen müssen die Entwickler diese Entscheidung in der Regel selbst treffen, damit sie das tun können, was für die Anwendungsfälle ihrer Anwendung am besten ist. Entwickler entscheiden in der Regel, das Modell für ihre App und die UI freizugeben. Diese Lösung lässt sich jedoch nicht gut skalieren, da sich die Benutzeroberfläche im Laufe der Zeit ändert und das gemeinsam genutzte Modell Probleme verursacht, die für Entwickler schwer vorherzusehen und zu beheben sind.

Stellen Sie sich beispielsweise eine UI vor, über die eine Liste von Book-Objekten geladen wird, wobei jedes Buch ein Author-Objekt enthält. Am Anfang könnten Sie Ihre Abfragen so gestalten, dass Lazy Loading verwendet wird, damit Book-Instanzen den Autor abrufen. Beim ersten Abrufen des Felds author wird die Datenbank abgefragt. Etwas später stellen Sie fest, dass Sie auch den Namen des Autors in der Benutzeroberfläche Ihrer App anzeigen müssen. Sie können problemlos auf diesen Namen zugreifen, wie im folgenden Code-Snippet gezeigt:

Kotlin

authorNameTextView.text = book.author.name

Java

authorNameTextView.setText(book.getAuthor().getName());

Diese scheinbar harmlose Änderung führt jedoch dazu, dass die Tabelle Author im Hauptthread abgefragt wird.

Wenn Sie Autoreninformationen im Voraus abfragen, wird es schwierig, die Art des Ladens der Daten zu ändern, wenn Sie diese Daten nicht mehr benötigen. Wenn auf der Benutzeroberfläche Ihrer App beispielsweise keine Author-Informationen mehr angezeigt werden müssen, lädt die App effektiv Daten, die nicht mehr angezeigt werden, wodurch wertvoller Speicherplatz verschwendet wird. Die Effizienz der Anwendung verschlechtert sich noch, wenn die Klasse Author auf eine andere Tabelle verweist, z. B. Books.

Wenn Sie mit „Room“ auf mehrere Entitäten gleichzeitig verweisen möchten, erstellen Sie stattdessen ein POJO, das alle Entitäten enthält. Schreiben Sie dann eine Abfrage, die die entsprechenden Tabellen verbindet. Dieses gut strukturierte Modell führt in Kombination mit den robusten Funktionen zur Abfragevalidierung von Room dazu, dass Ihre Anwendung beim Laden von Daten weniger Ressourcen verbraucht. Dadurch wird die Leistung und die Nutzerfreundlichkeit Ihrer Anwendung verbessert.