5

We have seen an interesting behaviour. When we call a method which tries to update locked records it failes with ENTITY_IS_LOCKED. When we call the same method as a future method it works! By design? An accident we shouldn't rely on? Anyone else noticed this?

The class is not marked as "with sharing", the only difference is the @future annotation.

3
  • Future methods operate in system context. I wonder if that is creating this behavior.
    – greenstork
    Commented Oct 30, 2013 at 16:50
  • If the class containing the method isn't "with sharing" or "without sharing", then its sharing is determined by the execution context. This means that, normally, the same function called from two different sources will have different effects. In an unspecified sharing model, a @future method calling the function will pass when a Visualforce page call will fail, all because of how sharing is applied. You'll need to grasp a better understanding-- see the link in the comments to the answer. This is by design; sharing must be enforced or not enforced by the developer.
    – sfdcfox
    Commented Oct 31, 2013 at 13:45
  • The execution context was the same both times. But are you saying that @future is always "without sharing"? Where is this easter egg documented?
    – Marc
    Commented Nov 1, 2013 at 7:11

1 Answer 1

2

Make sure the future method is in a class "with sharing" or it will be allowed to perform the update-- the default mode of operation for unspecified classes is "without sharing" (with exceptions). Without sharing means the act is performed "as an administrator", thus bypassing the lock.

Edit #2

A better understanding of the forces at play are mentioned on SFDC: Understanding With Sharing, Without Sharing & Unspecified Sharing Classes.

Edit #1

First, see: Using the with sharing or without sharing Keywords.

Next, you'll this answer is correct. I shall offer proof in a pattern that can be replicated in about 10 minutes, depending on your Internet connection.

Step 1: Custom Object

Name: Limited

Organization Wide Defaults: Public Read Only

Custom Fields:

Account__c Lookup(Account__c)
Counter__c Number(18, 0)

Triggers:

trigger updateCounter on Limited__c (before insert, before update) {
    for(limited__c record: trigger.new)
        if(record.counter__c == null)
            record.counter__c = 0;
        else
            record.counter__c++;
}

Step 2: Custom Profile

Create a new profile from the Read Only profile. Add "Create Edit Delete" permissions to Account and Limited.

Step 3: Future handler

public class FutureHandler {
    @future
    public static void updateContacts(Set<Id> accountIds) {
        update [select id from limited__c where account__c in :accountIds];
    }
}

Step 4: Accounts

Organization Wide Defaults: Public Read/Write

Triggers:

trigger InvokeFuture on Account (after update) {
    FutureHandler.updateContacts(Trigger.newMap.keySet());
}

Step 5: Set up test data

Create a new account, and add a new "limited to the account. The counter should read 0.

Edit the account, making any change that successfully saves. Look back at the "limited" record, the counter should now read 1.

Step 6: New User

Create a new user with the custom profile from before. Login to the account, find the test data, verify that the user cannot manually edit the "limited" record (click edit, you'll get insufficient privileges). Edit the account, and go back to the "limited", the counter should now read 2. Note This occurs because we are "without sharing", the default model.

Step 7: Update Future handler

As the administrator, update the FutureHandler class:

public with sharing class FutureHandler {
    @future
    public static void updateContacts(Set<Id> accountIds) {
        update [select id from limited__c where account__c in :accountIds];
    }
}

Step 8: Test Again

Log back in to the test user, verify they still cannot edit the "limited". Now, edit the account. Once saved, check the "limited". The counter should still be at 2.

Step 9: Verify with debugging

Turn on the Debug Logs for the test user, then log back in as the test user. Try editing the account again. The counter will remain at 2 after this edit. Check the debug logs:

Update failed. First exception on row 0 with id a0J5000000IrBosEAF; first error: INSUFFICIENT_ACCESS_OR_READONLY, insufficient access rights on object id: []   

Q.E.D.

8
  • 1
    I'm fairly certain that future methods run in system context regardless of sharing defined on the class.
    – greenstork
    Commented Oct 30, 2013 at 17:23
  • I've updated my answer for your benefit and others that may follow.
    – sfdcfox
    Commented Oct 30, 2013 at 18:42
  • I've updated my question: our class is not "with sharing" - the only difference is the @future annotation.
    – Marc
    Commented Oct 31, 2013 at 9:07
  • See my answer on SFDC: Understading the Sharing, Without Sharing & Unspecified Sharing Classes for an in-depth discussion on sharing/without sharing. If you want your synchronous function call to always succeed (e.g. from a Visualforce page), explicitly use "without sharing." If you want to always enforce record locks, always use "with sharing." Ideally, you should probably never use the default model (not specified) because it causes this sort of confusion.
    – sfdcfox
    Commented Oct 31, 2013 at 13:43
  • This doesn't really prove your point, I don't believe. The reason that your update is failing is because your trigger on Limited is not running. It is not in future context.
    – greenstork
    Commented Oct 31, 2013 at 14:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .