iosrealmrealm-migration

Realm migration causing intermittent crashes for some users


I needed to add a migration to Realm, a framework I had virtually no experience working with. After searching on SO and elsewhere, I found what I believe to be a proper way to do it. What I needed was to introduce a primaryKey to one object, and based on the existing code and what I found on SO, I added a single migration. Also, in addition to just force unwrapping everything, I added tests to insure that the oldObject was there, and its property was in fact a strong. In hindsight, I see one other test I might have made and I'll append that later on.

A new release with this code is now under phased release in the App Store, and about half of our users have gotten it, several hundred.

A small number of users are getting crashes in the Realm migration block. Our QA tested several cases of migrating with dirty or clean Stuff2 objects, and nothing odd ever happened. But we always started with the App version just prior to the latest, and not some ancient version.

Below is the latest code, and migration 34 is my addition.

private let latestRealmVersion: UInt64 = 34

var config = Realm.Configuration(
    schemaVersion: latestRealmVersion,
    migrationBlock: { migration, oldSchemaVersion in
        if (oldSchemaVersion < 2) {
            migration.enumerateObjects(ofType: String(describing: Stuff0.self)) { oldObject, newObject in
                newObject!["someTag"] = ""
            }
        }

        ...

        if oldSchemaVersion < 33 {
            migration.enumerateObjects(ofType: String(describing: Stuff1.self)) { _, newObject in
                newObject!["anotherTab"] = ""
            }
        }

        // My Change
        if oldSchemaVersion < 34 {
            migration.enumerateObjects(ofType: String(describing: Stuff2.self)) { oldObject, newObject in
                if let old  = oldObject?["primaryKeyTag"] as? String {
                    newObject!["primaryKeyTag"] = old
                } else {
                    newObject!["primaryKeyTag"] = "nil"
                }
                //Stuff2 updated with a primary key
            }
        }

config.deleteRealmIfMigrationNeeded = false
Realm.Configuration.defaultConfiguration = config

The Stuff2 object was modified with a primaryKey() addition:

// Older Object
class Stuff2: Object {
    @objc dynamic var primaryKeyTag = ""
    @objc dynamic var localID = ""
    ...
}

// New Object
class Stuff2: Object {
    @objc dynamic var primaryKeyTag = ""
    @objc dynamic var localID = ""
    ...

    override static func primaryKey() -> String? {
        return "localID"
    }
}

My take on this:

  1. My migration should have looked for an empty string:

    if let old = oldObject?["primaryKeyTag"] as? String, !old.isEmpty { ... }

  2. Between some much older version and now, the Stuff2 object changed, and that somehow that's causing a crash now. I can see an older migration block that uses an incorrect migration type, ie migration.enumerateObjects(ofType: String(describing: SoundsLikeStuff2.self)), which probably never did anything.

I could obvioously really use some comments or suggestions from anyone who really knows Realm.


Solution

  • I may be mis-reading the question so please add a comment if I miss the mark and I will update the answer.

    It appears you have a Realm object with no defined primary key

    class Stuff2: Object {
        @objc dynamic var primaryKeyTag = ""
        @objc dynamic var localID = ""
    }
    

    and then at some point a primary key was needed and you want to make the localID property that primary key, so you added the primaryKey function to the object (that will trigger a migration)

    class Stuff2: Object {
        @objc dynamic var primaryKeyTag = ""
        @objc dynamic var localID = ""
    
        override static func primaryKey() -> String? {
            return "localID"
        }
    }
    

    So the only thing needed in that case is to take the old localID value and assign it to the new localID property (which is now the primary key)

    let vers = UInt64(1)
    let config = Realm.Configuration( schemaVersion: vers, migrationBlock: { migration, oldSchemaVersion in
         if (oldSchemaVersion < vers) {
            migration.enumerateObjects(ofType: TestClass.className()) { oldItem, newItem in
    
                let oldVar = oldItem!["localID"] as! String
                newItem!["localID"] = oldVar
             }
         }
     })
    

    There are two getcha's; primary keys cannot be nil and must be unique so there should be some error checking to ensure that's enforced.

    Well, technically one objects primary key could be nil, but again, to enforce uniqueness it would be the only one.