Migrations with Synced Realms

One of the most common questions we get from mobile developers exploring the Realm Mobile Platform are regarding migrations. The exciting part is that it actually has a very simple and straight-forward approach:

  1. Schema changes are automatic - you don’t need to set the schema version or a migration block
  2. Schema changes are backwards compatible - old clients will continue to sync with newer ones
  3. Schema changes support adding or removing properties and classes

Let’s take a look at an example. Initially my app just has a single class, Dog and a string property:

class Dog: Object {
	dynamic var name = ""
}
class Dog : RealmObject() {
   var name = ""
}
public class Dog extends RealmObject {
    public String name = "";
    // getter / setter
}

After releasing my app, I now want to create an update that includes a new model, Person. To do so, I simply just add the class and associated properties:

class Dog: Object {
	dynamic var name = ""
	dynamic var owner: Person?
}

class Person: Object {
	dynamic var name = ""
	dynamic var birthdate: NSDate? = nil
}

let syncServerURL = URL(string: "realm://localhost:9080/Dogs")!
// No need to set schemaVersion or migrationBlock
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))

let realm = try! Realm(configuration: config)
class Dog : RealmObject() {
    var name = ""
    var owner: Person? = null
}

class Person : RealmObject() {
    var name = ""
    var birthdate: Date? = null
}

val syncServerURL = "realm://localhost:9080/Dogs"
val config = SyncConfiguration.Builder(user, syncServerURL).build()

val realm = Realm.getInstance(config)
public class Dog extends RealmObject {
  private String name = "";
  private Person owner;
  // getter / setter
}

public class Person extends RealmObject {
  private String name = "";
  private Date birthdate;
  // getter / setter
}

String syncServerURL = "realm://localhost:9080/Dogs"
SyncConfiguration config = new SyncConfiguration.Builder(user, syncServerURL).build();

Realm realm = Realm.getInstance(config);

Get more development news like this

It is that simple! Users on the old version will continue to sync with the latest version, but they will only see the Dog changes. Now what if we want to create a third version which removes the birthdate from the Person?

class Dog: Object {
	dynamic var name = ""
	dynamic var owner: Person?
}

class Person: Object {
	dynamic var name = ""
}

let syncServerURL = URL(string: "realm://localhost:9080/Dogs")!
// No need to set schemaVersion or migrationBlock
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))

let realm = try! Realm(configuration: config)
class Dog : RealmObject() {
  var name = ""
  var owner: Person? = null
}

class Person : RealmObject() {
  var name = ""
}

val syncServerURL = "realm://localhost:9080/Dogs"
// No need to set schemaVersion or migrationBlock
val config = SyncConfiguration.Builder(user, syncServerURL).build()

val realm = Realm.getInstance(config)
public class Dog extends RealmObject {
  private String name = "";
  private Person owner;
  // getter / setter
}

public class Person extends RealmObject {
  private String name = ""
  // getter / setter
}

String syncServerURL = "realm://localhost:9080/Dogs"
// No need to set schemaVersion or migrationBlock
SyncConfiguration config = new SyncConfiguration.Builder(user, syncServerURL).build();

Realm realm = Realm.getInstance(config);

Once again it’s just that simple! Internally, Realm will continue to maintain the birthdate column in its storage layer to maintain backwards compatibility with the older version. If a Person object is created by this third version, clients on the second version will simply see a nil value for birthdate. If this were a non-optional property, then the default value would be inserted instead.

This simple approach makes working with synchronized Realms easy across versions. The only caveat to keep in mind is that, while adding or removing properties or classes are automatically supported, there are a few schema changes that are not supported:

  • Changing a property’s type but keeping the same name
  • Changing a primary key
  • Changing a property from optional to required (or vice-versa)

These operations are not backwards compatible, so instead, simply create another synchronized Realm with the schema changes and if necessary copy the data from the old Realm to the new Realm:

class Dog: Object {
	dynamic var name = ""
	dynamic var owner: Person?
}

class Person: Object {
	dynamic var name = ""
}

class PersonV2: Object {
	dynamic var name: String? = nil
}

var syncServerURL = URL(string: "realm://localhost:9080/Dogs")!
var config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))
// Limit to initial object type
config.objectTypes: [Dog.self, Person.self]

let initialRealm = try! Realm(configuration: config)


syncServerURL = URL(string: "realm://localhost:9080/DogsV2")!
config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))
// Limit to new object type
config.objectTypes: [Dog.self, PersonV2.self]


let newRealm = try! Realm(configuration: config)
class Dog : RealmObject() {
    var name = ""
    var owner: Person? = null
}

class Person : RealmObject() {
    var name = ""
}

class PersonV2 : RealmObject() {
    var name = ""
}


@RealmModule(classes = arrayOf(Person::class, Dog::class))
class InitialModule

val syncServerURL = "realm://localhost:9080/Dogs"
val config = SyncConfiguration.Builder(user, syncServerURL)
            .modules(InitialModule())
            .build()
// Limit to initial object type
val initialRealm = Realm.getInstance(config)


@RealmModule(classes = arrayOf(Person::class, DogV2::class))
class NewModule

val syncServerURL = "realm://localhost:9080/DogsV2"
val config = SyncConfiguration.Builder(user, syncServerURL)
            .modules(InitialModule())
            .build()
// Limit to new object type
val newRealm = Realm.getInstance(config)
public class Dog extends RealmObject {
  private String name = "";
  private Person owner;
  // getter / setter
}

public class Person extends RealmObject {
  private String name = "";
  // getter / setter
}

public class PersonV2 extends RealmObject {
  private String name = "";
  // getter / setter
}

@RealmModule(classes = { Person.class, Dog.class }) class InitialModule {}

String syncServerURL = "realm://localhost:9080/Dogs"
SyncConfiguration config = new SyncConfiguration.Builder(user, syncServerURL)
              .modules(new InitialModule())
              .build();
// Limit to initial object type
Realm initialRealm = Realm.getInstance(config);



@RealmModule(classes = { Person.class, DogV2.class }) class NewModule {}

String syncServerURL = "realm://localhost:9080/Dogs"
SyncConfiguration config = new SyncConfiguration.Builder(user, syncServerURL)
              .modules(new NewModule())
              .build();
// Limit to new object type
Realm initialRealm = Realm.getInstance(config);

Next Up: Getting Started with the Realm Mobile Platform - Guided Demo

General link arrow white

Adam Fish

Adam is the Director of Product at Realm, where he manages the product development for currently supported mobile platforms and upcoming new products. He has a strong background in entrepreneurship and software development, having previously co-founded Roobiq, a mobile-first sales productivity app used by sales teams worldwide.