5Best Practices for Groovy Performance
Best Practices for Groovy Performance
Following the advice in this section will ensure your application has the best performance possible.
Search Using at Least One Indexed Field
Explicitly Select Only the Attributes You Need
When performing a business object query, it's important to indicate which fields your code will access from the results. This includes fields your logic plans to update as well. By doing this proactively, your application gains two advantages:
- You retrieve only the data you need from the database, and
- You avoid an additional system-initiated query to "fault-in" missing data on first reference
selectAttributesBeforeQuery()
function to select the attributes your code will access before it performs a view object's query. As shown in the example below, the first parameter is a view object you have created with
newView()
and the second argument is a case-sensitive list of field names. If you are including a sort in your query by calling
setSortBy()
, make sure to include the sort field name(s) in the selected attributes list as well.
def employees = newView('StaffMember') addBindVariable(employees,'Job','Text') addBindVariable(employees,'Dept','Number') // Make sure that JobId or DepartmentId is indexed! employees.appendViewCriteria('JobId = :Job and DepartmentId = :Dept') employees.setSortBy('Salary desc') selectAttributesBeforeQuery(employees,['Email','LastName','FirstName','Salary']) setBindVariable(employees,'Job','SH_CLERK') setBindVariable(employees,'Dept',50) employees.executeQuery() while (employees.hasNext()) { def employee = employees.next() // Work with employee.Email, employee.LastName, employee.FirstName, employee.Salary }
selectAttributesBeforeQuery()
function before executing a view object for a custom object you've created with
newView()
, then by default the query will retrieve only the primary key field from the database when initially performing the query, and then
for each row of the
while
loop as soon as your code references one of the other attributes like
Email
,
LastName
,
FirstName
, or
Salary
, the system is forced to perform an additional query to retrieve
all of the current row's fields from the database using the primary key. If your object has 200 fields, this means retrieving 200 fields of data even through your code may actually reference only four of them. This can quickly lead to your application's performing many, many avoidable extra queries and fetching much unnecessary data. Neither of these situations is good for performance.
Test for Existence by Selecting a Single Row
When you need to check if at least one row matches a particular criteria, for best performance select only the primary key field and just the first row of the result. If it's not null
, then the existence test succeeds. If it's null
, then no such row exists. Use this technique instead of calling getEstimatedRowCount()
. For example, to test whether any employee exists in a given department with a given job identifier, you can write a helper function like this:
Object Function: Boolean employeeExistsInDepartmentWithJob( Long department, String jobCode)
def employees = newView('StaffMember') addBindVariable(employees,'Job','Text') addBindVariable(employees,'Dept','Number') // Make sure that either JobId or DepartmentId is indexed! employees.appendViewCriteria('JobId = :Job and DepartmentId = :Dept') // Retrieve only the primary key field selectAttributesBeforeQuery(employees,['EmployeeId']) setBindVariable(employees,'Job',jobCode) setBindVariable(employees,'Dept',department) employees.executeQuery() // Retrieve just the first row! return employees.first() != null
employeeExistsInDepartmentWithJob()
helper function in place, our business logic in the
StaffMember
object can use it like this:
if (employeeExistsInDepartmentWithJob(50,'SH_CLERK')) { /* etc. */ }
Avoid Using newView() Inside a Loop
newView()
inside a loop can lead to unpredictable
ExprResourceException
errors when you inadvertently create more view objects than the system allows in a single trigger or object function. For example, consider the following code that iterates over an unknown number of uncleared transaction records. For each uncleared transaction processed, if the transaction currency is not
GBP
then it queries the historical exchange rate based on the transaction date to convert the non-
GBP
currency amount in question into
GBP
. It uses the
newView()
function inside the loop to query the
ExchangeRate
business object and does so without using bind variables. This approach creates one new view object for each loop iteration. If the number of transaction rows being iterated over is unpredictably large, this technique can produce an
ExprResourceException
error when it hits the upper limit on number of view objects that can be created in a single trigger or function.
// NON-BEST-PRACTICE EXAMPLE: USES newView() INSIDE A LOOP !! // ~~~~~~~~~~~~~~~~~~~~~~~~~ May lead to unpredictable ExprResourceException error // Create view object for processing uncleared transactions def txns = newView('Transaction') txns.appendViewCriteria("cleared = 'N'") selectAttributesBeforeQuery(txns,['id','cleared','currency','amount','txnDate']) txns.executeQuery() // Process each uncleared transaction while (txns.hasNext()) { def exchangeRate = 1 def txn = txns.next() def curr = txn.currency if (curr != 'GBP') { def date = txn.txnDate // NON-BEST PRACTICE: USE OF newView() INSIDE A LOOP !! def rates = newView('ExchangeRate') rates.appendViewCriteria("fromCurr = '${curr}' and toCurr = 'GBP' and rateDate = '${date}'") rates.executeQuery() exchangeRate = rates.first()?.rate } if (exchangeRate) { txn.cleared = 'Y' // Multiply original txn amount by rate and round to 2 decimal places txn.amountInGBP = (txn.amount * exchangeRate as Double).round(2) } }
- Create a single view object outside the loop that references bind variables in its filter criteria
- Inside the loop, set the values of the bind variables for the current loop iteration
- Execute the query on the single view object once per loop iteration
By adopting this technique, you use a single view object instead of an unpredictably large number of view objects and you avoid encountering the ExprResourceException
when iterating over a larger number of rows. The code below implements the same functionality as above, but follows these best practice guidelines.
// BEST PRACTICE: Single VO with bind variables outside the loop // ~~~~~~~~~~~~~ // Create view object to be reused inside the loop for exchange rates def rates = newView('ExchangeRate') addBindVariable(rates,'Base','Text') addBindVariable(rates,'ForDate','Date') rates.appendViewCriteria("fromCurr = :Base and toCurr = 'GBP' and rateDate = :FromDate") // Create view object for processing uncleared transactions def txns = newView('Transaction') txns.appendViewCriteria("cleared = 'N'") selectAttributesBeforeQuery(txns,['id','cleared','currency','amount','txnDate']) txns.executeQuery() // Process each uncleared transaction while (txns.hasNext()) { def exchangeRate = 1 def txn = txns.next() def curr = txn.currency if (curr != 'GBP') { def date = txn.txnDate // BEST PRACTICE: Set bind variables & execute view object created outside loop setBindVariable(rates,'Base',curr) setBindVariable(rates,'ForDate',date) rates.executeQuery() exchangeRate = rates.first()?.rate } if (exchangeRate) { txn.cleared = 'Y' // Multiply original txn amount by rate and round to 2 decimal places txn.amountInGBP = (txn.amount * exchangeRate as Double).round(2) } }
If the functionality inside the loop becomes more involved, you may benefit by refactoring it into an object function. The object function below shows an exchangeRateForCurrencyOnDate()
helper function that accepts the single view object created outside the loop as a parameter of type Object
. Inside the function, it sets the bind variables, executes the view object's query, and returns the resulting exchange rate.
Object Function: Float exchangeRateForCurrencyOnDate( Object rates, String curr, Date date)
// Set bind variables and execute view object passed in setBindVariable(rates,'Base',curr) setBindVariable(rates,'ForDate',date) rates.executeQuery() return rates.first()?.rate
After refactoring the code into this object function, the if
block in the original best-practice code above can be changed to:
// etc. if (curr != 'GBP') { def date = txn.txnDate // Pass single 'rates' view object into the helper function rate = exchangeRateForCurrencyOnDate(rates,curr,date) } // etc.
Set Field Values in Bulk
setAttributeValuesFromMap()
function. Using these bulk-assignment functions saves processing time and can eliminate avoidable queries related to your Dynamic Choice List and Fixed Choice List attribute validation when compared to the equivalent job performed one field at a time. For example, the following code example sets the values of five fields of an existing staff member row. The code finds an employee by its employee
id
value which we know is always indexed.
// Find an existing staff member #123456789 by the indexed primary key // field id, then bulk-assign 5 field values whose names are also included // in the view object's select list to avoid unnecessary "fault-in" queries. def employees = newView('StaffMember') addBindVariable(employees,'bind_id','Number') employees.appendViewCriteria('id = :bind_id') selectAttributesBeforeQuery(employees, ['id','email','carMake','carModel','vacation','accrualDate']) setBindVariable(employees,'bind_id',123456789) employees.executeQuery() def emp = employees.first() if (emp) { emp.setAttributeValuesFromMap( email: emp.email.replace('old.org','new.org'), carMake: 'VW', carModel: 'GLF', vacation: 160, accrualDate: today()) }When creating a new row, you can accomplish the same bulk assignment task using the
createAndInitRowFromMap()
function. The following example creates a new staff member assigning all fields in bulk:
def emps = newView('StaffMember') // If StaffMember view object will only be used for insert, then // this call will stop any query from being performed emps.setMaxFetchSize(0) // Insert a new staff member, setting all necessary fields in bulk emps.insertRow(emps.createAndInitRowFromMap( Email: 'jane.barnes@example.org', CarMake: 'AUD', CarModel: 'A8', Vacation: 200, AccrualDate: today()))
Both examples in this section illustrate Groovy's support for removing the square brackets around a literal Map
passed inline to a function with a leading Map
argument. To learn more about how your own functions can leverage this feature, see Using Optional, Named Method Arguments.
setAttributeValues()
function. The example below shows how it may fit your situation better than
setAttributeValuesFromMap()
. This alternative function accomplishes the same performance improvement. It assumes you've created another
error()
helper method to throw an error with a given message.
// void doBulkAssignment(Object row, List fieldNames, List fieldValues) // accepting row to assign, field names and field values as separate Lists if (fieldNames.size() == fieldValues.size()) { row.setAttributeValues(fieldNames, fieldValues) } else { error("Must supply same number of fields and values to assign!") }
Avoid Revalidating Known Valid Data
Normally your business logic will use the equals sign assigment operator to set a single field's value, or use the setAttributeValuesFromMap()
or createRowAndInitFromMap()
functions to set two or more field values in bulk. Any fields assigned through these methods will be validated by any field and object-level validation rules that are defined to ensure that the business object data saved to the database is always 100% valid.
On special occasions, you may know a priori that the value your code assigns to a field is already valid. In cases where you are 100% certain the value being assigned to a field is valid, you can consider using the populateValidAttribute()
function to knowingly assign a valid value to a field without causing additional validation to occur.
// NOTE: Consciously assigning a known-valid value // ~~~~ without further validation! order.populateValidAttribute('OrderStatus','CLOSED')
Use Left Shift Operator To Append to Lists
<<
) or call the list's
add()
function for best performance. For example, the following code processes a collection of products and adds the value of the
Id
field from a subset of the products encountered to a new list:
list productIdsToProcess = [] for (prod in products) { if (prod.Status == 'RETURNED') { // Append the current product id to the list // Same as calling productIdsToProcess.add(prod.Id) productIdsToProcess << prod.Id } }
This technique is better than using the plus or plus-equals operator to do the same job because both of those create a new list each time.