In this article, we’ll examine how custom iterators can optimize your data iteration process in Salesforce. We’ll cover the basics of what iterators are and how they work. Furthermore, we’ll explore examples of how you can use custom iterators to streamline your code.
What is an Iterator?
So what is an iterator, exactly? At its simplest, an iterator is an object. It provides a way to access elements of a collection, like an array or a list. It accesses these elements sequentially. You don’t need to know the underlying structure of the collection. In Apex, you can define your own custom iterators by implementing the Iterator interface.
By default, batch classes employ a QueryLocator to retrieve records from the database. However, in some cases, using a custom iterator may be more appropriate.
Here are a few reasons why you might want to use a custom iterator in your apex class:
- Processing records from multiple objects Using Wrapper Class: If you need to process records from multiple objects, you can use a custom iterator that retrieves records from multiple queries. This can be more efficient than using a separate batch class for each object. Example using a wrapper class list in batch class.
- Sorting records: If you need to process records in a specific order, you can use a custom iterator that sorts the records according to your criteria.
- Filtering records: If you only want to process records that meet certain criteria, you can use a custom iterator to filter out records that don’t match your criteria.
- Complex record retrieval logic: You can use a custom iterator to retrieve records using multiple queries or other logic if you cannot express the retrieval of records with complex logic in a single SOQL query.
Details :
Custom iterators can be a powerful tool for developers. They provide more control over the data processing logic. Additionally, they can help optimize performance and scalability. However, they also require more development effort and expertise and can be more difficult to maintain, test, and debug.
To create a custom iterator in Apex, you’ll need to define a class that implements the Iterator interface. This interface requires you to define three methods: hasNext(), next(), and remove(). The hasNext() method checks if there are more elements in the collection to iterate over. On the other hand, the next() method returns the next element in the collection. The remove() method is optional and allows you to remove the current element from the collection.
Salesforce Links –
https://developer.salesforce.com/docs/atlas.en-us.242.0.apexref.meta/apexref/apex_comparable.htm
Note – Batch class can work on up to 50M sObject records using QueryLocator. However, it can only work up to 50k records using Iterator.
Pros :
- Flexibility: Custom iterators offer enhanced flexibility to developers for filtering, sorting, and transforming data with intricate logic.
- Performance: Custom iterators optimize data retrieval for faster and more efficient performance, reducing database calls and data processing.
Cons:
- Complexity: Custom iterators require more development effort because they involve writing more code and designing more complex data processing logic.
- Testing & Debugging: Custom iterators involve complex data processing logic and require multiple test cases for testing and debugging. As a result, isolating and fixing issues in them requires more effort.
Below are some of the use cases where we can use custom iterators –
Scenario 1 – A company needs to monitor user activity on Salesforce. They also need to alert inactive users and their managers via email. We should send alerts for users who have not logged in to Salesforce for 30 or 60 days. We should also notify those who have never logged in. The goal is to optimize the company’s investment in the platform.
Code Explanation:
For the above scenario, we created the InactiveUserBatch batch class, and below are the batch class details –
The InactiveUserBatch is a Batch Apex class that sends email alerts. The system administrator, as well as the user and their manager, should receive alerts. The batch runs if a user has not logged in for 30 or 60 days, or has never logged in.
The UserWrapper class is a wrapper class that acts as a custom-filtered scope list. The InactiveUserBatch class uses it for processing. The wrapper class helps in organizing and filtering user data.
The InactiveUserIterator is an inner class. It filters out inactive users who have not logged in for 30 or 60 days. It also filters users who have never logged in. The InactiveUserIterator filters out the inactive users. The resulting list of users is then passed to the execute method of the batch class for further processing.
The InactiveUserBatch class consists of three methods. These methods work together to send email alerts. The methods ensure efficient processing of user data and sending of alerts.
- Start Method: This method is called at the beginning of the batch process to retrieve the records to process. It returns an iterator of UserWrapper objects. The start method queries all active users and their managers.
- Execute Method: This method is called for each batch of records to be processed. It sends an email alert to the user and the user’s manager. This occurs if the user has not logged in for 30 days or 60 days. It also happens if the user has never logged in. The execute method iterates over each user in the batch and creates a new email message for each user. It sets the email addresses, subject, and body of the email message based on the user’s login status.
- Finish Method: This method is called at the end of the batch job. It sends an email notification to the system administrator. The email lists the users who have been sent the Inactive User Alert. If any users have been sent emails, the finish method sends an email notification to the system administrator.
The complete sample code is shown below. As per the requirement, we couldn’t use soql directly to filter out users. Therefore, we filtered out the users in the iterator class using a wrapper class. which filter out the user based on login and in execute method, we identify the users to process with wrapper class variables and pass on to execute method,
/*
* This class sends an email alert to a user and user's manager,
* when a user has not logged in for 30 or 60 days, or has never logged in.
* This class implements the Batchable interface a large number of user records in batches.
* This class uses custom iterator to filter the users based on login
* This class also sends out a mail to system admin, once the batch is finished,
* mentioning the user name for which email alert has been sent.
*/
public class InactiveUserBatch implements Database.Batchable<InactiveUserBatch.UserWrapper>, Database.Stateful {
/*
* userEmailsSent is a list to store the email addresses of users who have been sent the Inactive User Alert.
* It is used in the finish method to send an email to the system administrator.
*/
private List<String> userEmailsSent = new List<String>();
/*
* This method is called at the beginning of the batch process to retrieve the records to process.
* It returns an iterator of UserWrapper objects.
*/
public Iterable<UserWrapper> start(Database.BatchableContext bc) {
// Query for all active users and their managers
return new InactiveUserIterator();
}
/*
* This method is called for each batch of records to be processed.
* It sends an email alert to the manager of each user who has not logged
* in for 30 or 60 days, or has never logged in.
*/
public void execute(Database.BatchableContext bc, List<UserWrapper> scope) {
// Create a list to store all the email messages to be sent
List<Messaging.SingleEmailMessage> emailsToSend = new List<Messaging.SingleEmailMessage>();
// Iterate over each user in the batch
for (UserWrapper uw : scope) {
// Create a new email message for each user
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
// Set the Bcc address to the user's manager's email, if available
if (uw.managerEmail != null) {
email.setBccAddresses(new List<String>{ uw.managerEmail });
}
// Set the To address to the user's email
email.setToAddresses(new List<String>{ uw.u.Email });
// Set the subject of the email
email.setSubject('Inactive User Alert');
// Check the user's login status and set the email body accordingly
switch on uw.loginStatus {
when 'Inactive30' {
email.setPlainTextBody(
'User ' + uw.u.Name + ' has not logged in for 30 days.'
);
// Add the email to the list of emails to be sent
emailsToSend.add(email);
// Add the user's name to the list of users who have been sent emails
userEmailsSent.add(uw.u.Name);
}
when 'Inactive60' {
email.setPlainTextBody(
'User ' + uw.u.Name + ' has not logged in for 60 days.'
);
// Add the email to the list of emails to be sent
emailsToSend.add(email);
// Add the user's name to the list of users who have been sent emails
userEmailsSent.add(uw.u.Name);
}
when 'Never Logged In' {
email.setPlainTextBody('User ' + uw.u.Name + ' has never logged in.');
// Add the email to the list of emails to be sent
emailsToSend.add(email);
// Add the user's name to the list of users who have been sent emails
userEmailsSent.add(uw.u.Name);
}
}
}
// Send all the emails in one go
if (!emailsToSend.isEmpty()) {
Messaging.sendEmail(emailsToSend);
}
}
/**
* This method is called at the end of the batch job to send an email notification to the system administrator
* listing the users who have been sent the Inactive User Alert.
* @param bc The batchable context object
*/
public void finish(Database.BatchableContext bc) {
// If any users have been sent emails, send an email notification to the system administrator
if (!userEmailsSent.isEmpty()) {
// Create a new email message
Messaging.SingleEmailMessage adminEmail = new Messaging.SingleEmailMessage();
// Set the To address to the system administrator's email
adminEmail.setToAddresses(new List<String>{ 'systemadmin@yourorg.com' });
// Set the subject of the email
adminEmail.setSubject('Inactive User Alert - User List');
// Set the body of the email to list the users who have been sent the Inactive User Alert
adminEmail.setPlainTextBody(
'The following users have been sent the Inactive User Alert:\n' +
String.join(userEmailsSent, '\n')
);
// Send the email to the system administrator
Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ adminEmail });
}
}
/**
* Wrapper class to store User information along with their manager's email and login status.
*/
public class UserWrapper {
// The User object for the user
public User u;
// The email address of the user's manager
public String managerEmail;
// The login status of the user ('Inactive30', 'Inactive60', or 'Never Logged In')
public String loginStatus;
}
/**
* This class implements the Iterator and Iterable interfaces to allow iteration over a list of UserWrapper objects
* representing active users in the Salesforce org. The iterator filters out inactive users and returns only those
* users who have not logged in for 30 days, 60 days, or never logged in.
*/
private without sharing class InactiveUserIterator implements Iterable<UserWrapper>, Iterator<UserWrapper> {
private List<UserWrapper> users;
private Integer index;
/**
Constructor that initializes the list of active users and filters out inactive users.
*/
public InactiveUserIterator() {
// Initialize the list of UserWrapper objects
users = new List<UserWrapper>();
// Query for all active users in the Salesforce org
List<User> activeUsers = [
SELECT Id, Name, Email, ManagerId, Manager.Email, LastLoginDate
FROM User
WHERE IsActive = TRUE
];
// Iterate over each active user and add them to the list of UserWrapper objects
for (User u : activeUsers) {
UserWrapper userwrap = new UserWrapper();
userwrap.u = u;
userwrap.managerEmail = u.Manager.Email;
users.add(userwrap);
}
index = 0;
}
/**
Returns an iterator over a set of elements of type UserWrapper.
*/
public Iterator<UserWrapper> iterator() {
return this;
}
/**
Returns true if the iteration has more elements.
*/
public Boolean hasNext() {
return index < users.size();
}
/**
* This method returns the next user record that matches the inactive criteria.
* It loops through the active users list and filters out the records based on LastLoginDate.
* If the user has not logged in for more than 30 days, the loginStatus is set to 'Inactive30'.
* If the user has not logged in for more than 60 days, the loginStatus is set to 'Inactive60'.
* If the user has never logged in, the loginStatus is set to 'Never Logged In'.
@return UserWrapper - the next user record that matches the inactive criteria
*/
public UserWrapper next() {
while (hasNext()) {
UserWrapper u = users.get(index);
// Filter out the records based on LastLoginDate
if (u.u.LastLoginDate != null) {
Date lastLogin = Date.newInstance(
u.u.LastLoginDate.year(),
u.u.LastLoginDate.month(),
u.u.LastLoginDate.day()
);
Date currentDate = System.today();
Integer days = lastLogin.daysBetween(currentDate);
// Check if the user has not logged in for more than 30 days
if (days >= 30 && days < 60) {
u.loginStatus = 'Inactive30';
index++;
return u;
}
// Check if the user has not logged in for more than 60 days
else if (days >= 60) {
u.loginStatus = 'Inactive60';
index++;
return u;
} else {
// Skip this record as it has logged in within the last 30 days
index++;
}
}
// Check if the user has never logged in
else {
u.loginStatus = 'Never Logged In';
index++;
return u;
}
}
return null;
}
}
}
Scenario 2 – A company needs a weekly report that displays the total order value per account in the last 7 days. The report should be sorted by value and presented in CSV format via email. The CSV should include the account ID and total order amount for that week.
Code Explanation:
For the above scenario, we created the OrderReportBatch batch class, and below are the batch class details –
- OrderReportBatch batch class emails the administrator with account details & order amount CSV, sorted by highest amount.
- OrderAccountWrapper class is a wrapper class that holds and sorts account data by order amount.
- OrderReportIterator class is an inner class, that sends sorted accounts to the scope list for processing by OrderReportBatch
In the below code, we were unable to use SOQL to filter the highest amount of orders per account directly. Instead, we used an aggregate query and sorted the wrapper list using Comparable within the iterator class.
We then passed this list to the execute method. In the execute method, we created a CSV body using the wrapper list, which was used in the finish method.
/*******************************************************************************************
* @Name : OrderReportBatch
* @Author : thesalesforcedev
* @Description : This batch class demonstrates use of iterator in apex batch class.
* This class sends an email to a System admin, attaching a csv with account details
* and total order amount, sorted by total order amount desc.
* The inner child classes uses Iterable, Iterator to iterate over OrderAccountWrapper class
* and Comparable to sort the accounts by total order amount.
**************************************************************************************/
public class OrderReportBatch implements Database.Batchable<OrderReportBatch.OrderAccountWrapper>, Database.Stateful {
// The body of the email message to be sent
public String body;
// A Map of Account records used in the batch job
public Map<Id, Account> accountsMap;
// Constructor that initializes the map and the body of the email message
public OrderReportBatch() {
accountsMap = new Map<Id, Account>();
body = '';
}
/**************************************************************************************
* @Description : The start method returns an iterable object of OrderAccountWrapper
* @Param : bc - A batchable context object
* @Return : Iterable<OrderAccountWrapper> - An iterable of OrderAccountWrapper
**************************************************************************************/
public Iterable<OrderAccountWrapper> start(Database.BatchableContext bc) {
return new OrderReportIterator();
}
/**************************************************************************************
* @Description : The execute method processes a list of OrderAccountWrapper objects
* and create the body of csv file with account id and total order amount
* which will be used in finish method to send as attachment.
* @Param : bc - A batchable context object
* @Param : scope - A list of OrderAccountWrapper objects
* @Return : void
**************************************************************************************/
public void execute(Database.BatchableContext bc, List<OrderAccountWrapper> scope) {
for (OrderAccountWrapper wrapper : scope) {
body += wrapper.accId + ',' + wrapper.totalOrderAmount + '\n';
}
}
/**************************************************************************************
* @Description : The finish method send the list of all sorted accounts
* with max order amount,with thier acount id
* and total order amount to
* System Admin in an email as attachment.
* @Param : bc - A batchable context object
* @Return : void
**************************************************************************************/
public void finish(Database.BatchableContext bc) {
// Build CSV string
String header = 'Account Name,Total Order Value\n';
String csvString = header + body;
Blob csvBlobSuccess = Blob.valueOf(csvString);
String csvBlobEncoded = EncodingUtil.base64Encode(csvBlobSuccess);
// Create and send the email message
Messaging.EmailFileAttachment attachment = new Messaging.EmailFileAttachment();
attachment.setFileName('Order Report.csv');
attachment.setBody(EncodingUtil.base64Decode(csvBlobEncoded));
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setToAddresses(new List<String>{ 'systemadmin@yourorg.com' });
email.setSubject('Order Report');
email.setPlainTextBody('Please find attached the Order Report');
email.setFileAttachments(
new List<Messaging.EmailFileAttachment>{ attachment }
);
Messaging.SendEmailResult[] results = Messaging.sendEmail(
new List<Messaging.SingleEmailMessage>{ email }
);
}
/*******************************************************************************************
* @Name : OrderAccountWrapper
* @Description : This inner wrapper class is used to wrap an
* Account Object Id and its Total Order Amount
* for sorting by order amount descending
* using Comparable Interface and CompareTo method
*******************************************************************************************/
public class OrderAccountWrapper implements Comparable {
public Id accId;
public Decimal totalOrderAmount;
/**************************************************************************************
* @Description : Constructor that sets the Account ID and
* total order amount for an
OrderAccountWrapper object
* @Param : aId - The ID of the Account
* @Param : orderAmount - The total order amount for the Account
**************************************************************************************/
public OrderAccountWrapper(Id aId, Decimal orderAmount) {
this.accId = aId;
this.totalOrderAmount = orderAmount;
}
/**************************************************************************************
* @Description : The compareTo to method compares this OrderAccountWrapper object
* to another for sorting by total order amount in descending order
* @Param : other - The other OrderAccountWrapper object to compare to
* @Return : Integer - sorted OrderAccountWrapper record by amount
**************************************************************************************/
public Integer compareTo(Object other) {
OrderAccountWrapper otherWrapper = (OrderAccountWrapper) other;
if (this.totalOrderAmount > otherWrapper.totalOrderAmount) {
return -1;
} else if (this.totalOrderAmount < otherWrapper.totalOrderAmount) {
return 1;
} else {
return 0;
}
}
}
/*******************************************************************************************
* @Name : OrderReportIterator
* @Description : This inner class is used to iterate
* OrderAccountWrapper records for the
* batch class processing.
*******************************************************************************************/
public without sharing class OrderReportIterator implements Iterable<OrderAccountWrapper>, Iterator<OrderAccountWrapper> {
private List<OrderAccountWrapper> accountsWithMaxOrderAmount;
private Integer index;
/**************************************************************************************
* @Description : Constructor that sets the accountsWithMaxOrderAmount and index.
* Also sort the OrderAccountWrapper list with total order amount.
**************************************************************************************/
public OrderReportIterator() {
// Initialize the list of OrderAccountWrapper objects
accountsWithMaxOrderAmount = new List<OrderAccountWrapper>();
// Retrieve a map of Account IDs and their respective order total.
Map<Id, Decimal> accountTotalMap = new Map<Id, Decimal>();
accountTotalMap = retieveaccountTotalMap();
// If there are any accounts in the map, create a new OrderAccountWrapper object and add it to the list.
if (!accountTotalMap.isEmpty()) {
for (Id i : accountTotalMap.keySet()) {
OrderAccountWrapper obj = new OrderAccountWrapper(
i,
accountTotalMap.get(i)
);
accountsWithMaxOrderAmount.add(obj);
}
}
// Sort the list of OrderAccountWrapper objects by totalOrderAmount in descending order.
accountsWithMaxOrderAmount.sort();
// Set the initial index to -1 to prepare for iteration.
index = -1;
}
/**************************************************************************************
* @Description : The retieveaccountTotalMap method Retrieves the total order amount
* for each account for the last 7 days and aggregates the total order amount
* for each AccountId using the SOQL AggregateResult.
* @Return : Map<Id, Decimal> - Returns a Map containing the AccountId as the key
* and the total order amount as the value.
**************************************************************************************/
public Map<Id, Decimal> retieveaccountTotalMap() {
Map<Id, Decimal> accountsAmountMap = new Map<Id, Decimal>();
for (AggregateResult result : [
SELECT AccountId, SUM(TotalAmount) totalAmount
FROM Order
WHERE createddate = LAST_N_DAYS:7
GROUP BY AccountId
]) {
accountsAmountMap.put(
(Id) result.get('AccountId'),
(Decimal) result.get('totalAmount')
);
}
return accountsAmountMap;
}
public Iterator<OrderAccountWrapper> iterator() {
return this;
}
public Boolean hasNext() {
return index < accountsWithMaxOrderAmount.size() - 1;
}
public OrderAccountWrapper next() {
if (hasNext()) {
index++;
return accountsWithMaxOrderAmount.get(index);
}
return null;
}
}
}
Scenario 3- A company selling books online needs to update the inventory’s book price through a batch process. The batch must update the books published at least two years ago and process records in batches of 100. The batch must retrieve Book and Publisher data, and apply a 10% discount to specific publisher’s books.
Code Explanation:
In the below sample code, we retrieved book data from the last two years using SOQL as required. We then iterated through the book data using an iterator and passed it on to the execute method. In the execute method, we updated the price of the book based on certain conditions.
/***************************************************************************************
* @Name : UpdateBookPriceBatch
* @Author : thesalesforcedev
* @Description : This batch class updates the price of Books that were published more than
* two years ago. Books published by "Discount Publishing" have their prices
* reduced by 10% while books by other publishers have their prices increased by 10%.
**************************************************************************************/
public class UpdateBookPriceBatch implements Database.Batchable<UpdateBookPriceBatch.BookWrapper> {
/**************************************************************************************
* @Description : The start method returns an iterable object of BookWrapper
* @Param : bc - A batchable context object
* @Return : Iterable<BookWrapper> - An iterable of BookWrapper
**************************************************************************************/
public Iterable<BookWrapper> start(Database.BatchableContext bc) {
// Retrieve books that were published more than two years ago
Date cutoffDate = Date.today().addYears(-2);
String query = 'SELECT Id, Name, Price__c, Publication_Date__c, Publisher__c, Publisher__r.Name FROM Book__c WHERE Publication_Date__c < :cutoffDate';
List<BookWrapper> books = new List<BookWrapper>();
List<Book__c> lst = Database.query(query);
for (Book__c b : lst) {
books.add(new BookWrapper(b));
}
// Return an iterator for the list of books
return new BookIterator(books);
}
/**************************************************************************************
* @Description : The execute method updates the prices of Books in the scope
* @Param : bc - A batchable context object
* : scope - A list of BookWrapper objects
* @Return : void
**************************************************************************************/
public void execute(Database.BatchableContext bc, List<BookWrapper> scope) {
List<Book__c> booksToUpdate = new List<Book__c>();
// Update the price of each book in the scope
for (BookWrapper bw : scope) {
Book__c b = bw.book;
if (bw.publisherName == 'Discount Publishing') {
b.Price__c *= 0.9; // Apply 10% discount
} else {
b.Price__c *= 1.1; // Increase price by 10%
}
booksToUpdate.add(b);
}
// Update the books in the database
update booksToUpdate;
}
/**************************************************************************************
* @Description : The finish method is not implemented
* @Param : bc - A batchable context object
* @Return : void
**************************************************************************************/
public void finish(Database.BatchableContext bc) {
// Implementation not needed for this example
}
/**************************************************************************************
* @Description : The BookWrapper class wraps a Book__c object and stores its publisher's name
**************************************************************************************/
public class BookWrapper {
public Book__c book;
public String publisherName;
public BookWrapper(Book__c b) {
book = b;
publisherName = b.Publisher__r.Name;
}
}
public class BookIterator implements Iterable<BookWrapper>, Iterator<BookWrapper> {
private List<BookWrapper> books;
private Integer index;
public BookIterator(List<BookWrapper> books) {
this.books = books;
index = 0;
}
public Iterator<BookWrapper> iterator() {
return this;
}
public Boolean hasNext() {
return index < books.size();
}
public BookWrapper next() {
if (!hasNext()) {
return null;
}
BookWrapper bw = books.get(index);
index++;
return bw;
}
}
}
For more helpful articles please visit – https://thesalesforcedev.in


Leave a comment