SOQL Returns Old Value After UI Update: Diagnosis for Developers
Developers frequently encounter scenarios where a field update performed via the Salesforce User Interface (UI) reflects immediately in the client view, yet an immediate follow-up SOQL query executed within Apex returns the old, stale value. This behavior is counter-intuitive but usually stems from transaction scope, database caching, or the specific timing of data commits, especially in complex execution contexts.
The Symptom: UI vs. Apex Data Mismatch
Consider the scenario described:
- Action: A user modifies a custom field (e.g.,
Date_Awarded__c) on anOpportunityrecord in the Salesforce UI. - UI Observation: The UI immediately displays the new value, and the
LastModifiedDatefield is correctly updated. - Apex Observation: Immediately executing a SOQL query for the same record returns the pre-update value for
Date_Awarded__c, whileLastModifiedDatereflects the update.
-- UI shows: Date_Awarded__c = 2026-XX-XX
SELECT Id, Date_Awarded__c, LastModifiedDate
FROM Opportunity
WHERE Id = :recordId
-- Apex SOQL returns: Date_Awarded__c = (Old Value), LastModifiedDate = (New Timestamp)
Root Cause Analysis: Transaction Context and Commit Timing
When data is modified directly through the UI, the change is usually committed to the database as part of the standard save operation initiated by the client-side framework (e.g., Aura or LWC rendering process). However, the context in which your subsequent Apex code runs is critical.
1. Implicit Transaction Context
If the Apex code executing the SOQL query is triggered synchronously as part of the same UI save operation (e.g., an after update trigger, or a method called directly from a component save handler), the record might not yet be visible to subsequent queries within that same transaction scope.
Key Concept: Database Visibility
Salesforce enforces transaction isolation. Changes made earlier in a running transaction are not guaranteed to be visible via subsequent queries within the same transaction unless those changes were made via DML operations issued by the executing Apex context itself. When the UI saves the record, the record ID and its new state are known to the running context, but a standalone SOQL query might still hit a potentially uncommitted or temporarily cached version, or rely on a different snapshot of the database state.
2. Apex Execution Context
If the Apex runs after the UI save operation has fully completed (e.g., triggered by an asynchronous event fired by the UI save, or a separate, later user action), the data should generally be present. If it is still stale, consider:
- Custom Caching Mechanisms: If any Apex code or external caching layer (unlikely for immediate UI-to-Apex calls) is involved, it might be serving stale data.
- Asynchronous Queuing: If the UI action triggered an asynchronous process (like Queueable or Future method) that runs later, the initial execution context might complete before the asynchronous operation finishes saving. However, this usually results in the initial Apex call seeing the update, and the later async call seeing the updated data.
Developer Solutions and Workarounds
The primary goal is to force Apex to read the most recently committed state, bypassing any immediate transaction-level visibility issues.
1. Utilizing refresh() or Re-querying in the UI Layer
If this is occurring during the LWC/Aura lifecycle immediately following a save, ensure the component is explicitly refreshing its wire adapters or imperative calls to re-fetch the record data from the server after the save completes. This isn't an Apex fix but addresses the client-side perception.
2. Forcing a Database Read (Apex Solution)
If your Apex logic must execute immediately after the UI save and read the committed value, the most reliable method is often to avoid a standard SOQL query and instead read the value directly from the record variable that resulted from the DML operation, or explicitly reread the record using Database.query or SObject.refresh() if available in the context.
If you are in an Apex Trigger context (after update), the record passed into the trigger context variables (Trigger.newMap) already contains the committed database values (or the values as of the end of the transaction). Accessing the field there is preferred over a SOQL query against the same record ID within the same transaction scope.
Example: Reading from Trigger Context (Recommended for Triggers):
// Inside an after update trigger
for (Opportunity opp : Trigger.new) {
// opp.Date_Awarded__c will reflect the value saved by the UI or previous DML
System.debug('Trigger new value: ' + opp.Date_Awarded__c);
}
Example: Using SObject.refresh() (If applicable and supported in your context):
While not universally applicable for all standard scenarios, explicitly re-loading the record if your execution context allows it can force a fresh database pull.
Opportunity o = [SELECT Id, Date_Awarded__c FROM Opportunity WHERE Id = :recordId FOR UPDATE];
// If the UI update happened outside this immediate transaction, this should now read correctly.
The Safest Bet: If the UI update is the source of the commit, and you are trying to read it back in an immediately following piece of Apex code (like an after insert/update trigger), rely on the data within the Trigger.new or the DML result variable, not a new SOQL query.
Key Takeaways
- SOQL queries within the same transaction that made the initial change may return stale data due to isolation levels.
- UI updates commit data, but immediate Apex reads might not see the finalized state until the transaction fully unwinds or a new transaction begins.
- For Apex triggers, always rely on the
Trigger.newmap to access values successfully committed by the save operation that fired the trigger. - If running imperative Apex post-UI save, consider re-executing the component's data fetch or ensuring the component waits for the server confirmation before attempting its next action.
Leave a Comment