zur Übersicht

Realm as Plain Storage – Pitfalls and Issues

Lesedauer ca. 6 Minuten
09.05.2023

Um Daten auf mobilen Endgeräten dauerhaft zu speichern, gibt es viele Ansätze und technische Möglichkeiten. Datenbanken und speziell SQL-Datenbanken sind dabei sehr verbreitet. Häufig verfügen diese aber über jene Kriterien, auf die es nur für Anwendungsfälle außerhalb von Apps wirklich ankommt. Komplexe Abfragen und Aggregationen mögen in vielen Cloud- und Server-Anwendungen selbstverständlich sein, auf mobilen Endgeräten zählt jedoch oftmals die Zugriffsgeschwindigkeit deutlich mehr. Speziell hierfür hat sich Realm von MongoDB etabliert. Es handelt sich dabei um eine auf Zugriffe optimierte NoSQL-/Objektdatenbank.

Abgrenzung

Realm bietet als Datenbank für mobile Geräte diverse Mechanismen für lokale Live-Updates und auch eine Cloud-gestützte Synchronisation über mehrere Devices und Betriebssysteme hinweg. Darum soll es in diesem Beitrag jedoch nicht gehen. Stattdessen beleuchten unsere Xperten, auf welche Dinge beim Einsatz von Realm als reine Speicherlösung geachtet werden sollte.

Realm als intuitiv-minimale Schnittstelle

In der objektorientierten Programmierung hat es sich mittlerweile in vielen Anwendungen etabliert, verschiedene Schichten bei der Erstellung von Klassen zu trennen. Etwa die Geschäftslogik von der Speicherebene oder von einer Webschnittstelle. Die Gründe dafür lassen sich auf die Formate ihrer Properties und dem Single-responsibility principle (SRP) zurückführen. Bei Speicher- und Webschnittstellen sind diese Klassen sogenannte Data Transfer Objects, kurz DTOs. In Realm werden basierend auf dem eigenen Datenmodell im Hintergrund DTOs generiert, welche alle Felder als Lazy Access behandeln. Das geht Hand in Hand mit dem NoSQL-Ansatz von Realm. Daten werden also erst gelesen und geschrieben, wenn die Felder der DTOs angesprochen werden und auch nur genau diese. Um Daten zu speichern oder upzudaten, bestehen somit effektiv zwei Möglichkeiten:

  • Ein neues DTO erzeugen und per Primary Key ein neues Objekt anlegen oder ein bestehendes aktualisieren und alle relevanten Felder mit Daten beschreiben
  • Ein existierendes DTO per Query aus der Datenbank laden und Felder mit neuen Werten beschreiben

Obwohl Realm nur Felder lazy schreibt, werden bei Erzeugung eines neuen DTO trotzdem alle Felder in die Datenbank geschrieben, auch jene, die von Entwickelnden nicht selbst gefüllt wurden. Felder, die auf null belassen werden, würden bei einem Update mit neuem DTO also dennoch bisher existierende Felder desselben Objekts in der Datenbank überschreiben. Ein Update einzelner Felder muss also immer über ein vorheriges Query durchgeführt werden.

Ein einfaches Realm-Objekt könnte wie folgt aussehen:


dto


Objekt-Dependencies und Nested Object Graph

Da Realm eine Objektdatenbank ist und Nested Objects explizit vorgesehen sind, ergeben sich als Storage-Möglichkeit für Realm sehr schnell komplexe Graphen oder sogar Zyklen.

Beispiel Nested Objects:


dtos


Intern arbeitet Realm hinter den generierten DTOs mit Referenzen auf Speicheradressen, die auf dieselben Speicherstellen referenzieren. Dies gilt allerdings nur für Simple Properties. Referenzen auf andere Nested Objects werden durch das Wrapping in DTOs auf eigene Instanzen gemappt. Für eine Verwendung in bspw. HashSets oder Listen könnte dies schnell zu unerwünschten Duplikaten führen. Um das Codebeispiel von vorhin wieder aufzugreifen, könnten etwa zwei verschiedene ShoppingCarts auf dasselbe ShopItem verweisen. Realm würde dabei zwei Instanzen des ShopItems erzeugen, die aber beide auf dieselben Properties verweisen und bei einer Änderung des einen auch eine Änderung des anderen bewirken würden.

Objects

DTO vs. Modelle

Realm ist ausgelegt auf direktes UI-Binding durch eigene Adapter, die bei Änderungen in der Datenbank reagieren und die UI aktualisieren. Was auf den ersten Blick sehr praktisch erscheinen mag, stellt sich jedoch schnell als technische Hürde heraus – bedingt durch die stetig wachsende Speicheranforderung an die offen gehaltene Live-Datenbank. Zudem widerspricht ein direktes Binding der DTOs einer sauberen Abstraktion zwischen Datenbank-Repräsentation und Modell-Ebene einer App. Die üblichen Inkompatibilitäten zwischen diversen Datentypen sind nur eines der typischen Probleme (bspw. File/URI vs. String). Volatile Properties, die lediglich zur Laufzeit Verwendung finden und nicht gespeichert und somit explizit als ignored markiert werden müssen, bilden ein weiteres Problem, welches bei der automatischen Datenbankmigration schnell zu einem Fallstrick und einem inkompatiblen Datenbankschema führen kann. Speziell unter iOS ist die Swift-API für Realm so gestaltet, dass sie eigenständig Migrationen von einer Datenbankschemaversion zur nächsten durchführt, ohne den Entwickelnden die Möglichkeit expliziter Migration zu bieten.

Beispiel eines Geschäftslogik-Modells in Anlehnung zum oben geführten DTO:


model


Eine Umwandlung von DTOs in Modell-Objekte steht dabei vor der Herausforderung des im vorherigen Abschnitt beschriebenen Nested Object Graphs. Identische Objekte, die durch separate DTO-Instanzen repräsentiert werden, könnten bei einer Konversion leicht als eigene Objekte instanziiert werden. Änderungen an ihren Properties wären zur Laufzeit unabhängig voneinander möglich, aber beim Schreiben zurück in die Datenbank würden bei einer Rückkonvertierung in DTOs zwei DTOs mit unterschiedlichen Properties und demselben Primary Key erzeugt werden.

Das effektive Ergebnis hinge von der Bearbeitungsreihenfolge ab und wäre somit als undefinierbar einzustufen. Eine Caching-Strategie von Modell-Instanzen wäre im Schritt der DTO-to-Model-Konvertierung ein Ausweg aus dieser Situation. So würden alle Objekte auf dieselben Instanzen von Nested Objects verweisen und es gäbe keine Duplikate. Eine Rückkonvertierung in DTOs würde zwar noch immer mehrere DTOs erzeugen, ihre Properties wären allerdings alle identisch. Es bliebe hier lediglich ein Overhead zu beachten, der gewisse DTOs eventuell mehrfach in die Datenbank schreiben würde, abhängig von der Anzahl ihrer Referenzen.

Beispiel:


models


Zyklische Nested Objects

Wie in jedem komplexen System wäre auch hier ein Zyklus zwischen diversen Objekten denkbar. Das obige Codebeispiel zeigt einen Zyklus zwischen User und ShoppingCart, was für den Zugriff zwar zweckdienlich ist, aber für die DTO-Konvertierung eine Hürde darstellt. Realm nutzt intern einen Backlink von Tabellenspalten auf Nested Objects, sodass eine doppelt verkettete Referenz entsteht. Schauen wir uns also bspw. das UserDTO an, so nutzt Realm intern im ShoppingCartDTO eine Spalte, die zurück auf das UserDTO verweist. Leider ist dieser Backlink über die Hochsprachenbibliotheken von Realm in Swift oder Kotlin nicht verfügbar. Daher existiert in den DTOs nur eine Referenz von UserDTO auf ShoppingCartDTO, also ein Tree.

Ein zyklischer Graph ließe sich technisch über eine zusätzliche Referenz anlegen. Realm selbst ist in der Lage, mit direkten zyklischen Referenzen umzugehen. Man könnte also im ShoppingCartDTO eine Referenz auf UserDTO einführen. In Anbetracht einer DTO-to-Model-Konvertierung wäre davon allerdings abzuraten. Ein übliches Vorgehensmodell erinnert hier an Foreign-Key-Mechaniken von SQL-Datenbanken. Vorstellbar wäre also eine Property im ShoppingCartDTO, die den Universally Unique Identifier (UUID) des Users enthält. Bei der Konvertierung wäre das User-Objekt dann aus dem Cache oder aus Realm ladbar. Eine andere Möglichkeit ergäbe sich daraus, keine Rückreferenz zu haben, solange die Konvertierung immer beim User-Objekt beginnt und den Tree tiefer wandert. So wäre es bspw. bei der Konvertierung des ShoppingCartDTOs möglich, dem ShoppingCart die bereits konvertiere User-Instanz zuzuweisen. ShoppingCart wäre in diesem Fall sinnvoll als WeakProperty zu führen.

In der Modell-Schicht der Anwendung ließe sich dies exakt so abbilden und als direkte Referenzierung zwischen Objekten modellieren. Bei der DTO-to-Model-Konvertierung wäre in Kombination mit Modell-Caching zu beachten, dass zunächst das Parent-Object User selbst im Cache hinterlegt wird, bevor das Nested Object ShoppingCart geladen wird. Letzteres würde sonst das Parent-Object User nicht anhand des Primary Keys im Cache finden und eine neue Instanz erzeugen, womit zwei unabhängige User-Instanzen existieren würden.

Ausblick

Die bisher erläuterten Details sind inhärent in Realm und finden sich so oder in ähnlicher Form auch in anderen Datenbanken. Darüber hinaus lassen sich bei Realm auch Hürden identifizieren, die nicht inhärent, sondern auf Bugs und interne Probleme zurückzuführen sind. Das trifft bspw. auf das geführte ChangeLog der Kotlin/Realm-API zu und ein genauerer Blick darauf kann sich durchaus lohnen. Was die Ursachen und Fehler betrifft, so ist MongoDB/Realm stets hinterher, diese zu beheben.

Auch wenn Realm nach unserer Erfahrung womöglich nicht zur stabilsten aller Datenbanken zählt, ist sie mit klar seriellem Zugriff ein äußerst zuverlässiges Tool. Gerade für große Datenmengen mit sehr simplen Zugriffsmustern eignet sich Realm aus Sicht unserer Xperten besonders.