Syncing Related Records
Supported Relationship Types
Related record sync supports two types of one-to-many relationships: Lookup and Master-detail. Both types are defined in the child-to-parent, many-to-one direction. Each child knows its parent, but the parent doesn’t know its children.
Lookup Relationships- Child records don’t require a parent field.
- Changes to one object don’t affect the security, access, or deletion of a linked object.
- Child records require a parent field.
- The parent’s access level determines the access level of its children.
- If a parent record is deleted, its children are also deleted.
Salesforce supports up to two master-detail fields per object, and up to three levels of master-detail relationships.
See Object Relationships Overview in Salesforce Help.
Objects Used in Related Record Sync
Related record sync uses two special types of sync targets:
- Parent-children sync up target
-
- iOS: SFParentChildrenSyncUpTarget
- Android: ParentChildrenSyncUpTarget
- Handles locally created or updated records and deleted records.
- Parent-children sync down target
-
- iOS: SFParentChildrenSyncDownTarget
- Android: ParentChildrenSyncDownTarget
- Supports resync and cleanResyncGhosts methods.
These targets support leave-if-changed and overwrite merge modes. Each target provides a factory method (on iOS) or a constructor (on Android) that initializes an instance with the required information you provide. To perform the sync, you configure a new target class instance and pass it to the standard Mobile Sync sync method that accepts a target object.
- Parent information object
-
-
iOS:
- Swift
- ParentInfo
- Objective-C
- SFParentInfo
- Android: ParentInfo
-
iOS:
- Includes:
- Object type
- Soup name
- ID field name (Optional in all cases. Defaults to “Id”. Set this value only if you’re specifying a different field to identify the records.)
- Last Modified Date field name (Optional in all cases. Defaults to “LastModifiedDate”. Set this value only if you’re specifying a different field for timestamps.)
- External ID field name (Optional in all cases. If provided, and if a locally created parent or child record specifies a value for it, Mobile Sync performs an upsert instead of create).
- Child information object
-
- iOS: ChildrenInfo
- Android: ChildrenInfo
- Includes:
- Object type
- Soup name
- Parent ID field name
- ID field name (Optional in all cases. Defaults to “Id”. Set this value only if you’re specifying a different field to identify the records.)
- Last Modified Date field name (Optional in all cases. Defaults to “LastModifiedDate”. Set this value only if you’re specifying a different field for timestamps.)
- External ID field name (Optional in all cases. If provided, and if a locally created parent or child record specifies a value for it, Mobile Sync performs an upsert instead of create).
Preparing Your SmartStore Data Model
To prepare for handling related objects offline, you first set up a SmartStore soup for each expected parent and child object type. For each soup, add indexed ID fields that model the server-side relationships. Here’s the list of required indexed fields:
- Soup for a parent object:
- Field for server ID of record
- Soup for a child object:
- Field for server ID of record
- Field for server ID of parent record
- When you sync down related records, you are targeting parent records, and the sync downloads those records and all their children. For example, if you’re syncing accounts with their contacts, you get the contacts linked to the accounts you’ve downloaded. You don’t get contacts that aren’t linked to those accounts.
- When you sync up related records, Mobile Sync iterates over the soup of parent records and picks up related children records. Modified child records that aren’t related to a parent record are ignored during that sync up operation.
Sync Up
To initialize the parent-child sync-up target, you provide parent information objects and child information objects. These objects fully describe the relationships between parent-child records, both on the server and in the local store. You also provide the list of fields to sync. Here’s the full list of required information:
- Parent information object
- Child information object
- Relationship type (for example, master-detail or lookup)
- Fields to sync up in parent and children soups
The sync up operation iterates over the soup containing the parent records and uses the given information to pull related records from the children’s soups. A record is considered dirty when the __local__ field is set to true. A record tree—consisting of one parent and its children—is a candidate for sync up when any record in the tree is dirty. Whether the sync up actually occurs depends on how Mobile Sync handles the merge mode.
“Leave-if-changed” Merge Mode Handling
Mobile Sync fetches Last Modified Date fields of the target parent and children server records. The sync up operation skips that record tree if
- the last modified date of any fetched server record is more recent than its
corresponding local record
or
- if any fetched server record has been deleted.
Updates Applied after Sync Up
After the local changes have been synced to the server, Mobile Sync cleans up related records.
- If sync up creates any parent or child record on the server, Mobile Sync updates the server ID field of the corresponding local record. If the created record is a parent, the parent ID fields of its children are also updated in the local store.
- If any server records were deleted since the last sync down, Mobile Sync deletes the corresponding local records.
- If sync up deletes a parent record and the relationship type is master-detail, Mobile Sync deletes the record’s children on the server and in the local store.
Sync Down
To initialize the parent-children sync-down target, you provide parent information objects and children information objects. These objects fully describe the relationships between parent-child records, both on the server and in the local store. You also provide a list of fields to sync and some SOQL filters.
The new sync down target is actually a subclass of the SOQL sync down target. Instead of being given the SOQL query, however, the target generates it from the parent and children information objects, list of fields, and SOQL filters.
Information Passed to Sync Down Targets
- Parent information object
- Child information object
- Relationship type (for example, master-detail or lookup)
- Fields to sync down in parent and children soups
- SOQL filter to apply in query on root records during sync down—for example, the condition of a WHERE clause
Server Call
Mobile Sync fetches the record trees, each consisting of one parent and its children, using SOQL. It then separates parents from their children and stores them in their respective soups.
“Leave-if-changed” Merge Mode Handling
Local record trees that contain any dirty records—locally created, modified, or deleted records—are left unaltered. For example, if a parent record has one dirty child, Mobile Sync doesn’t update the parent or the child. This rule applies even if the parent is clean locally but has been changed on the server.
Handling Resync
During resync, Mobile Sync adjusts the SOQL query to download only those record trees in which the parent changed since the last sync.
Implementing Related Record Sync
After you understand the principles and requirements involved, implementing it is straightforward. You can add related record sync to your code in a few steps. The following code snippets demonstrate the technique using Account (parent) and Contact (child) objects.
- Create a userstore.json file that defines SmartStore soups with
the required indexed fields.
1{ 2 "soups": [ 3 { 4 "soupName": "ContactSoup", 5 "indexes": [ 6 { "path": "Id", "type": "string"}, 7 { "path": "LastName", "type": "string"}, 8 { "path": "AccountId", "type": "string"}, 9 { "path": "__local__", "type": "string"}, 10 { "path": "__locally_created__", "type": "string"}, 11 { "path": "__locally_updated__", "type": "string"}, 12 { "path": "__locally_deleted__", "type": "string"} 13 ] 14 }, 15 { 16 "soupName": "AccountSoup", 17 "indexes": [ 18 { "path": "Id", "type": "string"}, 19 { "path": "Name", "type": "string"}, 20 { "path": "Description", "type": "string"}, 21 { "path": "__local__", "type": "string"}, 22 { "path": "__locally_created__", "type": "string"}, 23 { "path": "__locally_updated__", "type": "string"}, 24 { "path": "__locally_deleted__", "type": "string"} 25 ] 26 } 27 ] 28} - Create a usersyncs.json file that defines two named syncs. In
this example, ”DownSync” and “UpSync” configurations model the parent-child relationship
between Account and Contact objects.
1{ 2 "syncs": [{ 3 "syncName": "DownSync", 4 "syncType": "syncDown", 5 "soupName": "AccountSoup", 6 "target": { 7 "iOSImpl": "SFParentChildrenSyncDownTarget", 8 "AndroidImpl": "ParentChildrenSyncDownTarget", 9 "parent": { 10 "idFieldName": "Id", 11 "sobjectType": "Account", 12 "modificationDateFieldName": "LastModifiedDate", 13 "soupName": "AccountSoup" 14 }, 15 "parentFieldlist": [ 16 "Id", 17 "Name", 18 "Description" 19 ], 20 "children": { 21 "parentIdFieldName": "AccountId", 22 "idFieldName": "Id", 23 "sobjectType": "Contact", 24 "modificationDateFieldName": "LastModifiedDate", 25 "soupName": "ContactSoup", 26 "sobjectTypePlural": "Contacts" 27 }, 28 "childrenFieldlist": [ 29 "LastName", 30 "AccountId" 31 ], 32 "relationshipType": "MASTER_DETAIL", 33 "parentSoqlFilter": "Name LIKE 'A%' ", 34 "type": "parent_children", 35 "idFieldName": "Id" 36 }, 37 "options": {"mergeMode": "OVERWRITE"} 38 }, 39 40 { 41 "syncName": "UpSync", 42 "syncType": "syncUp", 43 "soupName": "AccountSoup", 44 "target": { 45 "iOSImpl": "SFParentChildrenSyncUpTarget", 46 "AndroidImpl": "ParentChildrenSyncUpTarget", 47 "childrenCreateFieldlist": [ 48 "LastName", 49 "AccountId" 50 ], 51 "parentCreateFieldlist": [ 52 "Id", 53 "Name", 54 "Description" 55 ], 56 "childrenUpdateFieldlist": [ 57 "LastName", 58 "AccountId" 59 ], 60 "parentUpdateFieldlist": [ 61 "Name", 62 "Description" 63 ], 64 "parent": { 65 "idFieldName": "Id", 66 "sobjectType": "Account", 67 "modificationDateFieldName": "LastModifiedDate", 68 "soupName": "AccountSoup" 69 }, 70 "relationshipType": "MASTER_DETAIL", 71 "type": "rest", 72 "modificationDateFieldName": "LastModifiedDate", 73 "children": { 74 "parentIdFieldName": "AccountId", 75 "idFieldName": "Id", 76 "sobjectType": "Contact", 77 "modificationDateFieldName": "LastModifiedDate", 78 "soupName": "ContactSoup", 79 "sobjectTypePlural": "Contacts" 80 }, 81 "parentUpdateFieldlist": [ 82 "Name", 83 "Description" 84 ], 85 "idFieldName": "Id" 86 }, 87 "options": {"mergeMode":"LEAVE_IF_CHANGED"} 88 } 89 ] 90}
The following steps demonstrate how to use these configurations to synchronize related Account (parent) and Contact (child) records.
- In your Xcode project under , add the userstore.json and usersyncs.json files to your Xcode target.
- Load the store and sync
configurations. Call these loaders after Mobile SDK is
initialized and before you call any SmartStore or Mobile Sync methods. For
example, in your AppDelegate class, call these
methods in the block you pass to loginIfRequired.
Don’t call either of these methods more than once.
- Swift
-
1func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 2 self.window = UIWindow(frame: UIScreen.main.bounds) 3 self.initializeAppViewState() 4 AuthHelper.loginIfRequired { 5 MobileSyncSDKManager.shared.setupUserStoreFromDefaultConfig() 6 MobileSyncSDKManager.shared.setupUserSyncsFromDefaultConfig() 7 self.setupRootViewController() 8 } 9... 10 return true 11} - Objective-C
-
1- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 2{ 3 self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 4 [self initializeAppViewState]; 5 [SFSDKAuthHelper loginIfRequired:^{ 6 [self setupRootViewController]; 7 [[MobileSyncSDKManager sharedManager] setupUserStoreFromDefaultConfig]; 8 [[MobileSyncSDKManager sharedManager] setupUserSyncsFromDefaultConfig]; 9 }]; 10... 11 return YES; 12}
- Call a Mobile Sync
reSync method that takes a sync name. By passing
sync names from your configuration file, you can use the reSync(named:update:) method for every sync up and sync down operation.
- Swift
-
1self.syncMgr.reSync(named: kSyncName) { [weak self] (syncState) in 2 // Handle updates 3} - Objective-C
-
1[self.syncMgr reSyncByName:kSyncUpName updateBlock:^(SFSyncState* sync) { 2 // Handle updates 3}
- Place the userstore.json and usersyncs.json files in your project’s res/raw folder.
- Call the Mobile Sync
sync method that takes a sync name. By passing sync names from your configuration file,
you can use the reSync(named:update:) method for
every sync up and sync down operation.For sync up and sync down:
1//Resync a predefined sync by its name 2 3syncManager.reSync(syncName, 4 new SyncUpdateCallback() { 5 @Override 6 public void onUpdate(SyncState sync) { 7 // Handle updates 8 } 9 });