Updating Schema of Synced Realm and iOS App in Production
Tackling
AttributeGraph precondition failure: setting value during update
using Realm in SwiftUIUpdating Schema of Synced Realm and iOS App in Production
Synced Realm on iOS with SwiftUI using Sign-in with Apple for Authentication
Realm offers a development mode that allows you to conveniently design your app’s data models from within its source code. Once your app enters the production environment, you turn off the development mode as recommended. However, this leaves you in an unfortunate situation where updating your app’s data models becomes a little more complicated. The work required to perform such an update of the data model schema depends on the nature of the change to apply. The documentation of Realm classifies different types of changes according to their severance on the client and server side, respectively. In this article, I focus on a schema update that adds a required property to an existing object type.
After launching catalyst, a native iOS client for GitLab, I continued to fix bugs and develop new features. Eventually, this led me to an inevitable schema update that adds a new required property to an existing object type. Since this new property should be persisted in the database, the schema update would affect the client and the server side, i.e., the iOS app and Realm. Luckily, both of these changes are non-breaking according to the documentation.
Updating schema in Realm
As an example, I demonstrate the process of allowing users to pin a repository in catalyst to the list of repositories for faster access. We start by adding the required property isPinned
to the schema of Project
on the server side:
_13{_13 "title": "Project",_13 "bsonType": "object",_13 "required": ["isPinned"],_13 "properties": {_13 "_id": {_13 "bsonType": "uuid"_13 },_13 "isPinned": {_13 "bsonType": "bool"_13 }_13 }_13}
Since this modification is a non-breaking change, it does not require modifications on the client side, i.e., the iOS app’s source code. However, on the server side, we need to update existing documents. I choose functions to accomplish the update. In particular, I create two functions: one to update existing documents and another to update new documents. The latter is necessary since catalyst will not be aware of the new required property without an update. To update existing documents, I manually execute the following function once:
_13exports = async function () {_13 const projects = context.services_13 .get("catalyst-production")_13 .db("catalyst")_13 .collection("Project");_13_13 const userProjects = await projects.updateMany(_13 {},_13 { $set: { isPinned: false } }_13 );_13_13 return null;_13};
For newly created documents, I use a function combined with a database trigger that goes off on insertions, i.e., when a new document is created:
_17exports = async function (changeEvent) {_17 const documentID = changeEvent.documentKey._id;_17 const projects = context.services_17 .get("catalyst-production")_17 .db("catalyst")_17 .collection("Project");_17_17 const document = await projects.findOne({ _id: documentID });_17 if (document.isPinned === undefined) {_17 await projects.updateOne(_17 { _id: documentID },_17 { $set: { isPinned: false } }_17 );_17 }_17_17 return null;_17};
Updating schema in iOS
After taking care of the server side, I also need to update the client side to be able to use the isPinned
property. For this, I add the property to the object model and initialize the variable with a value of false
since we do not want projects to be pinned by default:
As opposed to the server-side change, the client requires a migration. It should be noted that the official tutorial for Migrating Your iOS App's Synced Realm Schema in Production claims that such a migration would not be necessary since “Realm can automatically update the local realm to include this new attribute and initialize it to false
”. However, doing so crashes catalyst and prompts me with the following error message:
RealmSwift/SwiftUI.swift:1481: Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=io.realm Code=10 "Migration is required due to the following errors:- Property 'Project.isPinned' has been added." UserInfo={NSLocalizedDescription=Migration is required due to the following errors:- Property 'Project.isPinned' has been added., Error Code=10}
This makes sense given that the documentation on Realm’s Swift SDK notes that “Realm Database does not automatically set values for new required properties. You must use a migration block to set default values for new required properties.” Although the migration itself happens automatically, you have to specify a new schemaVersion
in Realm’s default configuration, i.e., Realm.Configuration.defaultConfiguration
, at least.
With the client and server side updated, catalyst can now act on the isPinned
property and store the corresponding documents in Realm. Meanwhile, older versions of catalyst can still interact with Realm despite not being aware of the newly added required property.