realm / realm-kotlin

Kotlin Multiplatform and Android SDK for the Realm Mobile Database: Build Better Apps Faster.
Apache License 2.0
963 stars 61 forks source link

Add documentation about migrations to website #992

Open tarasmorskyi-mediamonks opened 2 years ago

tarasmorskyi-mediamonks commented 2 years ago

As AutomaticSchemaMigration is available for data migration from old schema to new one there is still no documentation for it.

It's possible to understand what to do with source code comments but I'm not fully sure if I'm doing it correctly as current result looks like super complicated and hard to read.

~Main issue there is handling DynamicRealmObjects, first for reading as I have to get child DynamicRealmObjectImpl instead of proper class and then extending DynamicRealmObject which creates lots of noise in class for inserting into new realm version.~ Haven't noticed comment for migration about data been kept if only new fields were added and nothing else which means I don't need to do anything there.

rorbech commented 2 years ago

Hi @tarasmorskyi-mediamonks. Thanks for the feedback, we will try to improve the docs. Here is a small sample snippet to assist grasping and combining the API/concepts in the meantime

val config = RealmConfiguration.Builder(
    schema = setOf(ModelClass::class)
)
.migration(AutomaticSchemaMigration {

    val oldRealm = it.oldRealm
    val newRealm = it.newRealm

    // Access old objects with the string based API as DynamicRealmObjects
    val oldObjects = oldRealm.query("ModelClass").find()
    for (oldObject in oldObjects) {
        val fieldValue: String = oldObject.getValue("fieldName", String::class)
        val child: DynamicRealmObject? = oldObject.getObject("childObjects")
    }

    // Access migrated objects as mutable objects in the migrated realm. Unmodified schema properties will be left as is
    val oldObjectInMigratedRealm: DynamicMutableRealmObject? =
        newRealm.findLatest(oldObjects[0])
    oldObjectInMigratedRealm?.let {
        it.set("fieldName", "new field value")
    }

    // Fast iteration of all objects in the old Realm
    it.enumerate("ModelClass") { oldObject: DynamicRealmObject, newObject: DynamicMutableRealmObject? ->
        // Some common use cases are highlighted at
        // https://www.mongodb.com/docs/realm-sdks/kotlin/1.0.2/library-base/-realm%20-kotlin%20-s-d-k/io.realm.kotlin.migration/-automatic-schema-migration/-migration-context/enumerate.html
    }

    // Creating of new objects from scratch in the migrated realm
    val scratchObjectInMigratedRealm = newRealm.copyToRealm(
        DynamicMutableRealmObject.create("ModelClass", mapOf("fieldName" to "scratch value"))
    )
}

Feel free to drop additional questions here, so that we can try to cover as much when updating the docs

brnmr commented 7 months ago

Thanks @rorbech for the provided examples. What is still not clear to me is how to transfer migrations from the java-sdk to the kotlin-sdk. We want to migrate to the kotlin-sdk and we are having hard times figuring out what is the correct way to do it. This so far is preventing us from giving it a go.

We have a bunch of migration blocks in the java-sdk variant like this:

object RealmMigrationHelper {

    // PUBLIC ⤵

    /**
     * Returns [RealmMigration] object that manages all migrations based on the old and new schema versions
     * You must add new if block everytime changes to the Realm classes are made
     * @return [RealmMigration]
     */
    fun migrate(): RealmMigration {

        return RealmMigration { realm, oldVersion, newVersion ->
            var version = oldVersion
            val schema: RealmSchema = realm.schema

            // Changes from schema version 0 to 1:
            if (version == 0L) {
                migrate0to1(schema)

                version++
            }

            // Changes from schema version 1 to 2:
            if (version == 1L) {
                migrate1to2(schema)

                version++
            }

            if (version == 2L) {
                migrate2to3(schema)

                version++
            }

            if (version == 3L) {
                migrate3to4(schema)

                version++
            }

            if (version == 4L) {
                migrate4to5(schema)

                version++
            }

            if (version == 5L) {
                migrate5to6(schema)

                version++
            }

            if (version == 6L) {
                migrate6to7(schema)

                version++
            }

            if (version == 7L) {
                migrate7to8(schema)

                version++
            }

            if (version == 8L) {
                migrate8to9(schema)

                version++
            }

            if (version == 9L) {
                migrate9to10(schema)

                version++
            }

            if (version == 10L) {
                migrate10to11(schema)

                version++
            }

            if (version == 11L) {
                migrate11to12(schema)

                version++
            }

            if (version == 12L) {
                migrate12to13(schema)

                version++
            }

            if (version == 13L) {
                migrate13to14(schema)

                version++
            }
        }
    }

    // PRIVATE ⤵

    /**
     * Migrates the database from version 0 to 1
     */
    private fun migrate0to1(schema: RealmSchema) {
        schema.get(RealmEntityLinks.TAG)
            ?.addField("name", String::class.javaObjectType)

        schema.get(RealmEntityFeatures.TAG)
            ?.addField("name", Boolean::class.javaObjectType)
    }

    /**
     * Migrates the database from version 2 to 3
     */
    private fun migrate1to2(schema: RealmSchema) {
        val detailsSchema = schema.get("RealmEntityResultDetails")
        val performerSchema = schema.get(RealmEntityPerformer.TAG)

        detailsSchema?.let { unwrappedDetailsSchema ->
            performerSchema?.let { unwrappedPerformerSchema ->
                unwrappedDetailsSchema
                    .removeField("performer")
                    .addRealmObjectField("performer", unwrappedPerformerSchema)
            }
        }
    }

    // And so on... until migrate13to14() function

    /**
     * Migrates the database from version 13 to 14
     * - Add field "linkingDate" to class [RealmEntityLinkedProfile]
     * - Add field "gender" to class [RealmEntityLinkedProfile]
     */
    private fun migrate13to14(schema: RealmSchema) {
        schema.get(RealmEntityLinkedProfile.TAG)
            ?.addField("linkingDate", String::class.javaObjectType)
            ?.addField("gender", Int::class.javaObjectType)
    }
}

Let's say we now updated all dependencies to reflect the ones required for having the kotlin-sdk up and running. How would the migration helper look like in the kotlin-sdk variant? In addition, should we increment the schema version when updating from java-sdk to kotlin-sdk? This also not clear.

Thanks in advance for your help.

oleksandrpriadko commented 2 months ago

@rorbech Hey Claus, I hit the same problem as @brnmr mentioned. I decided to upgrade my old app from 6.0.2 to the latest version but 6.0.2 is not available anymore. Can you please advice how to upgrade from 6.0.2 to 10.x.x without losing user data? Thanks.

blackwiz4rd commented 1 month ago

Tied to https://github.com/realm/realm-kotlin/issues/1619, I think also this part needs attention: creating a new field in already existing DynamicRealmObject:

It's unclear from the docs if it's possible to obtain the list of property names of DynamicRealmObject. The only workaround I have found is to check for the value and catch the exception:

try {
    oldObject.getValue("autoprint", Boolean::class)
} catch (e: IllegalArgumentException) {
    newObject?.set("autoprint", true)
}