Stat Tracker

Sunday, February 3, 2013

Creating Mirror Relationship Records in SFDC with Apex Triggers

Reciprocal or Mirror Records Overview


In this blogpost I will be discussing creating reciprocal relationship records in Force.com using Apex Triggers. I'd first like to say that this use case is one that should be scrutinized before you implement it. Creating reciprocal or mirror relationships of records in Force.com can sometimes create duplicate records. To keep your managed data down, you would often like to avoid this process if possible. However there are times when you just have to keep record values in sync, so carefully evaluate your use case before implementing this. Now that I have the disclaimer out the way here we go!

When working with records in Force.com we sometimes have a need to create a reciprocal relationship between two records. For example, sometimes we may have a use case which involves keeping two records in sync. When you update Record A, you may need to update some corresponding fields on Record B. Below is a simple diagram illustrating this.


Force.com Data Model


The best way to implement this is to have a lookup field on the object which is a lookup to it self. In this case I have create a lookup field on contact called "Linked Contact" and related it to the Contact record as shown in the schema builder diagram below.



Apex Trigger Flow

Now that I have the data model configured for the reciprocal relationships, I need to keep the records in sync automatically. This means that whenever Contact record A is updated, make sure to update Contact record B. We can accomplish this via Apex Triggers when a record is inserted or updated to keep the lookup fields in sync, thereby allowing us to "link" the records. However, there is problem with this scenario.

In an Apex Trigger you only have acces to the records Id in the After context, which makes sense. There cannot be an Id until after the record has been created. So the flow would look like this:
The problem as you may see is that in a After context in an Apex Trigger you cannot update the original record! So in this scenario we cannot update Rec A in the Apex Trigger because we will get a read only error as shown below:

So you may be asking yourself how can we make this work in the Apex Trigger? The answer is quite simple actually but it is not clearly documented to my knowledge. If we create a new instance of an SObject in the Apex Trigger in memory using the Id of the newly created record as provided in the After Trigger context, we can perform an Update DML statement and not get a read only error! This is because in Apex, the SObject is seen as a new reference (even though the records have the same SFDC ID) and therefore is eligible for DML operations! The below snippet of code illustrated this working and not working.

List<Contact> originals = new List<Contact>();
if(mirrorResultMap.values().size() > 0)
{
for(Contact origContact : contactRecs.values())
{
Contact mirrorContact = mirrorResultMap.get(origContact.Id);
//origContact.Linked_Contact__c = mirrorContact.Id; //Link the Original Record tot he Mirror Record WILL FAIL
Contact origContactUpdate = new Contact(Id=origContact.Id, Linked_Contact__c = mirrorContact.Id); //This will WORK
originals.add(origContactUpdate);
}
//update contactRecs.values(); //Update the Records -> THIS WILL FAIL AS ITS ORIGINAL RECORDS IN MEMORY
update originals;
}


With this code in place we will no longer get an error message, and the two records will now be linked! You can see the lookup fields are now populated to reference each other.


This is useful as I said when you want to link two records together to keep them in sync on DML operations. They can be two records of the same type as I illustrated in this example, or you could keep two different objects in sync via lookups if you needed to. Its a handy skill set to know so go forth and code!

Full Code Dump:


Apex Trigger:

trigger ContactTrigger on Contact (after delete, after insert, after undelete
after update, before delete, before insert, before update
{
if(trigger.isAfter)
{
if(trigger.isInsert)
{
ContactTriggerHandler.processAfterInsert(trigger.newMap);
}
}
}


Apex Class (Trigger Handler)

public class ContactTriggerHandler 
{
public static void processAfterInsert(map<Id,Contact> contactRecs)
{
//Query for the existing mirror records
Map<Id,Contact> mirrorContacts = new Map<Id,Contact>([Select c.Id, c.Linked_Contact__c From Contact c where c.Linked_Contact__c in: contactRecs.keySet() ]);
List<Contact> mirrorInserts = new List<Contact>(); 
Map<Id,Contact> updateReciprical = new Map<Id,Contact>();
//Iterate over the Trigger New Records
for(Contact contactRec : contactRecs.values())
{
if(contactRec.Linked_Contact__c == null)
{
Contact mirrorContact = mirrorContacts.get(contactRec.Id);
if(mirrorContact == null) //If this record does not have a linked mirror record, create one and link it to this record
{
Contact c = new Contact(FirstName = contactRec.FirstName, LastName = contactRec.LastName, Description = 'Mirror Record', Linked_Contact__c = contactRec.Id);
mirrorInserts.add(c);
}
}
}
Map<Id,Contact> mirrorResultMap = new Map<Id,Contact>();
if(mirrorInserts.size() > 0)
{
insert mirrorInserts; //After Insert DML, the ID's will be populated
for(Contact mirrorInsert : mirrorInserts) //Store the results in a Map so we can now use these Id's to link the records
{
mirrorResultMap.put(mirrorInsert.Linked_Contact__c,mirrorInsert);
}
}
List<Contact> originals = new List<Contact>();
if(mirrorResultMap.values().size() > 0)
{
for(Contact origContact : contactRecs.values())
{
Contact mirrorContact = mirrorResultMap.get(origContact.Id);
//origContact.Linked_Contact__c = mirrorContact.Id; //Link the Original Record tot he Mirror Record WILL FAIL
Contact origContactUpdate = new Contact(Id=origContact.Id, Linked_Contact__c = mirrorContact.Id); //This will WORK
originals.add(origContactUpdate);
}
//update contactRecs.values(); //Update the Records -> THIS WILL FAIL AS ITS ORIGINAL RECORDS IN MEMORY
update originals;
}
}
}