Avoid Heap Size Limits in Visualforce Pages
I built this great Visualforce page where I display and allow for editing of many records at once. The initial business requirement was for only a few hundred records to be used in this application but, as usual, requirements morphed and now the business wants to be able to use this app for thousands of records. So guess what... we now encounter the "System.LimitException: Apex heap size too large" governor limit. WTF!
I do understand the purpose of governor limits on the force.com platform but I just dislike when I encounter them. This particular limit was a nuisance because I already had a functioning application in a production Salesforce org for a few months before the limit was hit. And, of course, the fix became a top priority "we can’t do anything until this is fixed" blah blah blah...
Maybe you hit this limit in the past or maybe you just encountered it and this post came up in your search for a solution. Either way, here is a trick to allowing users to use a Visualforce page that traverses lots of data and attempts to avoid the apex heap size governor limit.
The reason I was encountering the heap size issue is due to the fact that I was displaying and editing many records in a single Visualforce page. Although I allowed for pagination between buckets of 50 records at a time, I was holding all of the records in a series of sObject lists within the controller for the page. For example, the user could see 50 records at a time but there may be 2,000 records in the result set that I was holding in the list behind the scenes and allowing the user to page through. The number of fields for each record in conjunction with the number of records that were being held in the controller were causing that heap limit to be reached/exceeded.
I didn't want to create a usability issue by simply telling the user you reached the maximum number of records so you need to change your search criteria or something. I’ve seen that before and as a user I really dislike that sort of design.
At a high level, the solution is pretty straight forward. Simply reduce the amount of data being returned to the page. In order to accomplish this I decided to change my controller so that I no longer grabbed all the data for the thousands or records up front. Instead, I altered the logic to grab only the fields needed for the 50 records that were being displayed on the page at any one time. I was able to reduce the heap size by about 86% when I took this approach.
At a more granular level I changed my pagination lists from SObjects to Strings and only populated the lists with the Id of each record. I then created an additional sObject list and populated that with the all the fields that needed to be displayed to the page in order to use the page.
Please see the controller below as an example:
/*
Created by: Greg Hacic
Last Update: 7 February 2012 by Greg Hacic
Questions?: greg@interactiveties.com
*/
public with sharing class controller_for_page {
public List<Account> display_list = new List<Account>(); //list for all Account records and fields
public List<String> current_list = new List<String>(); //list for holding many record Ids
public List<String> next_list = new List<String>(); //list for holding record Ids that are after the current records
public List<String> previous_list = new List<String>(); //list for holding record Ids that are before the current records
Integer list_size = 50; //number of records to display on the page
//initiates the controller and displays some initial data when the page loads
public controller_for_page() {
Integer record_counter = 0; //counter
for (Account a : [SELECT Id FROM Account ORDER BY Name LIMIT 10000]) { //for a bunch of accounts
if (record_counter < list_size) { //if we have not yet reached our maximum list size
current_list.add(a.Id); //add the Id of the record to our current list
} else { //otherwise, we reached our list size maximum
next_list.add(a.Id); //add the Id to our next_list
}
record_counter++;
}
}
public List<Account> getRecordsToDisplay() {
Set<String> record_ids = new Set<String>(); //set for holding distinct Ids
Boolean records_added = record_ids.addAll(current_list); //add all the records from our current_list list
display_list = [SELECT AccountNumber, Id, Name, OwnerId, Phone, Site, Type FROM Account WHERE Id in : record_ids ORDER BY Name]; //query for the details of the records you want to display
return display_list; //return the list of full records
}
public Integer getCurrentSize() {
return current_list.size(); //number of record in current_list
}
public Integer getPrevSize() {
return previous_list.size(); //number of record in previous_list
}
public Integer getNextSize() {
return next_list.size(); //number of record in next_list
}
}
As you can see, I create three String lists initially. These are used to keep track of the records that I’ve already paged through, those that I am currently displaying on the page and those that we have yet to page through. The list of Ids that I am displaying to the page is later used in an additional query to grab all the fields for those records.
The Visualforce page below ties everything together. So you can see what is being done in a practical example within your developer or sandbox org.
<apex:page controller="controller_for_page" sidebar="false">
<!--
Created by: Greg Hacic
Last Update: 7 February 2012 by Greg Hacic
Questions?: greg@interactiveties.com
-->
<apex:form>
<apex:pageBlock mode="maindetail" title="Reduce Heap Size: Example">
<apex:outputPanel>{!IF( OR(prevSize > 0, currentSize > 0), prevSize + 1, 0)} to {!prevSize + currentSize} of {!prevSize + currentSize + nextSize}</apex:outputPanel>
<apex:actionStatus>
<apex:facet name="start">Loading...</apex:facet>
<apex:facet name="stop">
<apex:pageBlockTable value="{!recordsToDisplay}" var="r">
<apex:column value="{!r.Name}"></apex:column>
<apex:column value="{!r.Site}"></apex:column>
<apex:column value="{!r.AccountNumber}"></apex:column>
<apex:column value="{!r.Phone}"></apex:column>
<apex:column value="{!r.Type}"></apex:column>
<apex:column value="{!r.OwnerId}"></apex:column>
</apex:pageBlockTable>
</apex:facet>
</apex:actionStatus>
<apex:outputText rendered="{!IF(currentSize > 0,false,true)}" value="No Records to Display..."></apex:outputText>
</apex:pageBlock>
</apex:form>
</apex:page>
Thanks for reading.