Auf komplexe Daten mit Room verweisen

Room kann primitive und boxed-Typen konvertieren, lässt aber keine Objektreferenzen zwischen Entitäten zu. Informationen zur Verwendung von Typkonvertern und dazu, warum Room keine Objektreferenzen unterstützt

Typkonverter verwenden

Manchmal müssen Sie in Ihrer App einen benutzerdefinierten Datentyp in einer einzelnen Datenbankspalte speichern. Sie unterstützen benutzerdefinierte Typen durch die Bereitstellung von Typkonvertern. Das sind Funktionen, die Room mitteilen, wie benutzerdefinierte Typen in bekannte Typen konvertiert werden können, die von Room beibehalten werden können, und umgekehrt. Sie identifizieren Typkonverter mit der @ColumnTypeConverter Annotation.

Angenommen, Sie müssen Instanzen von Date in Ihrer Room-Datenbank beibehalten. Room kann Date-Objekte nicht nativ beibehalten. Daher müssen Sie Typkonverter definieren:

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

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

In diesem Beispiel werden zwei Typkonverterfunktionen definiert: eine, die ein Date-Objekt in ein Long-Objekt konvertiert, und eine, die ein Long-Objekt wieder in ein Date-Objekt konvertiert. Da Room Long-Objekte beibehalten kann, können diese Konverter verwendet werden, um Date-Objekte beizubehalten.

Fügen Sie als Nächstes der AppDatabase Klasse die @ColumnTypeConverters Annotation hinzu, damit Room die von Ihnen definierte Konverter Klasse verwenden kann:

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

Nachdem diese Typkonverter definiert wurden, können Sie Ihren benutzerdefinierten Typ in Ihren Entitäten und DAOs genauso verwenden wie primitive Typen:

@Entity
data class User(
    @PrimaryKey val id: Long,
    val name: String,
    val birthday: Date?
)

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

Da Sie AppDatabase in diesem Beispiel mit @ColumnTypeConverters annotiert haben, kann Room den definierten Typkonverter überall verwenden. Wenn Sie Typkonverter stattdessen auf bestimmte Entitäten oder DAOs beschränken möchten, annotieren Sie Ihre @Entity- oder @Dao-Klassen mit @ColumnTypeConverters.

Initialisierung von Typkonvertern steuern

Normalerweise instanziiert Room Typkonverter für Sie. Wenn Sie jedoch zusätzliche Abhängigkeiten an Ihre Typkonverterklassen übergeben müssen, muss Ihre App die Initialisierung direkt steuern. Annotieren Sie in diesem Fall Ihre Konverter klasse mit @ProvidedColumnTypeConverter:

@ProvidedColumnTypeConverter
class ExampleConverter {
    @ColumnTypeConverter
    fun stringToExample(string: String?): ExampleType? {
        return string?.let { ExampleType() }
    }

    @ColumnTypeConverter
    fun exampleToString(example: ExampleType?): String? {
        return example?.toString()
    }
}

Zusätzlich zur Deklaration Ihrer Konverterklasse in @ColumnTypeConverters, verwenden Sie die RoomDatabase.Builder.addColumnTypeConverter Funktion, um eine Instanz Ihrer Konverterklasse an den RoomDatabase Builder zu übergeben:

val db = Room.databaseBuilder<MyDatabase>(applicationContext, "database-name")
    .addColumnTypeConverter(exampleConverterInstance)
    .build()

Warum Room keine Objektreferenzen zulässt

Wichtigste Erkenntnis:Room lässt keine Objektreferenzen zwischen Entitätsklassen zu. Stattdessen müssen Sie die Daten, die Ihre App 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 das Programm Eigenschaften lädt, sobald darauf zugegriffen wird, funktioniert der Server weiterhin gut.

Clientseitig ist diese Art des Lazy Loading jedoch nicht möglich, da sie in der Regel im UI-Thread erfolgt. Das Abfragen von Informationen auf der Festplatte im UI-Thread führt zu erheblichen Leistungsproblemen. Der UI-Thread hat in der Regel etwa 16 ms Zeit, um das aktualisierte Layout einer Aktivität zu berechnen und zu zeichnen. Selbst wenn eine Abfrage nur 5 ms dauert, ist es wahrscheinlich, dass Ihre App nicht genügend Zeit hat, den Frame zu zeichnen, was zu sichtbaren visuellen Fehlern führt. Die Abfrage kann noch länger dauern, wenn parallel eine separate Transaktion ausgeführt wird oder wenn auf dem Gerät andere festplattenintensive Aufgaben ausgeführt werden. Wenn Sie jedoch kein Lazy Loading verwenden, ruft Ihre App mehr Daten ab als erforderlich, was zu Problemen mit der Speicherauslastung führt.

Bei objektrelationalen Zuordnungen wird diese Entscheidung in der Regel den Entwicklern überlassen, damit sie das tun können, was für die Anwendungsfälle ihrer App am besten geeignet ist. Entwickler entscheiden sich in der Regel dafür, das Modell zwischen ihrer App und der UI zu teilen. Diese Lösung ist jedoch nicht gut skalierbar, da das gemeinsame Modell im Laufe der Zeit zu Problemen führt, die für Entwickler schwer vorhersehbar und zu debuggen sind.

Nehmen wir beispielsweise eine UI, die eine Liste von Book-Objekten lädt, wobei jedes Buch ein Author-Objekt hat. Sie können Ihre Abfragen so gestalten, dass Lazy Loading verwendet wird, damit Instanzen von Book den Autor abrufen. Beim ersten Abruf der Eigenschaft author wird die Datenbank abgefragt. Einige Zeit später stellen Sie fest, dass Sie den Namen des Autors auch in der UI Ihrer App anzeigen müssen. Sie können auf diesen Namen zugreifen, wie im folgenden Code-Snippet gezeigt:

Text(text = book.author.name)

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

Wenn Sie Autoreninformationen im Voraus abfragen, sie aber nicht benötigen, ist es schwierig, die Art und Weise zu ändern, wie Daten geladen werden. Wenn die UI Ihrer App beispielsweise keine Author-Informationen mehr anzeigen muss, lädt Ihre App Daten, die nicht angezeigt werden, und verschwendet so wertvollen Speicherplatz. Die Effizienz Ihrer App sinkt noch weiter, wenn die Klasse Author auf eine andere Tabelle verweist, z. B. Books.

Wenn Sie gleichzeitig auf mehrere Entitäten mit Room verweisen möchten, erstellen Sie ein Datenobjekt, das jede Entität enthält, und schreiben Sie dann eine Abfrage, die die entsprechenden Tabellen verknüpft. Dieses gut strukturierte Modell in Kombination mit den robusten Funktionen zur Abfragevalidierung von Room ermöglicht es Ihrer App, beim Laden von Daten weniger Ressourcen zu verbrauchen, was die Leistung und Nutzerfreundlichkeit Ihrer App verbessert.