Using Iterator & Iterable Interface in Apex

Using Iterator & Iterable Interface in Apex

In this blog post, we will discuss about the custom iterator, their usage and examples. Custom iterators can be useful in apex. They are majorly beneficial in batch classes because they provide more flexibility in how you iterate over records. By default, batch classes use a QueryLocator to retrieve records from the database. However, there are cases where you may want to use a custom iterator instead.

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. Use a custom iterator. This iterator 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, use a custom iterator. This iterator sorts the records according to your criteria.
  • Filtering records: If you need to process records meeting certain criteria, use a custom iterator. It will filter out records that don’t match your criteria.
  • Complex record retrieval logic: If you need to retrieve records using complex logic, it will be challenging. You will find it difficult to express this in a single SOQL query. You can use a custom iterator to retrieve records using multiple queries or other logic.

Custom iterators can be a powerful tool for developers. They offer more control over the data processing logic. Developers can also optimize performance and scalability. However, they also require more development effort and expertise and can be more difficult to maintain, test, and debug.

Pros :

  1. Flexibility: Custom iterators provide greater flexibility in terms of data processing and manipulation. They allow developers to write complex logic. This logic can filter, sort, and transform data.
  2. Performance: Custom iterators can often be faster and more efficient than QueryLocator or Iterable. They can optimize the data retrieval and processing logic. This optimization helps minimize the number of database calls and reduce the amount of data processed.
  3. Scalability: Custom iterators are more scalable. They can handle large volumes of data. This prevents hitting platform limits or running out of memory.

Cons:

  1. Complexity: Custom iterators require more development effort and expertise than QueryLocator or Iterable. They involve writing more code and designing more complex data processing logic.
  2. Maintenance: Custom iterators can be more difficult to maintain and troubleshoot because they involve more code and complexity.
  3. Testing & Debugging: Custom iterators can be more difficult to test and debug. They involve more complex data processing logic. More test cases are required to cover all possible scenarios. More effort is needed to isolate and fix issues.

Below are some of the use case where we can use custom iterators –

Scenario 1 – A company wants to monitor user login activity on the Salesforce platform. They aim to take action when necessary. This ensures that their investment in the platform is being used effectively. They want to check the login status of users. They will send an email to inform the users about their login status. If a user has not logged into Salesforce within the last 30 days, they will notify them. They will also email managers after 60 days of inactivity. They will also inform if the user has never logged in.

The InactiveUserBatch class is a Batch Apex class. It sends email alerts to a user’s manager and the system administrator. These alerts are triggered when a user has not logged in for 30 days. Alerts also send if a user has not logged in for 60 days or has never logged in.

The UserWrapper class is a wrapper class, that will used as custom filtered scope list to be processed by InactiveUserBatch class

The InactiveUserIterator class is an inner class. It will be used to filter out the users who have not logged in for 30 or 60 days. It also filters users who have never logged in. It will send the filtered user list to the batch class execute method.

The InactiveUserBatch class has three methods. These methods work together to send email alerts. They notify inactive users’ managers and the system administrator.

  • 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 manager. The alert targets users who have not logged in for 30 days. It also includes users who have not logged in for 60 days or have 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.

Here is the complete sample code –

/*
 * 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;
    }
  }
}

Leave a comment