Group, Sum and Order data in Apex
by Andrew Waite on October 5, 2008 at 02:07 PM
Last week I offered a solution for sorting in Apex in response to some discussions taking place on the discussion boards . As promised, I'm back to discuss one approach to addressing the business problem highlighted in a very detailed fashion in this thread. The relevant technical requirement outlined therein can be paraphrased as "How can I group, sum and order opportunity information in Apex?" And of course doing so with the minimal impact against the respective governor limits.
The end result should look something like this:
Let's address each of the goals in sequence.
1. Group
A notable limitation in SOQL, the Salesforce Object Query Language, is the inability to GROUP BY. Fortunately there is a technique for grouping data that happens to coincide with the business requirement being addressed. That technique is to leverage the aggregate relationship functionality of SOQL. The following example will return accounts with their respective opportunities:
SELECT Name, (SELECT Name, Amount FROM Opportunities) FROM Account
"But doesn't this bring back accounts that have no opportunities too", you ask? Why yes, yes it does. Don't want those? Enter the Semi-joins capability of SOQL, new in the Winter '09 release coming soon to a cloud near you. With the following change (in bold), the result will only include accounts that have related opportunities:
SELECT Name, (SELECT Name, Amount FROM Opportunities)
FROM Account WHERE Id IN (SELECT AccountId from Opportunity)
2. Sum
Now we have the opportunities grouped by account and are ready to total them up. The end goal for this example is to present the information acquired through Apex on a Visualforce page. As such we need a temporary structure that provides access to the account, the opportunities and the summarized total. This structure can be defined as a class in Apex:
public class accountTotal {
public Account account { get; private set; }
public Opportunity total { get; private set; }
public accountTotal(Account a) {
account = a;
total = new Opportunity(amount = 0);
for(Opportunity o:a.opportunities) {
if(o.amount != null) {
total.amount += o.amount;
}
}
}
}
At this point you might be wondering why this class has an Opportunity member for the total rather than a primitive, such as Decimal. Given the goal of outputting to a Visualforce page we can get the standard currency formatting for free as long as we can bind to a field definition rather than just a simple primitive.
Now the following method will return a collection of the AccountTotal structures for each account in the query result:
public List<accountTotal> getAccountTotals(){
List<accountTotal> accountTotals = new List<accountTotal>();
for(Account a:[SELECT Name, (SELECT Name, Amount FROM Opportunities)
FROM Account WHERE Id IN (SELECT AccountId from Opportunity)]) {
accountTotals.add(new AccountTotal(a));
}
return accountTotals;
}
3. Order
So now we have just the accounts with opportunities (grouped) and their relevant totals but there's one last step to allow the user to see the most interesting accounts first. The following method will order the AccountTotal collection in descending order of the total using the approach described in my last post :
private List<AccountTotal> sortAccountTotals(List<AccountTotal> totals) {
List<AccountTotal> returnList = new List<AccountTotal>();
Map<Decimal, List<AccountTotal>> totalMap = new Map<Decimal, List<AccountTotal>>();
for(AccountTotal t:totals) {
if(totalMap.get(t.total.amount) == null) {
totalMap.put(t.total.amount, new List<AccountTotal>());
}
totalMap.get(t.total.amount).add(t);
}
List<Decimal> keys = new List<Decimal>(totalMap.keySet());
keys.sort();
/* Sort puts things in ascending order so for descending iterate over
the keys backwards. */
for(Integer i = (keys.size()-1);i >= 0; i--) {
returnList.addAll(totalMap.get(keys.get(i)));
}
return returnList;
}
Putting it all together
The following controller class definition combines each of the above requirements to enable a Visualforce page to display the desired information in the expected way:
public class accountsWithOpptysCon {
public List<AccountTotal> getaccounttotals() {
List<AccountTotal> accounttotals = new List<AccountTotal>();
for(Account a:[select name, (select name,amount from opportunities)
from account
where id in (select accountid from opportunity)]) {
accounttotals.add(new AccountTotal(a));
}
return sortAccountTotals(accounttotals);
}
public class accountTotal {
public Account account { get; private set; }
public Opportunity total { get; private set; }
public accountTotal(Account a) {
account = a;
total = new Opportunity(amount = 0);
for(Opportunity o:a.opportunities) {
if(o.amount != null) total.amount += o.amount;
}
}
}
private List<AccountTotal> sortAccountTotals(List<AccountTotal> totals) {
List<AccountTotal> returnList = new List<AccountTotal>();
Map<Decimal, List<AccountTotal>> totalMap = new Map<Decimal, List<AccountTotal>>();
for(AccountTotal t:totals) {
if(totalMap.get(t.total.amount) == null) {
totalMap.put(t.total.amount, new List<AccountTotal>());
}
totalMap.get(t.total.amount).add(t);
}
List<Decimal> keys = new List<Decimal>(totalMap.keySet());
keys.sort();
/* Sort puts things in ascending order so for descending iterate over
the keys backwards. */
for(Integer i = (keys.size()-1);i >= 0; i--) {
returnList.addAll(totalMap.get(keys.get(i)));
}
return returnList;
}
}
And finally, the Visualforce page markup for the solution as depicted above:
<apex:page controller="accountsWithOpptysCon" tabStyle="Account">
<apex:pageBlock>
<apex:pageBlockTable value="{!accounttotals}" var="a">
<apex:column value="{!a.account.name}"/>
<apex:column headerValue="Opportunity Amount Total" value="{!a.total.amount}"/>
<apex:column headerValue="Opportunities">
<apex:pageBlockTable value="{!a.account.opportunities}" var="o">
<apex:column value="{!o.name}"/>
<apex:column value="{!o.amount}"/>
</apex:pageBlockTable>
</apex:column>
</apex:pageBlockTable>
</apex:pageBlock>
</apex:page>
As some have pointed out, there are other ways of addressing this challenge depending on your specific requirements. For example, if you need the summed total value to be part of your data model and the filter is static then you should look at creating a roll-up summary field (search for "Roll-up Summary Field" within the help system in your account) and let the platform maintain the calculation for you.
In this case, recall that from the requirements this was meant to be used within an interactive interface where the user changes the filter criteria on the fly, something that can easily be added to this example with either Dynamic SOQL (also new in Winter '09) or through bind variables in the SOQL WHERE clause.
Keeping on top of all the latest features and techniques for
efficiently achieving the needs of your business can be tough. We'll
do our best to help you here! Until next time...
TrackBack
TrackBack URL for this entry: http://www.typepad.com/services/trackback/6a00d8341cded353ef010535453e4e970b
Listed below are links to weblogs that reference Group, Sum and Order data in Apex:


Comments
Posted by Jon Mountjoy on October 6, 2008 12:42 AM:
Cool Andrew. Unfortunately your link to SOQL semi-join doesn't work, which leaves me with a question: Where is that "Id" coming from in your query?
ie. you have "Where Id IN ..." but you never return an Id anywhere. Is it implicitly returned from the SELECT?
Thanks,
Jon
Posted by Andrew Waite on October 6, 2008 06:22 AM:
Sorry about that Jon. Yes, the collection of AccountIds are returned from the sub-select implicitly. It's essentially the same effect as having done something like this:
...FROM Account WHERE Id IN (ID1, ID2, ID3, ....)
but in with the new syntax you aren't bound by the SOQL statement length limit or collection size limit in Apex in terms of the size of the collection of IDs since the the operation is passed through to the DB.
It appears deep linking into the online documentation doesn't work in all browsers (FF3 appears to work though I see Safari 3 does not).
For reference, you can find the location I linked to by starting at the top of the API documentation:
http://www.salesforce.com/us/developer/docs/api/index.htm
and following this path down the navigation tree:
Reference > Core Calls > query > Salesforce Object Query Language (SOQL) > SELECT > Comparison Operators
Or search for "Semi-Joins" and click on SELECT from the results.
From there scroll down to Semi-Joins using IN.
Posted by JP Seabury on October 6, 2008 02:27 PM:
Great article!
Posted by Jason on October 8, 2008 03:38 PM:
Great post. I have tweaked the code a little here ( http://community.salesforce.com/sforce/board/message?board.id=Visualforce&message.id=5747#M5747 ) which allows you to dynamically show and hide the Opportunity details.
One question I have is how to you limit this type of query from returning too many results? With a normal query you can easily append "limit :Limits.getLimitQueryRows()" to the end of the query. Yet with this type of query there are queries within queries and I'm not sure where I should place these limits.
-Jason
Posted by Jeff Marinchak on January 11, 2009 07:14 AM:
Thanks for sharing your work. It really helps to see how others have tackled these Visualforce challenges.
I have a similar challenge. We want to do a quick commission estimate on opportunties. I've created a couple of inputfields to capture a person's variable salary and quota. (We don't want to store these permanently in the system.) Now I need to use those numbers to calculate the commission for each opportunity.
How difficult would it be to add a couple of calculations to the output? What if you wanted to perform a calculation on each opportunity amount - using the input values? How would you display the total commission value as the amount for each opportunity got added it.
Posted by Andrew Waite on January 11, 2009 09:10 AM:
Thanks for the comment Jeff.
If I understand your goal correctly, this should be pretty straight-forward. Take a look at the accountTotal class in this example. It is essentially a model for your controller/UI structured to establish/present the data desired, in this case the accounts and for each the total.
You can do something similar with opportunity where you might have properties on your model (also often referred to as a "wrapper") class for the variables used in your calculation.