There are many approaches and technical possibilities for permanently storing data on mobile devices. Databases and especially SQL databases are very common for this purpose. However, they often do have criteria that are not important for use cases in apps. Complex queries and aggregations may be a matter of course in many cloud and server applications, but on mobile devices, access speed is much more important. Realm from MongoDB has established itself specifically for this task. It is a NoSQL/object database optimised for access.
Delimitation
As a database for mobile devices, Realm offers various mechanisms for local live updates and also cloud-based synchronisation across several devices and operating systems. However, that is not what this article is about. Instead, our Xperts shed light on the things to look out for when using Realm as a pure storage solution.
Realm as intuitive-minimal interface
In object-oriented programming, it has now become best practise in many applications to separate different layers when creating classes. For example, the business logic from the storage layer or from a web interface. The reasons for this can be traced back to the formats of their properties and the single-responsibility principle (SRP). In the case of storage and web interfaces, these classes are so-called data transfer objects, or DTOs for short. In Realm, DTOs are generated in the background based on its own data model, which treat all fields as lazy access. This goes hand in hand with Realm's NoSQL approach. Data is therefore only read and written when the fields of the DTOs are addressed, and only these fields. To save or update data, there are effectively two possibilities:
- Create a new DTO and save a new object via primary key or update an existing one and fill all relevant fields with data
- Load an existing DTO from the database via query and fill fields with new values
Although Realm only writes fields lazy, when a new DTO is created, all fields are still written to the database, even those that were not filled by the developers themselves. Fields that are left null still overwrite previously existing fields of the same object in the database during an update with a new DTO. An update of individual fields must therefore always be carried out via a previous query.
A simple realm object could look like this:
Object Dependencies and Nested Object Graph
Since Realm is an object database and nested objects are explicitly supported, complex graphs or even cycles quickly emerge as storage options for Realm.
Example Nested Objects:
Internally, Realm works behind the generated DTOs with references to memory addresses that reference the same memory locations. However, this only applies to simple properties. References to other nested objects are mapped to their own instances by wrapping them in DTOs. For use in HashSets or lists, for example, this could quickly lead to unwanted duplicates. To go back to the code example from earlier, two different ShoppingCarts could refer to the same ShopItem. In this case, Realm would create two instances of the ShopItem, but both would refer to the same properties and a change in one would also cause a change in the other.
DTO vs. Models
Realm is designed for direct UI binding through its own adapters, which react to changes in the database and update the UI. What may seem very practical at first glance has been a technical hurdle in the past – due to the constantly growing memory requirements for the open live database. In addition, a direct binding of the DTOs contradicts a clean abstraction between the database representation and the model level of an application. The usual incompatibilities between various data types are only one of the typical problems (e.g. file / URI vs. string). Volatile properties, which are only used at runtime and do not have to be saved and thus explicitly marked as ignored, are another problem that can quickly lead to a pitfall and incompatible database schema during automatic database migration. Especially on iOS, the Swift API for Realm is designed to perform migrations from one database schema version to the next on its own, without offering the developer the option of an explicit migration.
Example of a business logic model based on the DTO above:
A conversion of DTOs into model objects faces the challenge of the nested object graph described in the previous section. Identical objects represented by separate DTO instances could easily be instantiated as separate objects during a conversion. Changes to their properties would be possible independently at runtime, but when writing back to the database, a conversion back to DTOs would create two DTOs with different properties and the same primary key.
The effective result would depend on the processing sequence and would thus be classified as nondeterministic. A caching strategy for model instances would be a way out of this situation in the DTO-to-model conversion step. This way, all objects would refer to the same instances of nested objects and there would be no duplicates. A conversion back to DTOs would still create several DTOs, but their properties would all be identical. The only overhead to consider here would be that certain DTOs might be written to the database multiple times, depending on the number of their references.
Example:
Cyclic Nested Objects
As in any complex system, a cycle between various objects would be conceivable. The code example above shows a cycle between User and ShoppingCart, which is useful for access but a hurdle for DTO conversion. Realm internally uses a backlink from table columns to nested objects, resulting in a double chained reference. If we look at the UserDTO, for example, Realm uses a column internally in the ShoppingCartDTO that links back to the UserDTO. Unfortunately, this backlink is not available via Realm's high-level language libraries in Swift or Kotlin. Therefore, only a reference from UserDTO to ShoppingCartDTO exists in the DTOs, i.e. a tree.
A cyclic graph could technically be created via an additional reference. Realm itself is able to handle direct cyclical references. One could therefore introduce a reference to UserDTO in ShoppingCartDTO. In view of a DTO-to-model conversion, however, this would be inadvisable. A common procedure model here is reminiscent of foreign key mechanisms of SQL databases. One approach would be to have a property in ShoppingCartDTO that contains the UUID of the user. During conversion, the user object would then be loadable from the cache or realm. Another possibility would be not to have a back reference as long as the conversion always starts at the user object and moves down the tree. For example, when converting the ShoppingCartDTO, it would be possible to assign the already converted user instance to the ShoppingCart. In this case, ShoppingCart would make sense as a WeakProperty.
In the model layer of the application, this could be mapped exactly in this way and modelled as a direct referencing between objects. With the DTO-to-model conversion in combination with model caching, it should be noted that the parent object User itself is first stored in the cache before the nested object ShoppingCart is loaded. Otherwise, the latter would not find the parent object User in the cache using the primary key and would create a new instance, resulting in two independent User instances.
Outlook
The details explained so far are inherent in Realm and exist in this or a similar form in other databases. In addition, Realm also has some issues that are not inherent, but rather due to bugs and internal problems. This applies, for example, to the change log of the Kotlin/Realm API, which is worth a closer look. As far as root causes and bugs are concerned, MongoDB/Realm is typically actively working on fixing them.
Even if, in our experience, Realm is probably not the most stable of databases, it is an extremely reliable tool for use cases with serial access. From the point of view of our Xperts, Realm is particularly suitable for large amounts of data with very simple access patterns.