Navigation

Modify a Synced Object Schema

On this page

  • Overview
  • Additive Changes
  • Destructive Changes
  • Procedure
  • Summary

When developing an application using Realm Sync, you may want to make changes to your schema at some point. Realm offers an API for making backward-compatible schema changes to synced realms, allowing old clients to sync with newer ones.

Note

This page details how to modify a schema when working with Realm Sync. If you are developing your application without Realm Sync, refer to the modify an object schema page for your SDK:

Additive changes, such as adding a class or adding a field to a class, are applied automatically to synced realms, meaning you can alter the schema with no additional configuration.

Note
Removing a Property from Your Schema Is Considered an Additive Change.

To maintain backward compatibility, removing a field from a schema doesn’t delete the field from the database and instead instructs Realm to ignore that field. New objects retain the removed field, but Realm automatically sets the field's value to null. Realm sets fields that are non-nullable to an appropriate empty value, such as a 0 for integer values or an empty string for string values.

Destructive changes to a schema are usually modifications of existing fields, such as:

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

Synchronized realms only support additive changes to a schema. Therefore, attempting to perform a destructive change on a synchronized realm will lead to errors like the following:

{
message: 'The following changes cannot be made in additive-only schema mode:\n' +
"- Property 'Task._id' has been changed from 'int' to 'string'.",
errorCode: 1
}

If you are developing an application using Realm Sync and require destructive schema changes, you can create a "partner collection".

A partner collection is a second collection that contains the same data as another collection but with a schema containing the required changes. Partner collections use database triggers to ensure that data flows in both directions, meaning that when one collection is written to, the other is also written to.

1

If you need to add or remove a field to a schema, you need to perform an additive change. You can do this by changing your schema directly, without any additional configuration.

If you want to modify an existing schema field, you need to perform a destructive change. The following steps detail how to perform a destructive change.

In the following example, the initial collection uses the JSON Schema below for a Task collection. The schema for the Task contains an _id field of int type and a name field of string type.

Task Schema
{
"title": "Task",
"bsonType": "object",
"required": [
"_id",
"name"
],
"properties": {
"_id": {
"bsonType": "int"
},
"_partition": {
"bsonType": "string"
},
"name": {
"bsonType": "string"
}
}
}
2

Since destructive changes cannot be performed directly on a synced object schema, you can create a partner collection with a schema containing the required changes. The partner collections should always have the same data to ensure that newer clients can synchronize with older clients.

To copy the data from your initial collection to your partner collection, perform an aggregation using the db.collection.aggregate() method with the Mongo shell.

Match all the documents in the initial collection by passing an empty filter to the $match operator.

Modify the desired fields of the initial collection by using an aggregation pipeline operator. In the following example, the data is transformed using the $addFields operator. The _id field is transformed to a string type with the $toString operator.

Finally, write the transformed data to the partner collection by using the $out operator and specifying the partner collection name. In this example, we wrote the data to a new collection named TaskV2.

Match All Documents in the Initial Collection and Output Them to the Partner Collection
use "<database-name>" // switch the current db to the db that the Task collection is stored in
collection = db.Task;
collection.aggregate([
{ $match: {} }, // match all documents in the Task collection
{
$addFields: { // transform the data
_id: { $toString: "$_id" }, // change the _id field of the data to a string type
},
},
{ $out: "TaskV2" }, // output the data to a partner collection, TaskV2
]);

The partner collection, TaskV2, has a JSON Schema that looks like the following:

Task Schema
{
"title": "TaskV2",
"bsonType": "object",
"required": [
"_id",
"name"
],
"properties": {
"_id": {
"bsonType": "string"
},
"_partition": {
"bsonType": "string"
},
"name": {
"bsonType": "string"
}
}
}
3

Once your partner collection is set up, you can use it to read existing data. However, any new writes to the data of either collection will not be shown on its partner. This will cause the old clients to be out of sync with the new clients that are using the new collection with the modified schema.

To ensure that data is reflected in both collections, you need to set up a database trigger for each respective collection. When data is written to one collection, the trigger's function should perform the write to the partner collection.

Follow the steps in the database trigger documentation to create a trigger to copy data from the Task collection to the TaskV2 collection. The trigger should fire for all operation types.

Repeat these steps and create a second trigger to copy data from the TaskV2 collection to the Task collection.

Create a Database Trigger to Copy Data from Task to TaskV2
click to enlarge
4

A forward migration trigger listens for inserts, updates, and deletes in the initial collection, modifies them to reflect the partner collection's schema, and then applies them to the partner collection.

The forward migration function follows an Extract, transform, load (ETL) procedure. The function performs an aggregation that extracts data from the initial collection, transforms the data using an aggregation pipeline operator, and loads the data into the partner collection.

In the following example, if the operation type is delete, meaning a document has been deleted in one collection, the document is also deleted in the other collection. If the operation type is a write event, a pipeline is created following the ETL procedure. The inserted or modified document in the initial collection is extracted using the $match operator. The extracted document is then transformed from an integer value to a string value to adhere to TaskV2's schema. Finally, the transformed data is loaded into TaskV2, by using the $merge operator.

copyTaskObjectToTaskV2 trigger
exports = function(changeEvent) {
const db = context.services.get("mongodb-atlas").db("ExampleDB");
const collection = db.collection("Task");
const changedDocId = changeEvent.documentKey._id; // the changed document's _id as an integer
// if a document in the Task collection has been deleted, delete the adjacent object in the TaskV2 collection
if(changeEvent.operationType === "delete") {
const tasksV2Collection = db.collection("TaskV2");
const deletedDocumentID = changedDocId.toString(); // get the deleted document's _id as a string value since TaskV2's _id are queried as a string
return tasksV2Collection.deleteOne({ _id: deletedDocumentID })
}
// if a document in the Task collection has been created, modified, or replaced, do the same to the adjacent object in the TaskV2 collection
const pipeline = [
// extract the changed document data from the Task collection
{ $match: { _id: changeEvent.documentKey._id } },
{
// transform the document, by altering the _id field
$addFields: {
_id: { $toString: "$_id" }, // change the _id field to a string type, since TaskV2 stores _id as a string
},
},
{ $merge: "TaskV2" } // insert the document into TaskV2, using the $merge operator to avoid overwriting the existing data in TaskV2
]
return collection.aggregate(pipeline);
};
5

To listen for changes to the second collection and apply them to the initial collection, write a reverse migration function for your second collection's trigger. The reverse migration follows the same idea as the previous step, but this time inserts data from the second collection to the initial collection.

The following example goes through similar steps as the example in the prior step. If a document has been deleted in one collection, the document is also deleted in the other collection. If the operation type is a write event, the changed document from TaskV2 is extracted, transformed from a string value to an integer value, and loaded into the Task collection.

copyTaskV2ObjectToTask trigger
exports = function(changeEvent) {
const db = context.services.get("mongodb-atlas").db("ExampleDB");
const collection = db.collection("TaskV2");
const changedDocId = changeEvent.documentKey._id; // the changed document's _id as a string
// if a document in the TaskV2 collection has been deleted, delete the adjacent object in the Task collection
if(changeEvent.operationType === "delete") {
const taskCollection = db.collection("Task");
const deletedDocumentID = parseInt(changedDocId); // get the deleted document's _id as an integer value since Task's _id are queried as an integer
return taskCollection.deleteOne({ _id: deletedDocumentID})
}
// if a document in the TaskV2 collection has been created, modified, or replaced, do the same to the adjacent object in the Task collection
const pipeline = [
// extract the changed document data from the TaskV2 collection
{ $match: { _id: changedDocId } },
{
// transform the document, by altering the _id field
$addFields: {
_id: { $toInt: "$_id" }, // change the _id field to an integer type, since Task stores _id as an integer
},
},
{ $merge: "Task" } // insert the document into Task, using the $merge operator to avoid overwriting the existing data in Task
]
return collection.aggregate(pipeline);
};
  • Schema changes on synced realms are backward compatible, allowing old clients to sync with newer ones.
  • Additive changes to the schema of a synced realm do not require any additional configuration.
  • Synchronized realms only support additive changes to a schema.
  • Destructive changes are modifications to existing fields of a schema.
  • Synchronized realms do not support destructive changes directly.
  • To perform destructive schema changes to a synced realm, create a partner collection with the necessary schema changes and manually copy the data from the first collection to the second collection.
  • To keep partner collections up-to-date with each other, set up database triggers to copy changed data from one collection to its partner.
Give Feedback

On this page

  • Overview
  • Additive Changes
  • Destructive Changes
  • Procedure
  • Summary