How to reconnect two records that where connected with S2S connection, and it was deleted? Or, for example, we have two records in two org, how to connect them with S2S without creation new ones? This is quick quid how to do it.
We have two orgs: Org1 and Org2, and two Accounts: "Salesforce.com Inc." from Org1 and "Salesforce.com Inc." from Org 2. We need to synchronize with S2S connection. We will share from Org1 to Org2.
Going to Org2 "Connections" tab:
Selecting needed connection;
Press "Subscribe/Unsubscribe":
Uncheck "Auto-Accept Accounts":
Going to Org1 to needed Account and share it:
Going to Org2 Account tab;
In the bottom there is block "Accounts from Connections", select "All" and press "Go":
You will see records list that were forwarded but not accepted:
Press "Accept" near Account record sent from Org1;
Go to "Create New or Link to Existing One" and select "Use Existing":
Search you existing Account and press "Save".
Now Account from Org1 is synchronized with Account from Org2.
P.S.: Do not forget to go back to "Connection" tab in Org2 and check again "Auto-Accept" Accounts.
Standard try/catch functionality in Salesforce include methods that allow you to add errors to appropriate objects in lists that was used in DML operation. Why it is so important to know what object failed and why? To answer this question lets look what happens if list of objects are inserted, for example ten Projects. If one Projects fails due to some error, validation rule, required field is not filled in or something else, all list will not be inserted. It means that valid Projects too. Another example, group of Opportunities are inserted, and trigger fires on after insert creates Project for each one. And the same thing - we have different errors on insert of several Projects. What to do with this errors? We need to show each one on appropriate Opportunity. I this cases try/catch has methods that allow us to find this failed objects and to track which object failed due to which error. Let's consider the following two cases:
Insert of Project from Opportunity trigger.
Update of Project from Opportunity trigger.
They are different because in first one Project fails on insert, and it means it do not have Id. So to identify what what Project or Projects fail we will use DMLException methods such as getNumDml and getDmlIndex. getNumDml() will return us number of failed objects and getDmlIndex() - row number of failed one. Lets look into simple trigger:
trigger OpportunityUpdateProjectTrigger on Opportunity ( after insert ) {
// Create List of Projects that we will insert.
List<Project__c> createProjectList = new List<Project__c>();
// Going through all Opportunities and creating Project for each one.
for( Opportunity opportunityItem : trigger.new )
createProjectList.add( new Project__c( Name = opportunityItem.Name,
Opportunity__c = opportunityItem.Id ) );
if ( !createProjectList.isEmpty() ){
try{
// Trying to insert Projects.
insert createProjectList;
} catch( DMLException dmlEx ){
// Going through all errors.
for( Integer i = 0; i < dmlEx.getNumDml(); i++ ){
String errorMessage = 'Error on creation of Project: ' + dmlEx.getDmlMessage( i );
// Attaching error to appropriate Opportunity from Trigger.newMap.
trigger.newMap.get( createProjectList[ dmlEx.getDmlIndex( i ) ].Opportunity__c ).addError(errorMessage);
}
}
}
}
Lets look what we can use in second case, where we have try/catch on update. Here we have update of Project on update of Opportunity. Trigger on Opportunity after update will query from database Projects that are related to this Opportunities, make some changes and update them. On update of Projects can be errors that we need to display on appropriate Opportunity, like in first case. But here we have Project Id. For this we will use DMLException method getDmlId, that returns Id of failed objects. We can use getDmlIndex, like previously. But in this case it would be better to use DMLException method getDmlId() will return Id of failed object. Lets look in to the code:
trigger OpportunityUpdateProjectTrigger on Opportunity ( after update ) {
// Create Map of Projects that we will updated.
Map<id,Project__c> projectsToUpdateMap =
new Map<id,Project__c>( [SELECT Id, Opportunity__c
FROM Project__c
WHERE Opportunity__c IN : trigger.newMap.keySet() ] );
/*
Here we have some updates on projects.
*/
if( !projectsToUpdateMap.keySet().isEmpty() ){
try{
// Trying to update Projects.
update projectsToUpdateMap.values();
} catch( DMLException dmlEx ){
for( Integer i = 0; i < dmlEx.getNumDml(); i++ ){
String errorMessage = 'Error creation of Project: ' + dmlEx.getDmlMessage( i );
// Attaching error to appropriate Opportunity from Trigger.newMap.
trigger.newMap.get( projectsToUpdateMap.get( dmlEx.getDmlId( i ) ).Opportunity__c ).addError( errorMessage );
}
}
}
}
So now we know we know how to add errors to failed one, and can make one more thing. In both cases valid Projects were not inserted, and as they didn't get errors we can try insert them in catch. For this we need to create new list of Projects that will include only valid ones and insert it.
...
try{
insert createProjectList;
} catch( DMLException dmlEx ){
// Crete new list of Projects to insert and fill it from old one
List<Project__c> validProjectsToInsert = createProjectList;
// Going through all errors.
for( Integer i = 0; i < dmlEx.getNumDml(); i++ ){
String errorMessage = 'Error on creation of Project: ' + dmlEx.getDmlMessage( i );
trigger.newMap.get ( createProjectList[ dmlEx.getDmlIndex( i ) ].Opportunity__c ).addError(errorMessage);
// Remove failed one
validProjectsToInsert.remove( dmlEx.getDmlIndex( i ) );
}
// And final insert of valid
insert validProjectsToInsert;
}
...
In the end want to say that we can also use getDmlType() and getDmlStatusCode() to identify what specific error was. Or getDmlFields() with getDmlFieldNames() to know if error was on fields.
In SF there are two objects responsible for S2S connection: PartnerNetworkConnection - keeps all information about current connection and PartnerNetworkRecordConnection - holds information about record that was forwarded. I will not write here how to create S2S connection, there is a lot of information about in internet. We will talk about forwarding objects that are already published and subscribed in both orgs. We can forward objects in two ways: manualy in page or in code. Below is sample of class to do this in code:
public class S2SService {
public static final String CONNTECTION_STATUS_ACCEPTED = 'Accepted';
// Method return Id of current connection.
public static Id getConnectionId( String connectionName ) {
return [SELECT Id
FROM PartnerNetworkConnection
WHERE connectionStatus = : CONNTECTION_STATUS_ACCEPTED
AND connectionName = : connectionName
LIMIT 1].Id;
}
/*
@param: objectList - list of objects you want to forward;
@param: parentRecordIdMap - Map<Record Id; Parent Id> map of Parent records;
@param: relatedRecordsMap - Map<Record Id; String with related records String>
map with objects API names for whom forwarding record
is parent and you want to forward them too;
@param: connectionName - name of connection you want to use;
*/
public static void sendToPartnerOrg( List<sObject> objectList,
Map<Id,Id> parentRecordIdMap,
Map<Id,String> relatedRecordsMap,
String connectionName ){
if ( objectList != null && !objectList.isEmpty() ){
// First thing we need to have connection name
Id connectionId = getConnectionId( connectionName );
// Creating list of PartnerNetworkRecordConnection that will be inserted
List<PartnerNetworkRecordConnection> objectConnections
= new List<PartnerNetworkRecordConnection>();
// Going throught list of recordth we want to share
for ( sObject obj: objectList )
// Adding new PartnerNetworkRecordConnection to list
objectConnections.add(
new PartnerNetworkRecordConnection(
// ConnectionId - setting our PartnerNetworkConnection Id
ConnectionId = connectionId,
// LocalRecordId - Id of record we want to forward
LocalRecordId = obj.Id,
// SendClosedTasks and SendOpenTasks false because we do not wont to forward tham too
SendClosedTasks = false,
SendOpenTasks = false,
SendEmails = true,
// ParentRecordId - parent of forwarding record
ParentRecordId = parentRecordIdMap != null ?
parentRecordIdMap.get( obj.Id ):
null,
// RelatedRecords - child objects you what to forward too
RelatedRecords = relatedRecordsMap != null ?
relatedRecordsMap.get( obj.Id ):
null )
);
// Upserting of PartnerNetworkRecordConnection list with linking error to the object
// we are forwarding
if ( !objectConnections.isEmpty() ) {
try{
database.upsert( objectConnections );
}catch( DMLException dmlEx ){
for ( Integer i = 0; i < dmlEx.getNumDml(); i++ )
if( dmlEx.getDmlStatusCode( i ) == 'INVALID_PARTNER_NETWORK_STATUS' )
for( PartnerNetworkRecordConnection connectionItem: objectConnections )
if( dmlEx.getDmlId( i ) == connectionItem.Id )
for( sObject obj : objectList )
if( connectionItem.LocalRecordId == obj.Id )
obj.addError( 'Incorrect partner network status.' );
}
}
}
}
}
Note: if there will be an error, in upserting one the record, all bunch will not be forwarded.
Also we do not need to forget about SF Governors and Limits. Because if we have triggers and some logic on forwarded records, in partner org, we can hit them. In this case we will not get error on Connection history, but all upserted PartnerNetworkRecordConnections will have status "Pending (sent)" and will not be forwarded.
To be able to forward records again we need to change status of PartnerNetworkRecordConnection from "Pending (sent)" to "Inactive". To do this we need to delete PartnerNetworkRecordConnection, but it will not be deleted, it will just change status to "Pending (sent)". After this we can insert tham again. If there will not be any errors, status will change to "Active (sent)" or "Connected" in several seconds. So if we need to change something in PartnerNetworkRecordConnection, we need to delete it and insert/upsert it again. This is strange way to work with, but it is only way.
One more thing. When we are forwarding records and filling in field ParentRecordId, we need to be sure that parent record is already forwarded, in other case we will have an error.
public static List<selectOption> generateOptions( List<Schema.PicklistEntry> picklistEntry,
Map<String, String> picklistMap,
Boolean addNone,
Boolean sortOptions ) {
//Initializing list of options
List<selectOption> options = new List<selectOption>();
//Check if we need to add first empty option
if( addNone )
options.add( new selectOption( '', 'None' ) );
//Check if we have picklist from sObject
if( picklistEntry != null )
for ( Schema.PicklistEntry items : picklistEntry )
options.add( new selectOption( items.getLabel(), items.getValue() ) );
// or from list
if( picklistMap != null )
for ( String itemsId : picklistMap.keySet() )
options.add( new selectOption( itemsId , picklistMap.get( itemsId ) ) );
// check if we need to sort options
if( sortOptions )
options.sort();
return options;
}
The issue is that we cannot see any logs or System debugs when work is being done under S2S connection.
For example we have Salseforce 2 Salesforce connection. In one of instances , lets call it a guest, we have trigger that fires before Object insert (or after). We add new object to main instance, and connection creates the same object on the guest. Salseforce does not let us see any debug logs while doing something under S2S connection. So what to do if we need to "debug" triggers and methods in connection? We tried to solve this issue with creating a temporary field in the Object. Something like "LogField" - Long Text Area. In triggers and classes add code e.g.:
After the work is done we can see "logs" in LogField of the Object.
Also if there were some errors while running the trigger or with creating the Object you can always see them on Connection page in Logs.
Everytime when we write test classes, we need to a create a big number of data to apply them to the methods that we are testing. We create a lot of methods leading to the creation of these or those objects. I have created the method leading to the creation of any type objects we need:
public with sharing class Utilities {
/*
Method for creating any Object
*/
public static List<sObject> createObject(Integer numberToCreate,
Boolean doInsert,
Schema.SObjectType objectType,
Map<String, String> fieldsMap){
List<sObject> objectsList = new List<sObject>();
List<String> objectFields = new List<String>();
List<String> fieldsList = new List<String>();
objectFields.addAll(objectType.getDescribe().fields.getMap().keySet());
fieldsList.addAll(fieldsMap.keySet());
if(numberToCreate > 0){
for( Integer countOfObjects = 0; countOfObjects < numberToCreate; countOfObjects++ ){
SObject currentObject = objectType.newSObject();
for(String fieldFromList : fieldsList){
for(String fieldFromObject : objectFields){
if(fieldFromObject == fieldFromList){
currentObject.put(fieldFromList , fieldsMap.get(fieldFromList));
}
}
}
objectsList.add(currentObject);
}
if ( doInsert ) insert objectsList;
}
return objectsList;
}
}
Also I have added test class for it:
@isTest
private class TestUtilities {
private static final Integer TEST_RECORD_COUNT = 10;
static Map<String, String> fieldsMap;
static Schema.SObjectType objectType;
static List<String> objectFields = new List<String>();
static String query;
private static void setup(){
objectType = Account.getsObjectType();
objectFields.addAll(objectType.getDescribe().fields.getMap().keySet());
fieldsMap = new Map<String, String>{'Name' => 'Test', //Fill in fields
'BillingCountry' => 'Usa', //IMPORTANT You need to fill in requaired fields
'AccountSource' => 'Web'};
}
private static testmethod void testCreateObject(){
List<sObject> testCreatedObjects;
List<sObject> objectsFromDB;
List<String> fieldsFromBD = new List<String>();
setup();
Test.startTest();
testCreatedObjects = Utilities.createObject(TEST_RECORD_COUNT,
true,
objectType,
fieldsMap);
Test.stopTest();
query = 'SELECT Id';
fieldsFromBD.addAll(fieldsMap.keySet());
for(String fieldDB : fieldsFromBD){
for(String fieldObject : objectFields){
if(fieldDB == fieldObject){
query += ', ' + fieldDB;
}
}
}
query += ' FROM ' + objectType + ' WHERE Id IN : ' + 'testCreatedObjects' + ' ';
objectsFromDB = Database.query(query);
System.assertEquals( TEST_RECORD_COUNT , objectsFromDB.size() );
}
}