Inside an Apex before insert
trigger, what is the correct way to get an exclusive lock all records of a particular object for the length of the trigger's transaction?
This trigger needs an exclusive lock to perform validation that can't easily be expressed any other way. The canonical example I see is to check for overlapping dates in a record. Plenty of questions and answers on how to do that, but none seem to address the race condition between doing the select
followed by the insert
.
It's our understanding that a single trigger runs serially within its own transaction. But it doesn't have exclusivity across multiple clients.
Example:
Check if a new record has overlapping dates with any existing records:
- Client A: Enters
before insert trigger
- Client A: Fetches records and checks for overlapping dates.
- Client B: Enters
before insert trigger
- Client B: Fetches records and checks for overlapping dates.
- Client B: Writes new record to the database, trigger ends.
- Client A: Now has a stale result with an
insert
pending.
The Salesforce documentation says to use FOR UPDATE
to acquire an exclusive lock on certain records:
Apex Developer Guide: Locking Statements
Given the example in the documentation, is the following a valid (or even recommended) way to lock all records?
Account [] accts = [SELECT Id, StartDate, EndDate FROM Account FOR UPDATE];
Note that we're only likely to be looping over a small number of records. Think of it like a membership with monthly billing records. The dates for each monthly Billing record should not overlap with any others for a given membership.
Update 1:
Refer to: Salesforce Platform: Order of Execution Overview
When using FOR UPDATE
in a SOQL query, is the "lock" effectively acquired in step 3 of the execution order and then released after step 19/20?
If so, is the following true:
The trigger using
FOR UPDATE
in step 3 will wait for all in-progress events on those records to complete before it can acquire the lock.Once acquired, all subsequent events will then wait on step 1 until the event with the lock completes step 19/20?
The code that initiated the
FOR UPDATE
does not have to do any explicit clean-up to release the lock.
WHERE
clause is likely to cause performance issues or even governor limit issues. I suggest you do, indeed, include per-Account date range comparison, via the use of dynamically generated SOQL that usesFOR UPDATE
on the related objects. There are SOQL query length limits, but here you can have at most 200 unique related objects since you are in a trigger.FOR UPDATE
on the N Accounts that have Billings in scope of the trigger execution. That gives you the scope of lock you need to uphold this invariant.