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

Whenever you perform a query, make sure that your view object's view criteria filter includes at least one indexed field in the predicate. Especially when the amount of data is large, using at least one index to filter the data makes a meaningful difference in application query performance.
Important: Failure to use an index of any kind for your an application business logic query implies the database will perform a full table scan that can be a recipe for slow response times and unhappy end users.

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:

  1. You retrieve only the data you need from the database, and
  2. You avoid an additional system-initiated query to "fault-in" missing data on first reference
Call the 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
}
Important: If you fail to call the 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
With the 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

Using 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)   
  }
}
In these situations, use this approach instead:
  • 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

Wherever possible in your code, for best performance set the values of all fields in a row in a single call to the 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.

When writing generic helper code, if you find it more convenient to process the field names to assign and corresponding values to assign in separate lists, then consider using the 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.

For example, your code can change the value of an order's OrderStatus field to one of the values that you know is valid like CLOSED by using the following code:
// NOTE: Consciously assigning a known-valid value 
// ~~~~  without further validation!
order.populateValidAttribute('OrderStatus','CLOSED')

Use Left Shift Operator To Append to Lists

To append elements to an existing list, use the left shift operator ( <<) 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.