function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
doughauck-corpdoughauck-corp 

Visualforce DML changes do not persist

I am having a problem with the upsert DML command in an Apex class I'm using with my Visualforce page.

The user makes changes on a generic SObject, which is exposed as the editSObject property of my custom Apex class, called SObjectTreeNode (I'm not very inventive with names, sorry).  When the user hits the Save button, the SObjectTreeNode.upsertSObject() method is called to send the changes to the database, and then the page is refreshed.

When the page refreshes right after a Save, the new values show up in the changed fields just as they should.  Since refreshing the page includes a re-query of the associated SObject, I assumed this meant my changes were in the database.  Not so, though - refreshing the screen a second time, or navigating to the normal SFDC detail page for the object, shows the fields back at their original values.

Below is the upsertSObject() method - with some extra debug code - and its associated output.  (Please note that User__c is a custom text-only field that has no connection to the SFDC User object.  Sorry for the confusion, but I didn't name it.  I picked it for my example because I know there are no WF or Validation rules on this field, but all fields show the same behavior.)

Here is the code:
public SObjectTreeNode upsertSObject()
{
    if(!isEditable)
        throw new sObjectTreeNodeException('Error in node \'' + nodeName + '\': upsertSObject() can only be called on Editable nodes.');
    if(editSObject == null)
        throw new sObjectTreeNodeException('Error in node \'' + nodeName + '\': editSObject is null.');
    
    system.debug(' ');
    system.debug('Running upsertSObject()...');
    
    id editSObjId = (id)editSObject.get('id');
    sObject dbaseSObject = [select Id, User__c from Inspection__c where Id = :editSObjId limit 1];
    
    system.debug(' ');
    system.debug('Pre-Upsert Values:');
    system.debug(' - editSObject.id       = ' + editSObject.get('id'));
    system.debug(' - editSObject.User__c  = ' + editSObject.get('user__c'));
    system.debug(' - dbaseSObject.id      = ' + dbaseSObject.get('id'));
    system.debug(' - dbaseSObject.User__c = ' + dbaseSObject.get('user__c'));
    
    upsert editSObject;
    
    editSObjId = (id)editSObject.get('id');
    dbaseSObject = [select Id, User__c from Inspection__c where Id = :editSObjId limit 1];
    
    system.debug(' ');
    system.debug('Post-Upsert Values:');
    system.debug(' - editSObject.id       = ' + editSObject.get('id'));
    system.debug(' - editSObject.User__c  = ' + editSObject.get('user__c'));
    system.debug(' - dbaseSObject.id      = ' + dbaseSObject.get('id'));
    system.debug(' - dbaseSObject.User__c = ' + dbaseSObject.get('user__c'));
    
    system.debug('Completed upsertSObject()');
    system.debug(' ');
    
    return this;
}

And here is the log output:
20:06:06.3 (2285501897)|USER_DEBUG|[85]|DEBUG|Running upsertSObject()...
20:06:06.3 (2298707241)|USER_DEBUG|[90]|DEBUG| 
20:06:06.3 (2298739034)|USER_DEBUG|[91]|DEBUG|Pre-Upsert Values:
20:06:06.3 (2298843638)|USER_DEBUG|[92]|DEBUG| - editSObject.id       = a1HS0000001hrsRMAQ
20:06:06.3 (2298949438)|USER_DEBUG|[93]|DEBUG| - editSObject.User__c  = Barney Rubble
20:06:06.3 (2299062140)|USER_DEBUG|[94]|DEBUG| - dbaseSObject.id      = a1HS0000001hrsRMAQ
20:06:06.3 (2299176216)|USER_DEBUG|[95]|DEBUG| - dbaseSObject.User__c = Fred Flintstone
20:06:06.3 (2383815970)|USER_DEBUG|[102]|DEBUG| 
20:06:06.3 (2383854572)|USER_DEBUG|[103]|DEBUG|Post-Upsert Values:
20:06:06.3 (2384028718)|USER_DEBUG|[104]|DEBUG| - editSObject.id       = a1HS0000001hrsRMAQ
20:06:06.3 (2384148671)|USER_DEBUG|[105]|DEBUG| - editSObject.User__c  = Barney Rubble
20:06:06.3 (2384263266)|USER_DEBUG|[106]|DEBUG| - dbaseSObject.id      = a1HS0000001hrsRMAQ
20:06:06.3 (2384363465)|USER_DEBUG|[107]|DEBUG| - dbaseSObject.User__c = Barney Rubble
20:06:06.3 (2384393412)|USER_DEBUG|[109]|DEBUG|Completed upsertSObject()

As you can see, it's the same record before and after the upsert, but the value returned by the SOQL statement has changed to match the upserted value.   Any further queries in this same transaction - up until the page has been fully refreshed and my log comes to an end - will show the new value.  Any further refreshes, or navigation through normal SF pages, and the fields go back to their original values.

As far as I can tell, there are no errors encountered in this transaction, hence no reason to roll back changes.  (And my understanding is that rolled back changes wouldn't show up in a subsequent SOQL query, anyway.)   I am working with Visualforce, and in a Sandbox, if that makes any difference.  I can't find any documentation that says it should matter, though.

Can anyone tell me, what gives?

Thanks,
Doug
Shiva RajendranShiva Rajendran
HI Doug,
Could you check if there are any triggers or workflow associated with this object.
Also do share the vf and controller constructor code to help you better.
Is SObjectTreeNode gets called during button click? and is there any significance in your code for returning 'this' in this place? , i mean is this returning the current class instance reference?


Thanks and Regards,
Shiva RV
 
doughauck-corpdoughauck-corp
Hi, Shiva -

Thanks for the response.  Additional code is below; in the meantime, here is some further explanation, as well as answers to your specific questions:
  • First, an overview of the full application (because you never know what might jump out at you):
    • As you may have guessed from the name, SObjectTreeNode is a tree node that contains an SObject.  My intent is to use it for working with records that have a hierarchical structure. (e.g. Account->Contact->Cases).  The class has has an associated VF component,
    • Each node has a nodeSObject property, which stores the associated SObject while making/manipulating the tree.  However, because this class is primarily to be used with VF, and I don't want 500 SObjects serialized into the VF view state, this property is marked transient
    • Since the only time there is a need to store the SObject on the client side is if the user needs to Edit it, there is a separate, non-transient property, editSObject, which points to the same SObject, but only on nodes marked editable (isEditable=true); otherwise it is null, so it doesn't get serialized.  In my code above, note that it is editSObject that is used in the upsertSObject() method.
    • There is also a displayList property, a List<string> that holds the SObject field names I want displayed for that node.
    • Finally, there is a VF component for displaying the node in either View or Edit mode; the component uses a <repeat> block to loop through the values in displayList and assign the relevant fields to either <outputField> or <inputField> tags.
  • Now to your actual questions:
    • There are WF rules associated with certain fields on the Inspection__c object, though not with the User__c field.  There are no active triggers, flows, or processes associated with this object.
    • The method that calls upsertSObject() - and does just about everything else - is the inspectionNode property getter on the main page controller.  The getter calls the static createInspectionTree() method of the main controller to build the tree and return the top node (which contains the whole tree).  It re-creates the tree every time the getter is called, because of the transient SObjects.
    • There are two other properties which are used here: nodesToEdit is a list of nodes that need to be put in Edit mode upon refresh, and nodesToSave is a list of nodes that need to be saved before refresh.  If the user clicked the Edit button on a node, a reference to it is put into nodesToEdit, and if Save button is clicked on a node, a reference is put into nodesToSave
    • The inspectionNode getter and the createInspectionTree() method use these two lists to determine how the tree should be rebuilt, and what should be done before returning it to the VF page.  The getter calls upsertSObject() on every node in the nodesToSave list before it returns the updated tree, and that is what you are looking at in the code / results above.
Below is my code, or at least that part of it that works with creating and displaying the tree.  I know it's messy and needs a lot of work after this - it's very much in the initial stages.  However, I don't think there's anywhere that the mess should affect the behavior of the Upsert DML - I don't see anywhere that the Upsert could be double-called, or the SObject overwritten after the part you see in my results above.

There's a lot of code below, but when (if) I get an answer, I will edit the post to remove unnecessary stuff.

Thanks,
Doug

Relevant parts of the SObjectTreeNode class:
public class SObjectTreeNode implements Comparable // for sorting
{   
    public class sObjectTreeNodeException extends system.Exception {}
    public class setFieldException extends system.Exception {}
    public class treeSortException extends system.Exception {}
    
    /****  Public Properties  ****/
    
    // You can do anything you want with the node, but anything SObject-related is one-time set.
    public final transient sObject nodeSObject { get; private set; }   
    public final boolean isEditable { get; private set; }
    public final sObject editSObject { get; private set; }   
    
    // Transient (not saved in client-side view state) variables used to set up the tree.  
    public final transient Set<String> sObjFieldNames { get; private set; }
    public final transient Map<string, string> sObjFieldLabels { get; private set; }    
    public final List<string> displayFields { get; private set; } 
    
    public final Id recordId { get; private set; }
    public final string typeName { get; private set; }
    
    public string nodeName { get; set; }
    public integer nodeLevel { get; set; }
    public string cssClass { get; set; }
    
    public List<SObjectTreeNode> childNodes { 
        get
        {
            if(childNodes == null)
                childNodes = new List<SObjectTreeNode>();
            return childNodes;
        }
        set; 
    }
    public SObjectTreeNode parentNode { get; set; }    
    public integer childIndex { get; set; }    
    
    
    /****  Constructors  ****/
    
    public SObjectTreeNode(string nodeName, integer nodeLevel, sObject nodeSObject)
    {
        this(nodeName, nodeLevel, nodeSObject, false);
    }
    
    public SObjectTreeNode(string nodeName, integer nodeLevel, sObject nodeSObject, boolean isEditable)
    {
        if(nodeSObject == null)
            throw new sObjectTreeNodeException('Error in SObjectTreeNode(): Parameter \'nodeSObject\' cannot be null!');
        
        this.isEditable = isEditable;
        this.nodeSObject = nodeSObject;
        this.editSObject = isEditable ? nodeSObject : null;
        
        Schema.DescribeSObjectResult sObjDescribe = nodeSObject.getSObjectType().getDescribe();
        
        this.typeName = sObjDescribe.getName();
        this.recordId = getNodeRecordId();
        Map<String, Schema.SObjectField> sObjFields = sObjDescribe.fields.getMap();
        sObjFieldNames = sObjFields.keySet();
        sObjFieldLabels = new Map<string, string>();
        for(string fieldName : sObjFields.keySet())
            sObjFieldLabels.put(fieldName, sObjFields.get(fieldName).getDescribe().getLabel());

        this.displayFields = new List<string>();
        this.nodeName = nodeName;
        this.nodeLevel = nodeLevel;    
        
        cssClass = '';
        childIndex = -1;
        compareValue = nodeName;
    }
    
    
    /****  Public Instance Methods  ****/
    
    public SObjectTreeNode upsertSObject()
    {
        return upsertSObject(false);
    }
        
    public SObjectTreeNode upsertSObject(boolean keepEditMode)
    {
        if(!isEditable)
            throw new sObjectTreeNodeException('Error in node \'' + nodeName + '\': upsertSObject() can only be called on Editable nodes.');
        if(editSObject == null)
            throw new sObjectTreeNodeException('Error in node \'' + nodeName + '\': editSObject is null.');
        
        upsert editSObject;
        system.debug('Completed upsert of editSObject: ' + editSObject);
        
        return this;
    }
    
    
    /****  Private Instance Methods  ****/
    
    // Sets the node's recordId property to either the SObj's Id or a unique hash string.  
    private Id getNodeRecordId()
    {
        return (Id)nodeSObject.get('id');
    }
    
    
    /****  Comparable Interface Implementation  ****/
    
    public string compareValue { get; set; }    
    public integer compareTo(object compareTo)
    {
        SObjectTreeNode compareNode = (SObjectTreeNode)compareTo;
        if(this.compareValue == null)
            return compareNode.compareValue == null ? 0 : -1;
        else if (compareNode.compareValue == null)
            return 1;
        else
            return this.compareValue == compareNode.compareValue ? 0 : (this.compareValue < compareNode.compareValue ? -1 : 1);
                }
    
    public static void sortNodeList(List<SObjectTreeNode> nodeList)
    {
        if(nodeList == null || nodeList.isEmpty())
            return;
        nodeList.sort();
        renumberNodes(nodeList);
    }
    
    public static void deepSortNodeList(List<SObjectTreeNode> nodeList)
    {
        if(nodeList == null || nodeList.isEmpty())
            return;
        nodeList.sort();
        for(integer i=0; i < nodeList.size(); i++)
        {
            nodeList[i].childIndex = i;
            deepSortNodeList(nodeList[i].childNodes);
        }
    }
    
    public static void renumberNodes(List<SObjectTreeNode> nodeList)
    {
        if(nodeList == null)
            return;
        for(integer i=0; i < nodeList.size(); i++)
            nodeList[i].childIndex = i;
    }
}

The node display VF component:
<apex:component controller="editNodeComponentController" allowDML="true" >
    <apex:attribute name="forNode" description="This component's node." type="SObjectTreeNode" required="true" assignTo="{!thisNode}"/>
    <apex:attribute name="editList" description="List of nodes to edit." type="Id[]" required="false" assignTo="{!editNodes}"/>
    <apex:attribute name="saveList" description="List of nodes to edit." type="SObjectTreeNode[]" required="false" assignTo="{!saveNodes}"/>
    
    <div class="nodeName" style="display:inline-block; margin-right:2em;" >{!thisNode.nodeName}</div>
    
    <!-- Display the tags using output fields, for view mode. -->
    <apex:outputPanel id="viewPanel" layout="none" rendered="{!!thisNode.isEditable}" >
        <apex:form >
            <div class="actions" >
                <apex:commandButton id="btnShowEdit" value="Edit" action="{!showEditMode}" />        
            </div>
        </apex:form>
        <apex:repeat value="{!thisNode.displayFields}" var="fieldName" >
            <div class="param view">
                <div class="label"><apex:outputLabel for="fieldOutput" html-class="label" value="{!thisNode.sObjFieldLabels[fieldName]}:" /></div>
                <div class="value"><apex:outputField styleClass="value" id="fieldOutput" value="{!thisNode.nodeSObject[fieldName]}" /></div>
            </div>
        </apex:repeat>      
    </apex:outputPanel>
    
    <!-- Display the tags using output fields, for edit mode.       -->
    <apex:outputPanel layout="none" rendered="{!thisNode.isEditable}" >            
        <apex:form >
            <div class="actions" >
                <apex:commandButton id="btnSaveChanges" value="Save"  action="{!saveNodeChanges}" />
                <apex:commandButton id="btnHideEdit" value="Cancel"  action="{!hideEditMode}" />
            </div>
            <apex:repeat value="{!thisNode.displayFields}" var="fieldName" >
                <div class="param edit">
                    <div class="label"><apex:outputLabel for="fieldInput" value="{!thisNode.sObjFieldLabels[fieldName]}:" /></div>
                    <div class="value"><apex:inputField id="fieldInput" value="{!thisNode.editSObject[fieldName]}" /></div>                    
                </div>
            </apex:repeat>                  
        </apex:form>
    </apex:outputPanel>         
</apex:component>

The VF component's controller:
public class editNodeComponentController 
{
    public SObjectTreeNode thisNode { get; set; }
    public List<Id> editNodes { get; set; }
    public List<SObjectTreeNode> saveNodes { get; set; }
    
    private static void removeIdFromList(List<Id> idList, Id removeId)
    {
        if(idList == null || removeId == null)
            return;
        integer i;
        for(i=0; i < idList.size() && idList[i] != removeId; i++);
        if(i < idList.size())
            idList.remove(i);
    }
    
    public System.PageReference showEditMode()
    {
        if(editNodes != null && thisNode != null)
            editNodes.add(thisNode.recordId);
        return null;
    }
    
    public System.PageReference hideEditMode()
    {
        if(thisNode != null)
            removeIdFromList(editNodes, thisNode.recordId);
        return null;
    }

    public System.PageReference saveNodeChanges()
    {
        if(thisNode != null)
        {
            if(saveNodes != null)
                saveNodes.add(thisNode);
            removeIdFromList(editNodes, thisNode.recordId);
        }
        return null;
    }    
}

The main VF page (in its current form, it will only display the top node; I have everything else commented out to avoid distractions):
<apex:page showHeader="false" standardController="Inspection__c" extensions="inspTreePageController" >
    
      <div class="{!inspectionNode.cssClass}" style="" >
          <button onClick="display(this); toggleClass('{!inspectionNode.recordId}');" class="w3-btn;" style="min-width:20px;" type="button" >
              +
          </button>
          <button type="button" class="menuItem" style="color:#000;">
              <apex:outputText value="{!inspectionNode.nodeName}" />
          </button>
          <div id="{!inspectionNode.recordId}" class="container" style="display:none">              
              <c:editNodeComponent forNode="{!inspectionNode}" editList="{!nodesToEdit}" saveList="{!nodesToSave}" saveAction="{!saveNodeChanges}" />              
          </div>
      </div>
    
</apex:page>

And finally, here are the relevant parts of the main VF page controller:
public class inspTreePageController 
{   
    public static SObjectTreeNode createInspectionTree(Inspection__c inspection, List<Id> nodesToEdit, List<SObjectTreeNode> nodesToSave)
    { 
        SObjectTreeNode inspNode = new SObjectTreeNode('Initial inspNode', -1, inspection);
        if(inspection == null || inspection.Id == null)
            return inspNode;
        
        Inspection__c fullInsp = [
            select Id, Name, CreatedById, LastModifiedBy.Name, LastModifiedById, OwnerId, Account__c, Account__r.Name, Contact__c, 
            Contact__r.Name, Contact_Text__c, Created__c, Modified__c, Name__c, ReviewURL__c, SandName__c, SpotterId__c, User__c 
            from Inspection__c where Id = :inspection.Id limit 1];
        
        if(fullInsp == null)
            return inspNode;

        Set<Id> editIDs = new Set<Id>(nodesToEdit);
        
        Map<string, SObjectTreeNode> saveSObjects = new Map<string, SObjectTreeNode>();
        for(SObjectTreeNode saveNode : nodesToSave)
            saveSObjects.put(saveNode.recordId, saveNode);
        
        SObject nodeSObject = saveSObjects.containsKey(fullInsp.Id) ? saveSObjects.get(fullInsp.Id).editSObject : fullInsp;
        
        inspNode = new SObjectTreeNode('Inspection: ' + fullInsp.Name, 0, nodeSObject, editIDs.contains(fullInsp.Id));
        inspNode.cssClass = 'inspection level0 ' + (editIDs.contains(fullInsp.Id) ? 'editMode' : 'viewMode');
        inspNode.compareValue = fullInsp.Name;
        inspNode.showAll(editPageInspFields);	 
        
        List<Category__c> inspCategories = [ 
            select Id, Name, CreatedById, LastModifiedById, LastModifiedBy.Name, OwnerId, Created__c, Inspection__c, IsNotApplicable__c, Modified__c, Name__c, 
            Q_SortOrder__c, Q_Sort_Order__c, ReviewURL__c, SortOrder__c, SpotterId__c from Category__c where Inspection__c = : inspection.Id];
        
        if(inspCategories.size() == 0)
            return inspNode;
        
        for(Category__c category : inspCategories)
        {
            SObjectTreeNode catNode = new SObjectTreeNode('Category: ' + category.Name, 1, category, editIDs.contains(category.Id));
            
            inspNode.addChild(catNode);
            catNode.cssClass = 'category level1';
            catNode.compareValue = category.Q_Sort_Order__c;
            catNode.showAll(editPageCatFields);
            
            if(category.IsNotApplicable__c)
                continue;
            
            List<Question__c> catQuestions = [
                select Id, Name, CreatedById, CreatedBy.Name, LastModifiedById, LastModifiedBy.Name, Q_SortOrder__c, 
                ParentAdditionalQuestion__c, ParentDependentQuestion__c, Tag__c, Group_Name__c, IsNotApplicable__c, Text__c, Answer__c, 
                Long_Answer_del__c, Flag__c, ReviewUrl__c, 
                (select Id, Name, CreatedById, CreatedBy.Name, LastModifiedById, LastModifiedBy.Name, ContentType__c, 
                 Media_Preview__c, MediaType__c, Question__c, Question__r.Tag__c, Title__c, URL__c from QuestionMedia__r) 
                from Question__c where Category__c = :category.Id];
            
            Set<SObjectTreeNode> unprocessed = new Set<SObjectTreeNode>();
            for(Question__c question : catQuestions)
            {
                SObjectTreeNode qstnNode = new SObjectTreeNode('Question: ' + question.Tag__c, 2, question, editIDs.contains(question.Id));    
                unprocessed.add(qstnNode);
                qstnNode.cssClass = 'question';
                qstnNode.compareValue = question.Q_SortOrder__c;
                if(question.Flag__c != null && question.Flag__c != 'None') 
                    qstnNode.addCssClass('flag' + question.Flag__c);
                qstnNode.showAll(editPageQuesFields);
            }
            catNode.addChildren(processQuestions(unprocessed, editIDs));
        }
        SObjectTreeNode.deepSortNodeList(inspNode.childNodes);
        return inspNode;
    }
    
    public static List<SObjectTreeNode> processQuestions(Set<SObjectTreeNode> unprocessed, Set<Id> editIDs)
    {
	// ... some code that sorts the questions and puts them in the proper tree structure
    }
    
    public List<Id> nodesToEdit {
        get
        {
            if(nodesToEdit == null)
                nodesToEdit = new List<Id>();
            return nodesToEdit;
        }
        private set;
    }    
    
    public List<SObjectTreeNode> nodesToSave {
        get
        {
            if(nodesToSave == null)
                nodesToSave = new List<SObjectTreeNode>();
            return nodesToSave;
        }
        private set;
    }    
    
    public SObjectTreeNode inspectionNode {
        get {
           
            // Edit: Rebuild, setting specified to Edit Mode
            // Cancel: Rebuild, setting all to View Mode
            // Save: Rebuild, setting all to View Mode, then replace Save nodes with
            
            inspectionNode = createInspectionTree(thisInspection, nodesToEdit, nodesToSave);
            for(integer i = nodesToSave.size()-1; i >= 0; i--)
            {
                nodesToSave[i].upsertSObject();
                nodesToSave.remove(i);
            }            
            return inspectionNode; 
        }
        set;
    }
    
    
    /*****  Constructors  *****/
    
    public inspTreePageController(ApexPages.StandardController stdController) 
    {
        thisInspection = (Inspection__c)stdController.getRecord();
        
        if(thisInspection.Id == null)
            thisInspection = getTestInspection();        
    }
    
}