Skip to content

Performance Considerations

Triggers should be designed to process records in bulk, especially when making service calls, such as QueryService, RecordService and RecordRoleService. These services are designed to take a list of records or record role changes as input for CRUD operations. It is much more efficient to build a list of record for input and make a single call to these services rather than make service calls one record at a time inside a loop.

Triggers that do not process records in bulk will perform poorly, especially when there are multiple triggers (including nested triggers), execution will likely exceed the maximum elapsed time (100s) or CPU time (10s) allowed. In addition, queries that return large number of records with large number of fields (including fields not used in your code) will likely exceed the maximum memory allowed (40 MB).

Generally, you should never run a query or perform CRUD operations on records in a loop. Each iteration will make unnecessary service calls which can be easily batched to get the same result with a single service call.

The following poorly performing code executes a query inside a "for" loop, for each Product record in a request. That means when a request has multiple records, like from an API call or bulk update wizard, the QueryService#query call is made for each of the records. The only difference between each query is the WHERE clause contains a different Country reference field value. Performing multiple queries in this case is inefficient and time consuming. A better approach is to make a single query with a CONTAINS clause for each Country referenced by the Product records in the request.

To make performance even worse, as each query is executed to retrieve related records, a forEach loop is used to call RecordService.batchSaveRecords to save each new Country Brand record one at a time. Creating, updating, and deleting records are the most expensive and time-consuming operations. You should always batch records up in a list as input when calling batchSaveRecords.

While the better performing code requires more lines of code as illustrated below, it performs much better because it reduces data operations significantly by leveraging the Vault Java SDK's interfaces to process records in bulk.

@RecordTriggerInfo(object = "product__v", events = RecordEvent.AFTER_INSERT) public class ProductCreateRelatedCountryBrand implements RecordTrigger { public void execute(RecordTriggerContext recordTriggerContext) { for (RecordChange inputRecord : recordTriggerContext.getRecordChanges()) { QueryService queryService = ServiceLocator.locate(QueryService.class); String queryCountry = "select id, name__v from country__v where region__c=" + "'" + region + "'"; QueryResponse queryResponse = queryService.query(queryCountry); queryResponse.streamResults().forEach(queryResult -> { Record r = recordService.newRecord("country_brand__c"); r.setValue("name__v", internalName + " (" + queryResult.getValue("name__v", ValueType.STRING) + ")"); r.setValue("country__c",queryResult.getValue("id",ValueType.STRING)); r.setValue("product__c",productId); RecordService recordService = ServiceLocator.locate(RecordService.class); recordService.batchSaveRecords(VaultCollections.asList(r)).rollbackOnErrors().execute(); }); } }
@RecordTriggerInfo(object = "product__v", name= "product_create_related_country_brand__c", events = RecordEvent.AFTER_INSERT) public class ProductCreateRelatedCountryBrand implements RecordTrigger { public void execute(RecordTriggerContext recordTriggerContext) { // Get an instance of the Record service RecordService recordService = ServiceLocator.locate(RecordService.class); List<Record> recordList = VaultCollections.newList(); // Retrieve Regions from all Product input records Set<String> regions = VaultCollections.newSet(); recordTriggerContext.getRecordChanges().stream().forEach(recordChange -> { String regionId = recordChange.getNew().getValue("region__c", ValueType.STRING); regions.add("'" + regionId + "'"); }); String regionsToQuery = String.join (",",regions); // Query Country object to select countries for regions referenced by all Product input records QueryService queryService = ServiceLocator.locate(QueryService.class); String queryCountry = "select id, name__v, region__c " + "from country__v where region__c contains (" + regionsToQuery + ")"; QueryResponse queryResponse = queryService.query(queryCountry); // Build a Map of Regions (key) and Countries (value) from the query result Map<String, List<QueryResult>> countriesInRegionMap = VaultCollections.newMap(); queryResponse.streamResults().forEach(queryResult -> { String region = queryResult.getValue("region__c",ValueType.STRING); if (countriesInRegionMap.containsKey(region)) { List<QueryResult> countries = countriesInRegionMap.get(region); countries.add(queryResult); countriesInRegionMap.put(region,countries); } else countriesInRegionMap.putIfAbsent(region,VaultCollections.asList(queryResult)); }); // Go through each Product record, look up countries for the region assigned to the Product, // and create new Country Brand records for each country. for (RecordChange inputRecord : recordTriggerContext.getRecordChanges()) { String regionId = inputRecord.getNew().getValue("region__c", ValueType.STRING); String internalName = inputRecord.getNew().getValue("internal_name__c", ValueType.STRING); String productId = inputRecord.getNew().getValue("id", ValueType.STRING); Iterator<QueryResult> countries = countriesInRegionMap.get(regionId).iterator(); while (countries.hasNext()){ QueryResult country =countries.next(); Record r = recordService.newRecord("country_brand__c"); r.setValue("name__v", internalName + " (" + country.getValue("name__v", ValueType.STRING) + ")"); r.setValue("country__c", country.getValue("id", ValueType.STRING)); r.setValue("product__c", productId); recordList.add(r); } } // Save the new Country Brand records in bulk. Rollback the entire transaction when encountering errors. recordService.batchSaveRecords(recordList).rollbackOnErrors().execute(); } }