Data / ML · Customization

Updating Multiple Records with PXDataField in Acumatica

A client wanted a button on a Sales Order screen: select a batch of open orders in a grid, click "Apply Price Increase," and have a percentage adjustment applied across every.

John Kihiu12 min read

A client wanted a button on a Sales Order screen: select a batch of open orders in a grid, click "Apply Price Increase," and have a percentage adjustment applied across every selected line, for every selected order, in one action. My first draft did exactly what you'd expect - loop over selected rows, mutate them through the cache, call Save. It worked for a handful of orders. It fell over, slowly and visibly, once the client's actual usage pattern turned out to be several hundred orders at once. That's the story of every "why is PXDataField update slow" investigation I've done since: the honest answer to "should I use it" is almost always "it depends how many records, and whether you actually need the pipeline."

PXDataField and PXDataFieldAssign: SQL-level field updates, not cache mutation

PXDatabase.Update combined with PXDataField selectors and PXDataFieldAssign values lets you write a direct, set-based SQL UPDATE against a table, matching rows by whatever WHERE-equivalent fields you specify, entirely outside any graph's cache. It's the update-side counterpart to PXDatabase.Insert - same tradeoff, same bypass of the event pipeline, same reason it exists: doing a bulk operation as one SQL statement instead of N cache-driven saves.

C# · Direct multi-record update, no cache involved
PXDatabase.Update<SOLine>(
    new PXDataFieldAssign<SOLine.curyUnitPrice>(newPrice),
    new PXDataFieldRestrict<SOLine.orderType>(PXDbType.Char, 2, "SO"),
    new PXDataFieldRestrict<SOLine.orderNbr>(PXDbType.NVarChar, 15,
        orderNbr, PXComp.EQ)
);
// One UPDATE statement, one order's lines. No FieldUpdated fires,
// no dependent totals recalculate, no attribute re-validates.

This is dramatically faster than a cache-driven loop for genuinely bulk operations, for the same reason PXDatabase.Insert is faster than cache.Insert: there's no per-row, per-field event pipeline to pay for. It is also, just like PXDatabase.Insert, blind to everything that pipeline used to do - dependent field recalculation, cross-DAC totals, validation.

Why my first "obvious" fix for the price increase button was wrong twice, not once

My first version used the cache and was correct but slow. My second attempt reached straight for PXDatabase.Update and was fast but wrong - it updated CuryUnitPrice directly and left CuryLineAmt, the order's CuryOrderTotal, and the sales order's own cached totals completely stale, because none of that recalculation logic ran. The order screen looked wrong the next time anyone opened it, since those aggregate fields are normally kept correct by the same event handlers PXDatabase.Update skips.

Bulk SQL updates on fields with dependent calculations need a second pass

If the field you're updating in bulk feeds into any calculated total elsewhere - line amount from unit price and quantity, document total from line amounts, anything a RowPersisting or FieldUpdated handler normally recomputes - a raw PXDataField update leaves those totals stale until something forces a recalculation. Either recompute the dependent fields in the same bulk SQL pass explicitly, or accept the cache-driven cost specifically for rows where recalculation actually matters.

What I actually shipped: cache-driven, but bounded and batched

For the price increase feature, the honest fix wasn't a bigger hammer, it was recognizing the record counts involved (typically dozens of orders, a few hundred lines) didn't actually justify bypassing the pipeline at all - the totals recalculation was the entire point of the feature. What I did instead was bound the operation properly: process orders individually through the graph's normal cache-driven update-and-save, but structure it as a background processing job (PXLongOperation) rather than a synchronous UI action, so a few hundred orders didn't time out the request even though each one paid full pipeline cost.

C# · Cache-driven, but off the UI thread and processed in batches
protected virtual IEnumerable applyPriceIncrease(PXAdapter adapter)
{
    var selected = new PXAdapter(adapter).Get<SOOrder>()
        .RowCast<SOOrder>().ToList();

    PXLongOperation.StartOperation(Base, () =>
    {
        var graph = PXGraph.CreateInstance<SOOrderEntry>();
        foreach (SOOrder order in selected)
        {
            graph.Clear();
            graph.Document.Current = graph.Document.Search<SOOrder.orderNbr>(order.OrderNbr);
            foreach (SOLine line in graph.Transactions.Select())
            {
                line.CuryUnitPrice *= (1 + pricePct / 100m);
                graph.Transactions.Update(line); // full pipeline: totals recalc correctly
            }
            graph.Save.Press();
        }
    });
    return adapter.Get();
}

Where PXDataField bulk updates are actually the right call

Reach for PXDataField/PXDatabase.Update when the field being changed has no dependent calculations to worry about, and the record count is large enough that per-row pipeline cost is the actual bottleneck - flipping a status flag across thousands of archived records, clearing a deprecated custom field during a cleanup migration, updating an audit stamp on historical rows nobody's screen currently has open. None of those need FieldUpdated to fire for anything downstream to stay correct, which is exactly the condition that makes bypassing the pipeline safe rather than merely fast.

Wrapping up

PXDataField-based bulk updates buy real speed by skipping the event pipeline entirely, the same tradeoff as PXDatabase.Insert on the write side. Before using it on a field with dependent totals or cross-field validation, ask honestly whether the record count actually justifies bypassing recalculation - often the right fix isn't a faster write path, it's moving a correctly-computed cache-driven update onto a background operation so it doesn't need to be fast, just non-blocking.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.