Newer Version Available

This content describes an older version of this product. View Latest

Recalculating Apex Managed Sharing

Salesforce automatically recalculates sharing for all records on an object when its organization-wide sharing default access level changes. The recalculation adds Force.com managed sharing when appropriate. In addition, all types of sharing are removed if the access they grant is considered redundant. For example, manual sharing, which grants Read Only access to a user, is deleted when the object’s sharing model changes from Private to Public Read Only.

To recalculate Apex managed sharing, you must write an Apex class that implements a Salesforce-provided interface to do the recalculation. You must then associate the class with the custom object, on the custom object's detail page, in the Apex Sharing Recalculation related list.

Apex sharing reasons and Apex managed sharing recalculation are only available for custom objects.

Note

You can execute this class from the custom object detail page where the Apex sharing reason is specified. An administrator might need to recalculate the Apex managed sharing for an object if a locking issue prevented Apex code from granting access to a user as defined by the application’s logic. You can also use the Database.executeBatch method to programmatically invoke an Apex managed sharing recalculation.

Every time a custom object's organization-wide sharing default access level is updated, any Apex recalculation classes defined for associated custom object are also executed.

Note

To monitor or stop the execution of the Apex recalculation, from Setup, enter Apex Jobs in the Quick Find box, then select Apex Jobs.

Creating an Apex Class for Recalculating Sharing

To recalculate Apex managed sharing, you must write an Apex class to do the recalculation. This class must implement the Salesforce-provided interface Database.Batchable.

The Database.Batchable interface is used for all batch Apex processes, including recalculating Apex managed sharing. You can implement this interface more than once in your organization. For more information on the methods that must be implemented, see Using Batch Apex.

Before creating an Apex managed sharing recalculation class, also consider the best practices.

The object’s organization-wide default access level must not be set to the most permissive access level. For custom objects, this is Public Read/Write. For more information, see Access Levels.

Important

Apex Managed Sharing Recalculation Example

For this example, suppose you are building a recruiting application and have an object called Job. You want to validate that the recruiter and hiring manager listed on the job have access to the record. The following Apex class performs this validation. This example requires a custom object called Job, with two lookup fields associated with User records called Hiring_Manager and Recruiter. Also, the Job custom object should have two sharing reasons added called Hiring_Manager and Recruiter. Before you run this sample, replace the email address with a valid email address that you want to to send error notifications and job completion notifications to.

1global class JobSharingRecalc implements Database.Batchable<sObject> {
2    
3    // String to hold email address that emails will be sent to. 
4    // Replace its value with a valid email address.
5    static String emailAddress = 'admin@yourcompany.com';
6    
7    // The start method is called at the beginning of a sharing recalculation.
8    // This method returns a SOQL query locator containing the records 
9    // to be recalculated. 
10    global Database.QueryLocator start(Database.BatchableContext BC){
11        return Database.getQueryLocator([SELECT Id, Hiring_Manager__c, Recruiter__c 
12                                         FROM Job__c]);  
13    }
14    
15    // The executeBatch method is called for each chunk of records returned from start.  
16    global void execute(Database.BatchableContext BC, List<sObject> scope){
17       // Create a map for the chunk of records passed into method.
18        Map<ID, Job__c> jobMap = new Map<ID, Job__c>((List<Job__c>)scope);  
19        
20        // Create a list of Job__Share objects to be inserted.
21        List<Job__Share> newJobShrs = new List<Job__Share>();
22               
23        // Locate all existing sharing records for the Job records in the batch.
24        // Only records using an Apex sharing reason for this app should be returned. 
25        List<Job__Share> oldJobShrs = [SELECT Id FROM Job__Share WHERE ParentId IN 
26             :jobMap.keySet() AND 
27            (RowCause = :Schema.Job__Share.rowCause.Recruiter__c OR
28            RowCause = :Schema.Job__Share.rowCause.Hiring_Manager__c)]; 
29        
30        // Construct new sharing records for the hiring manager and recruiter 
31        // on each Job record.
32        for(Job__c job : jobMap.values()){
33            Job__Share jobHMShr = new Job__Share();
34            Job__Share jobRecShr = new Job__Share();
35            
36            // Set the ID of user (hiring manager) on the Job record being granted access.
37            jobHMShr.UserOrGroupId = job.Hiring_Manager__c;
38            
39            // The hiring manager on the job should always have 'Read Only' access.
40            jobHMShr.AccessLevel = 'Read';
41            
42            // The ID of the record being shared
43            jobHMShr.ParentId = job.Id;
44            
45            // Set the rowCause to the Apex sharing reason for hiring manager.
46            // This establishes the sharing record as Apex managed sharing.
47            jobHMShr.RowCause = Schema.Job__Share.RowCause.Hiring_Manager__c;
48            
49            // Add sharing record to list for insertion.
50            newJobShrs.add(jobHMShr);
51            
52            // Set the ID of user (recruiter) on the Job record being granted access.
53            jobRecShr.UserOrGroupId = job.Recruiter__c;
54            
55            // The recruiter on the job should always have 'Read/Write' access.
56            jobRecShr.AccessLevel = 'Edit';
57            
58            // The ID of the record being shared
59            jobRecShr.ParentId = job.Id;
60            
61            // Set the rowCause to the Apex sharing reason for recruiter.
62            // This establishes the sharing record as Apex managed sharing.
63            jobRecShr.RowCause = Schema.Job__Share.RowCause.Recruiter__c;
64            
65         // Add the sharing record to the list for insertion.            
66            newJobShrs.add(jobRecShr);
67        }
68        
69        try {
70           // Delete the existing sharing records.
71           // This allows new sharing records to be written from scratch.
72            Delete oldJobShrs;
73            
74           // Insert the new sharing records and capture the save result. 
75           // The false parameter allows for partial processing if multiple records are 
76           // passed into operation. 
77           Database.SaveResult[] lsr = Database.insert(newJobShrs,false);
78           
79           // Process the save results for insert.
80           for(Database.SaveResult sr : lsr){
81               if(!sr.isSuccess()){
82                   // Get the first save result error.
83                   Database.Error err = sr.getErrors()[0];
84                   
85                   // Check if the error is related to trivial access level.
86                   // Access levels equal or more permissive than the object's default 
87                   // access level are not allowed. 
88                   // These sharing records are not required and thus an insert exception 
89                   // is acceptable. 
90                   if(!(err.getStatusCode() == StatusCode.FIELD_FILTER_VALIDATION_EXCEPTION  
91                                     &&  err.getMessage().contains('AccessLevel'))){
92                       // Error is not related to trivial access level.
93                       // Send an email to the Apex job's submitter.
94                     Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
95                     String[] toAddresses = new String[] {emailAddress}; 
96                     mail.setToAddresses(toAddresses); 
97                     mail.setSubject('Apex Sharing Recalculation Exception');
98                     mail.setPlainTextBody(
99                       'The Apex sharing recalculation threw the following exception: ' + 
100                             err.getMessage());
101                     Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
102                   }
103               }
104           }   
105        } catch(DmlException e) {
106           // Send an email to the Apex job's submitter on failure.
107            Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
108            String[] toAddresses = new String[] {emailAddress}; 
109            mail.setToAddresses(toAddresses); 
110            mail.setSubject('Apex Sharing Recalculation Exception');
111            mail.setPlainTextBody(
112              'The Apex sharing recalculation threw the following exception: ' + 
113                        e.getMessage());
114            Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
115        }
116    }
117    
118    // The finish method is called at the end of a sharing recalculation.
119    global void finish(Database.BatchableContext BC){  
120        // Send an email to the Apex job's submitter notifying of job completion.
121        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
122        String[] toAddresses = new String[] {emailAddress}; 
123        mail.setToAddresses(toAddresses); 
124        mail.setSubject('Apex Sharing Recalculation Completed.');
125        mail.setPlainTextBody
126                      ('The Apex sharing recalculation finished processing');
127        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
128    }
129    
130}

Testing Apex Managed Sharing Recalculations

This example inserts five Job records and invokes the batch job that is implemented in the batch class of the previous example. This example requires a custom object called Job, with two lookup fields associated with User records called Hiring_Manager and Recruiter. Also, the Job custom object should have two sharing reasons added called Hiring_Manager and Recruiter. Before you run this test, set the organization-wide default sharing for Job to Private. Note that since email messages aren’t sent from tests, and because the batch class is invoked by a test method, the email notifications won’t be sent in this case.

1@isTest
2private class JobSharingTester {
3   
4    // Test for the JobSharingRecalc class    
5    static testMethod void testApexSharing(){
6       // Instantiate the class implementing the Database.Batchable interface.     
7        JobSharingRecalc recalc = new JobSharingRecalc();
8        
9        // Select users for the test.
10        List<User> users = [SELECT Id FROM User WHERE IsActive = true LIMIT 2];
11        ID User1Id = users[0].Id;
12        ID User2Id = users[1].Id;
13        
14        // Insert some test job records.                 
15        List<Job__c> testJobs = new List<Job__c>();
16        for (Integer i=0;i<5;i++) {
17        Job__c j = new Job__c();
18            j.Name = 'Test Job ' + i;
19            j.Recruiter__c = User1Id;
20            j.Hiring_Manager__c = User2Id;
21            testJobs.add(j);
22        }
23        insert testJobs;
24        
25        Test.startTest();
26        
27        // Invoke the Batch class.
28        String jobId = Database.executeBatch(recalc);
29        
30        Test.stopTest();
31        
32        // Get the Apex job and verify there are no errors.
33        AsyncApexJob aaj = [Select JobType, TotalJobItems, JobItemsProcessed, Status, 
34                            CompletedDate, CreatedDate, NumberOfErrors 
35                            from AsyncApexJob where Id = :jobId];
36        System.assertEquals(0, aaj.NumberOfErrors);
37      
38        // This query returns jobs and related sharing records that were inserted       
39        // by the batch job's execute method.     
40        List<Job__c> jobs = [SELECT Id, Hiring_Manager__c, Recruiter__c, 
41            (SELECT Id, ParentId, UserOrGroupId, AccessLevel, RowCause FROM Shares 
42            WHERE (RowCause = :Schema.Job__Share.rowCause.Recruiter__c OR 
43            RowCause = :Schema.Job__Share.rowCause.Hiring_Manager__c))
44            FROM Job__c];       
45        
46        // Validate that Apex managed sharing exists on jobs.     
47        for(Job__c job : jobs){
48            // Two Apex managed sharing records should exist for each job
49            // when using the Private org-wide default. 
50            System.assert(job.Shares.size() == 2);
51            
52            for(Job__Share jobShr : job.Shares){
53               // Test the sharing record for hiring manager on job.             
54                if(jobShr.RowCause == Schema.Job__Share.RowCause.Hiring_Manager__c){
55                    System.assertEquals(jobShr.UserOrGroupId,job.Hiring_Manager__c);
56                    System.assertEquals(jobShr.AccessLevel,'Read');
57                }
58                // Test the sharing record for recruiter on job.
59                else if(jobShr.RowCause == Schema.Job__Share.RowCause.Recruiter__c){
60                    System.assertEquals(jobShr.UserOrGroupId,job.Recruiter__c);
61                    System.assertEquals(jobShr.AccessLevel,'Edit');
62                }
63            }
64        }
65    }
66}

Associating an Apex Class Used for Recalculation

An Apex class used for recalculation must be associated with a custom object.

To associate an Apex managed sharing recalculation class with a custom object:
  1. From the management settings for the custom object, go to Apex Sharing Recalculations.
  2. Choose the Apex class that recalculates the Apex sharing for this object. The class you choose must implement the Database.Batchable interface. You cannot associate the same Apex class multiple times with the same custom object.
  3. Click Save.