Cannot edit a record in TableName. An update conflict occurred due to another user process deleting the record or changing one or more fields in the record.

I will show how to deal with most update conflict errors based on my case in standard application.
Particular error from the case: Cannot edit a record in Inventory transactions (InventTrans). An update conflict occurred due to another user process deleting the record or changing one or more fields in the record.
It occurs when the Item arrival journal is posted (against the Purchase order, which is part of the intercompany structure).
Failure is in InventUpd_Registered.pdsAdjustRegisteredInventoryQuantities():

The fix from Microsoft for a similar update conflict is awkward. The only field, that is used from InventTrans buffer is MarkingRefInventTransOrigin and it is not updated. The same information seems contained in inventTransCachedFieldsOnly buffer. So it should be possible to select the InventTrans record for update just before "inventTrans.updateSumUp();" line:

As you can see from the screenshot above inventTrans record buffer is not passed to any method and the buffer data is not updated, therefore it is safe to reread data. We have only two possibilities to reread the record before the actual update:
1. InventTrans.updateSumUp()
2. InventTrans.update()

We should always select the earliest place in the code before any changes are applied to the table buffer. No changes are made on the InventTrans buffer before InventTrans.updateSumUp() is called. Therefore it is safe to execute the record reread in CoC on updateSumUp() before actual logic is executed. As we can see Microsoft does reread() above to fix similar update conflict cases.

1. Using the CoC extension on InventUpd_Registered.updateNow method I will create a context to isolate the case:
/// <summary>
/// An extension class of the <c>InventUpd_Registered</c> class.
/// </summary>
[ExtensionOf(classStr(InventUpd_Registered))]
final class VKInventUpd_RegisteredCls_Extension
{
    /// <summary>
    /// CoC extension of the updateNow() method
    /// </summary>
    public void updateNow()
    {
        using(VKUpdateConflictInventTransContext context = VKUpdateConflictInventTransContext::construct())
        {
            next updateNow();
        }
    }

}

2. With CoC extension on the InventTrans table, I will track the record version using "find*" and update methods. I will also use InventTrans.updateSumUp to identify potential update conflicts and resolve them with reread:
/// <summary>
/// CoC extension for InventTrans table
/// </summary>
[ExtensionOf(tableStr(InventTrans))]
final class VKInventTransTbl_Extension
{
    public void update(
        NoYes       _dropInventOnHand,
        NoYes       _canDropInventSumDelta,
        InventDimId _inventDimIdTransferIssue)
    {
        next update(_dropInventOnHand, _canDropInventSumDelta, _inventDimIdTransferIssue);

        VKUpdateConflictInventTransContext context = VKUpdateConflictInventTransContext::current();
        if (context)
        {
            context.recordUpdated(this);
        }
    }

    /// <summary>
    /// CoC for findByInventTransOrigin method
    /// </summary>
    /// <param name = "_inventTransOriginId">InventTransOriginId</param>
    /// <param name = "_forUpdate">boolean</param>
    /// <returns>InventTrans</returns>
    public static InventTrans findByInventTransOrigin(
        InventTransOriginId _inventTransOriginId,
        boolean             _forUpdate)
    {
        InventTrans inventTrans = next findByInventTransOrigin(_inventTransOriginId, _forUpdate);

        if (_forUpdate)
        {
            VKUpdateConflictInventTransContext context = VKUpdateConflictInventTransContext::current();
            if (context)
            {
                context.recordSelectedForUpdate(inventTrans);
            }
        }

        return inventTrans;
    }

    /// <summary>
    /// CoC for findRecId method
    /// </summary>
    /// <param name = "_recId">RecId</param>
    /// <param name = "_forUpdate">boolean</param>
    /// <returns>InventTrans</returns>
    public static InventTrans findRecId(
        RecId       _recId,
        boolean     _forUpdate)
    {
        InventTrans inventTrans = next findRecId(_recId, _forUpdate);

        if (_forUpdate)
        {
            VKUpdateConflictInventTransContext context = VKUpdateConflictInventTransContext::current();
            if (context)
            {
                context.recordSelectedForUpdate(inventTrans);
            }
        }

        return inventTrans;
    }

    /// <summary>
    /// CoC for updateSumUp method
    /// </summary>
    /// <param name = "_always">NoYes</param>
    /// <param name = "_disableCacheForBufferCheck">boolean</param>
    /// <param name = "_deletedTransactions">Set</param>
    public void updateSumUp(NoYes _always, boolean _disableCacheForBufferCheck, Set _deletedTransactions)
    {
        VKUpdateConflictInventTransContext context = VKUpdateConflictInventTransContext::current();
        if (context)
        {
            if (context.shouldReread(this))
            {
                this.reread();
            }
        }
        next updateSumUp(_always, _disableCacheForBufferCheck, _deletedTransactions);
    }

}
3. General class with common logic to resolve update conflicts
/// <summary>
/// The class provides common logic to resolve table conflicts using context (isolated cases)
/// </summary>
abstract class VKUpdateConflictCommon implements System.IDisposable
{
    protected Map recVersionMap;

    protected void new()
    {
        recVersionMap = new Map(Types::Int64, Types::Int64);
    }

    /// <summary>
    /// Should be called when record is selected forUpdate
    /// </summary>
    /// <param name = "_record">Record</param>
    public void recordSelectedForUpdate(common _record)
    {
        if (recVersionMap.exists(_record.RecId))
        {
            recVersionMap.remove(_record.RecId);
        }
        recVersionMap.insert(_record.RecId, _record.RecVersion);
    }

    /// <summary>
    /// Should be called when record is updated (after super)
    /// </summary>
    /// <param name = "_record">Record</param>
    public void recordUpdated(common _record)
    {
        this.recordSelectedForUpdate(_record);
    }

    /// <summary>
    /// Call before record update to understand if there will be update conflict
    /// </summary>
    /// <param name = "_record">Record</param>
    /// <returns>Boolean</returns>
    public boolean shouldReread(common _record)
    {
        boolean ret = false;

        if (recVersionMap.exists(_record.RecId) && _record.RecVersion != recVersionMap.lookup(_record.RecId))
        {
            ret = true;
        }

        return ret;
    }

    /// <summary>
    /// Safely reread table buffer in case it has been updated to avoid update conflict.
    /// </summary>
    /// <param name = "_record">Table buffer record.</param>
    public static void safeReread(Common _record)
    {
        Common record       = _record;
        Common recordOrig   = _record.orig();
 
        DictTable dictTable = new DictTable(_record.TableId);
        DictField dictField;
 
        // Compare a source record with its original buffer to collect FieldId/Value pairs for updated fields.
        Map updatedFieldsMap = new Map(Types::Integer, Types::Container);
 
        for (int i = 1; i <= dictTable.fieldCnt(); i++)
        {
            dictField = new DictField(record.TableId, dictTable.fieldCnt2Id(i));
            if (!isSysId(dictField.id()) && record.(dictField.id()) != recordOrig.(dictField.id()))
            {
                updatedFieldsMap.add(dictField.id(), [record.(dictField.id())]);
            }
        }
 
        // Reread a source record
        record.reread();

        // Apply previously collected field values.
        if (updatedFieldsMap.elements() > 0)
        {
            MapEnumerator updatedFieldsMapEnum = updatedFieldsMap.getEnumerator();
            while (updatedFieldsMapEnum.moveNext())
            {
                if ([record.(updatedFieldsMapEnum.currentKey())] != updatedFieldsMapEnum.currentValue())
                {
                    [record.(updatedFieldsMapEnum.currentKey())] = updatedFieldsMapEnum.currentValue();
                }
            }
        }
    }

    abstract public void dispose()
    {
    }

}
In some cases, it might be possible to resolve the update conflicts on the table buffer, which is changed, on the update() method before super() with the use of VKUpdateConflictCommon::safeReread(), but there is a risk of incorrect data update and data inconsistency. I definitely do not recommend doing it with the InventTrans record. Therefore you must thoroughly evaluate specific cases before using VKUpdateConflictCommon::safeReread().

4. InventTrans record update conflict context. Create a separate for each unique table.
/// <summary>
/// InventTrans record update conflict context
/// </summary>
public class VKUpdateConflictInventTransContext extends VKUpdateConflictCommon
{
    static VKUpdateConflictInventTransContext instance;

    protected void new()
    {
        if(instance)
        {
            // Nesting of %1 is not supported
            throw error(strFmt("@WAX:NestingOfContextIsNotSupportedWarning", classStr(VKUpdateConflictInventTransContext)));
        }
        instance = this;

        super();
    }

    static public VKUpdateConflictInventTransContext construct()
    {
        VKUpdateConflictInventTransContext ret = VKUpdateConflictInventTransContext::current();

        if (!ret)
        {
            ret = new VKUpdateConflictInventTransContext();
        }

        return ret;
    }

    public void dispose()
    {
        instance = null;
    }

    /// <summary>
    /// Get the context if exists
    /// </summary>
    /// <returns>VKUpdateConflictInventTransContext</returns>
    static public VKUpdateConflictInventTransContext current()
    {
        return instance;
    }

}


Support The Author

 If you found value in what I share, I've set up a Buy Me a Coffee page as a way to show your support.

Buy Me a Coffee

Post a Comment


All Comments


No comments. Be the first one to comment on this post.

Search

About

DaxOnline.org is free platform that allows you to quickly store and reuse snippets, notes, articles related to Dynamics AX.

Authors are allowed to set their own "buy me a coffee" link.
Join us.

Blog Tags