Persisting Data in Acumatica Events – Part 2: Advanced Techniques
In Part 1, we explored the foundations of data persistence in Acumatica: the PXCache object, its primary data modification methods, and the persistence flow. Now, in Part 2, we'll dive deeper into persisting data within specific events, advanced techniques using PXDatabase, and critical patterns for avoiding infinite recursion.
⚡ Key Insight: Different events require different persistence strategies. Knowing when to use Insert/Update/Delete versus PXDatabase versus overriding Persist is essential for building robust customizations.
Persisting Within Specific Events
The event you're working in determines how and when you should persist data. Here are the most common scenarios:
1. Persisting During RowPersisting
The RowPersisting event is raised just before a record is saved to the database. At this point, a database transaction is already open. This is the ideal place to:
- Perform final validation before data is saved
- Modify data that should be stored in the same transaction
- Cancel the save operation if business rules are violated
protected virtual void _(Events.RowPersisting<SOOrder> e)
{
var order = e.Row;
// Validate business rule
if (order.TotalAmount > 1000000 && !IsManagerApproved(order))
{
// Cancel the save operation
e.Cancel = true;
throw new PXException("Orders over 1,000,000 require manager approval.");
}
// Modify data before save
if (order.DiscountPercent > 0)
{
order.CalculatedDiscount = order.TotalAmount * order.DiscountPercent / 100;
// The cache will automatically pick up changes to the same record
}
}
💡 Pro Tip: Avoid creating new records during RowPersisting. Since the transaction is already open, any Insert or Update operations on other caches within the same handler can lead to unexpected behavior or deadlocks. Instead, create new records in RowUpdated or RowInserted.
2. Persisting During RowPersisted
The RowPersisted event is raised after a record is saved to the database. At this point, the transaction is either committed (TranStatus = Completed) or aborted (TranStatus = Aborted). This is the ideal place for:
- Creating related records that depend on the parent record's ID
- Triggering external system updates
- Logging operations
protected virtual void _(Events.RowPersisted<SOOrder> e)
{
if (e.TranStatus != PXTranStatus.Completed) return;
var order = e.Row;
// Create related audit record after successful save
var audit = new OrderAudit
{
OrderNbr = order.OrderNbr,
UserID = Base.Accessinfo.UserID,
Action = "Order Created",
Timestamp = DateTime.Now
};
// Use a separate graph to avoid conflicts
using var auditGraph = PXGraph.CreateInstance<AuditGraph>();
auditGraph.AuditRecords.Insert(audit);
auditGraph.Actions.PressSave();
}
3. Persisting During RowUpdated/RowInserted
These events occur immediately after the update or insert operation in the cache, before the transaction is committed. This is the ideal place for:
- Creating related child records that need to be saved in the same transaction
- Cross-cache updates that must be consistent
- Validation that depends on the new state of the record
protected virtual void _(Events.RowInserted<SOOrder> e)
{
var order = e.Row;
// Create default lines for the order
var defaultItems = GetDefaultItemsForCustomer(order.CustomerID);
foreach (var item in defaultItems)
{
var line = new SOLine
{
OrderNbr = order.OrderNbr,
InventoryID = item.InventoryID,
Quantity = item.DefaultQty,
UnitPrice = item.DefaultPrice
};
Base.Transactions.Insert(line);
}
}
Using PXDatabase for Direct Data Operations
The PXDatabase class provides methods for performing direct database operations that bypass the cache and event system. Use this when:
- You need to perform bulk operations that don't require event logic
- You want to avoid triggering events on every record
- You need to update data outside the current graph's caches
- Performance is critical and you want to minimize overhead
Example: Bulk Update with PXDatabase
protected virtual void _(Events.RowPersisted<SOOrder> e)
{
if (e.TranStatus != PXTranStatus.Completed) return;
var order = e.Row;
// Update all open orders for this customer with a new discount
string sql = @"
UPDATE SOOrder
SET DiscountPercent = @Discount
WHERE CustomerID = @CustomerID
AND Status = 'Open'
AND OrderNbr != @CurrentOrderNbr";
PXDatabase.Execute(sql, new object[]
{
new PXDataField("@Discount", 5),
new PXDataField("@CustomerID", order.CustomerID),
new PXDataField("@CurrentOrderNbr", order.OrderNbr)
});
}
Using PXDataRecord for Data Retrieval
// Read data directly from the database
string selectSql = @"
SELECT COUNT(*)
FROM SOOrder
WHERE CustomerID = @CustomerID
AND Status = 'Open'";
int openOrdersCount = PXDatabase.SelectScalar(selectSql,
new object[] { new PXDataField("@CustomerID", customerID) });
if (openOrdersCount > 0)
{
// Apply business logic based on the count
}
⚠️ Important: PXDatabase bypasses all Acumatica events, security, and business logic. Use it carefully and only when necessary. Avoid using it to modify data that should trigger workflows, validations, or other event-driven logic.
Overriding Persist and PerformPersist
For advanced scenarios where you need complete control over the persistence process, you can override the Persist() or PerformPersist() methods in your graph.
Overriding Persist
Overriding Persist() allows you to execute custom logic before, during, or after the base persistence operation. You can use PXOverride or override the virtual method directly.
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
[PXOverride]
public void Persist(Action baseMethod)
{
// Pre-persist validation
ValidateAllOrders();
// Call the base persist
baseMethod();
// Post-persist actions
SendOrderConfirmation();
}
}
Overriding PerformPersist
Overriding PerformPersist() provides even more granular control over the persistence process. It's executed immediately before the actual database operations begin.
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
[PXOverride]
public void PerformPersist(Action baseMethod)
{
// Custom logic before persistence
if (IsSpecialProcessingRequired())
{
PrepareSpecialData();
}
// Invoke the base method with a wrapper to track operations
try
{
baseMethod();
LogSuccess();
}
catch (Exception ex)
{
LogFailure(ex);
throw;
}
}
}
💡 Pro Tip: When overriding Persist or PerformPersist, always call the base method to ensure the standard persistence flow is executed. Failing to do so can break the entire graph's functionality.
Avoiding Infinite Recursion
One of the most common issues when persisting data within events is infinite recursion—where an event handler modifies data, which triggers the same event again, leading to an endless loop.
The Problem
// ❌ Dangerous: Infinite recursion
protected virtual void _(Events.RowUpdating<SOOrder> e)
{
var order = e.Row;
order.Description = "Updated: " + order.Description;
Base.Orders.Update(order);
// This causes RowUpdating to fire again, leading to infinite recursion
}
Solution 1: Use a Flag
private bool _isProcessing = false;
protected virtual void _(Events.RowUpdating<SOOrder> e)
{
if (_isProcessing) return;
_isProcessing = true;
try
{
var order = e.Row;
order.Description = "Updated: " + order.Description;
Base.Orders.Update(order);
}
finally
{
_isProcessing = false;
}
}
Solution 2: Check e.Cache.GetStatus(e.Row)
protected virtual void _(Events.RowUpdating<SOOrder> e)
{
var order = e.Row;
// Only update if the record hasn't been modified yet
if (e.Cache.GetStatus(order) != PXEntryStatus.Updated)
{
order.Description = "Updated: " + order.Description;
Base.Orders.Update(order);
}
}
Solution 3: Use FieldUpdated Instead of RowUpdating
// ✅ Safer: Only triggers on specific field changes
protected virtual void _(Events.FieldUpdated<SOOrder, SOOrder.customerID> e)
{
var order = e.Row;
// This will only fire when CustomerID changes
order.Description = "Customer changed to: " + GetCustomerName(order.CustomerID);
Base.Orders.Update(order);
}
Best Practices Summary
✅ Use RowPersisted for Post-Save Actions
Create related records, trigger external systems, or log operations after the transaction is committed.
✅ Use RowInserted/RowUpdated for In-Transaction Updates
Create child records or update other caches within the same transaction.
✅ Use PXDatabase for Bulk Operations
When performance is critical and event logic isn't needed.
✅ Always Prevent Recursion
Use flags, status checks, or specific event handlers to avoid infinite loops.
Coming in Part 3
In the final part of this series, we'll explore:
- Validation Strategies: When to use PXException vs e.Cancel vs RowPersisting
- Cross-Table Persistence: Working with multiple caches and graphs
- Persisting with Transactions: Handling rollbacks and transaction isolation
- Performance Optimization: Techniques for high-volume data persistence
📌 Key Takeaway: Understanding when and how to persist data in Acumatica events is critical for building robust customizations. Use the right method for the right scenario, and always be mindful of transaction boundaries and recursion risks.
🔍 Further Reading: Review the Acumatica Developer Guide for detailed information on event sequences and transaction handling. Understanding these concepts will make your customizations more reliable and maintainable.