Monthly Archives: June 2015

REST SOQL OFFSET and LIMIT – 2000 records

I recently ran into an issue with an Apex batchable class that executed a REST SOQL query performed from SFDC org A against SFDC org B

The batch Apex code in org A was written to handle REST responses that looked like this:

{"totalSize":12,
"done":true | false,
"nextRecordsUrl" : "/services/data/v30.0/query/01g8000001J2eXvAAJ-2000",

"records":[
{"attributes":{
"type" : "the sobject",
"url" : "/services/data/v30.0/sobjects/the sobject/the id"
},
"field0 in query" : "value of field 0",
"field1 in query" : "value of field1",
...},
next record ...
]
}

If "done" : false was present, then, once the iterator reached the end of the retrieved SObjects, it would do an HTTP Request against the value in nextRecordsUrl .

Hence, the batch program could retrieve a large number of records.

And your problem was what?

Turns out my query string used in the original REST GET was of this form:

select id,name from Opportunity where closeDate = THIS_QUARTER Limit 10000 OFFSET 0

Even though there were more than 2000 Opportunities in Org B and even though the coding for the batchable class’s iterator could deal with the queryMore logic, the total number of records retrieved was exactly 2000.

Analysis

Turns out, if you code OFFSET 0, the maximum value returned in the totalSize JSON property of the REST response body is 2000 – regardless of there being more than 2000 rows in the queried org.

Solution
As there was already logic to deal with queryMore(), I simply removed the OFFSET clause. I verified there was no logic to increment OFFSET and repeat the query.

Why was there queryMore() logic in the code in the first place – why not implement LIMIT and OFFSET per SFDC spec?

Turns out, if the retrieved payload from a given query exceeds the maximum transfer size per REST response, SFDC will break up the query results into segments, indicated by the presence of "done": false. These segments will be a maximum of 2000 records but could be (and in my case were – do to payload mass) fewer. The queryMore logic had to be added anyway. Once this was present, the OFFSET portion of the SOQL became superfluous and should have been removed everywhere. Unfortunately, a vestigial OFFSET 0 was present causing ther REST query to only retrieve 2000 rows before saying – I’m done.

Visualforce onchange event not working?

Simple post here

I had a VF page where the onchange event within a pageBlockTable column cell wasn’t executing

<apex:column headerValue="some hdr">
  <apex:actionRegion >
    <apex:inputField value="{!r.foo}">
	<apex:actionSupport event="onChange" rerender="thePageBlockTableId"/>
    </apex:inputField>
  </apex:actionRegion>	
   <apex:inputField value="{!r.bar}" rendered="{!r.foo <> 'xyz'}"/>
</apex:column>

When I click on the cell for the foo column and change the inputField, the pageBlockTable does not rerender

Huh?

Problem is that unlike most of Apex and Visualforce, the event attribute values are case sensitive.

Solution?

replace event="onChange" with event="onchange"

Some additional information on case senstivity in Apex and Visualforce can be found here

inputFile body not Transmitted to VF Controller

Here was the use case:

  1. Display step 1 of a VF page with a commandLink to jump to step 2
  2. Step 2 was initially rendered invisible
  3. Step2 included an apex:inputFile component

When Step 2 was rendered, the inputFile component appeared.

But, if you select a file and click a button to upload the file to the controller, the controller does not receive the file body but does receive the filename and content type.

Huh?

The issue appears to be that initially unrendered apex:inputFile components don’t bind as expected to a controller variable declared as transient (as it typically must be to avoid Visualforce viewstate size issues). The upload action method occurs, all other non-transient attributes of inputFile do transmit but the file body does not.

Here’s proof

The controller

public with sharing class Foo {
	// Class to investigate rerender issue with inputFile
	public Boolean				hasStep1Completed		{get {return this.hasStep1Completed == null ? false : this.hasStep1Completed;} set;}						// vis control
	
	public transient Blob  		fileBody				{get; set;}		// must be transient as view state can't handle > 128KB				
	public String       		fileDescription			{get; set;}
	public String  				fileName				{get; set;}		
  	public Integer       		fileSize				{get; set;}
	public String       		fileType				{get; set;}
	
	public Boolean 				isFileBodyNullOnXmit	{get; set;}		// VF feedback for example
	
	public Foo() {}
	
	public PageReference upload() {
		this.isFileBodyNullOnXmit = this.fileBody == null ? true : false;
		return null;
	}
}

This VF page does not work

<apex:page controller="Foo"  tabStyle="Account">
 <apex:form id="form">
  <apex:outputPanel id="step1Op">
   <apex:pageBlock title="Step 1" id="step1Pb"  rendered="{!NOT(hasStep1Completed)}">
	<apex:actionregion >
	 <apex:commandLink value="Go to step2"  reRender="step1Op,step2Op">
		<apex:param name="hasStep1Completed" value="true" assignTo="{!hasStep1Completed}"/>
	 </apex:commandLink>
	</apex:actionregion>
   </apex:pageBlock>
  </apex:outputPanel>

<!--  This doesn't work, initially unrendered inputFile won't transmit file body once rendered -->
  <apex:outputPanel id="step2Op" >
   <apex:pageBlock title="Step2" id="step2Pb" rendered="{!hasStep1Completed}" >
	<apex:pageBlockButtons location="top">
         <apex:commandButton value="Upload" action="{!upload}"/>
	</apex:pageBlockButtons>
	<apex:inputFile value="{!fileBody}" filename="{!fileName}" contentType="{!fileType}" id="file"/>
   </apex:pageBlock>
 </apex:outputPanel>		
  
 <apex:outputPanel id="resultOp" >
   <apex:pageBlock rendered="{!hasStep1Completed}">
	<apex:outputText value="is FileBody Null On Xmit? {!isFileBodyNullOnXmit}"/>
   </apex:pageBlock>
 </apex:outputpanel> 
		
 </apex:form>
</apex:page>

The apex:inputFile is not initially rendered as Step1 hasn’t completed
FileXmitStep1

Now, by clicking the commandLink, I render it and then choose a file and click Upload
FileXmitStep2

The results outputPanel shows that no fileBody was transmitted!
FileXmitResultnullBodyisTrue

So, how to work around this?

The answer was provided in Salesforce StackExchange. Instead of using VF rendered= attributes, the inputFile must be rendered by made invisible by CSS. So, the revised Visualforce page (no changes to the controller)

<apex:page controller="Foo"  tabStyle="Account">
 <apex:form id="form">
  <apex:outputPanel id="step1Op">
   <apex:pageBlock title="Step 1" id="step1Pb"  rendered="{!NOT(hasStep1Completed)}">
	<apex:actionregion >
	 <apex:commandLink value="Go to step2"  reRender="step1Op,step2Op">
		<apex:param name="hasStep1Completed" value="true" assignTo="{!hasStep1Completed}"/>
	 </apex:commandLink>
	</apex:actionregion>
   </apex:pageBlock>
  </apex:outputPanel>

<!--  This does work, initially rendered by css invisible inputFile will transmit file body once made visible --> 
  <apex:outputPanel id="step2Op" style="display: {!IF(hasStep1Completed, 'inline-block', 'none')};">
   <apex:pageBlock title="Step2" id="step2Pb" >
    <apex:pageBlockButtons location="top">
	<apex:commandButton value="Upload" action="{!upload}"/>
    </apex:pageBlockButtons>
    <apex:inputFile value="{!fileBody}" filename="{!fileName}" contentType="{!fileType}" id="file"/>
  </apex:pageBlock>
 </apex:outputPanel>		
  
 <apex:outputPanel id="resultOp" >
   <apex:pageBlock rendered="{!hasStep1Completed}">
	<apex:outputText value="is FileBody Null On Xmit? {!isFileBodyNullOnXmit}"/>
   </apex:pageBlock>
 </apex:outputpanel> 
		
 </apex:form>
</apex:page>

Only the second outputPanel was changed. Note the use of style="display: {!IF(hasStep1Completed, 'inline-block', 'none')};" to trick Visualforce into thinking the inputFile is rendered but using display: none to make invisible.

The sequence of steps now is:
FileXmitStep1
FileXmitStep2

The results outputPanel shows that fileBody was transmitted! Problem solved.
FileXmitResultnullBodyisFalse

Conga Composer – INVALID_FIELD: No such relation ‘Product2’ on entity ‘OpportunityLineItem’

I had built a Conga Query that exploited the Product2 relationship from the OpportunityLineItem. Yet, when I used this in a Conga Composer URL, I got this error:

INVALID_FIELD: No such relation 'Product2' on entity 'OpportunityLineItem'

I know that the Product2 relationship from OpportunityLineItem was added by SFDC in V30

yet my Conga Composer URL was V33:

&serverUrl=https://mydomain.my.salesforce.com/services/Soap/u/33.0/someId

So, what gives?

Per Conga Support, the serverUrl is used by Conga solely for determining the instance name and they smash the rest of the URL with V29 (!%!).

Of course, this could change in the future so, hopefully, this post will become obsolete.

Workaround
Instead of using the OpportunityLineItem.Product2 relationship, you can fallback to the OpportunityLineItem.PricebookEntry.Product2 relationship. I don’t know what I would do were I needed a Conga Query on Order – an SObject introduced in V30

Conga Composer QVar used within QueryId pv

I was working on making my Conga Composer button more compact and easier to maintain by leveraging the &QVar0Id and &QVar0Fmt value to return a list of strings to be substituted into a Conga Query ...IN (...) ... expression but I couldn’t get it to work. With assistance from Conga Tech Support, here are the details of the problem and solution:

Conga Query 1
select id, opportunity.owner.name, account.name
from Opportunity where closedate = {pv2} and owner.userrole.name IN({pv1})

Note the use of {pv1} within the IN expression. Since owner.userrole.name is a string, you would expect the value passed into {pv1} to look like 'Sales','Marketing' – that is, a comma-delimited list of strings.

So, I set up Conga Query 2 that would always return the Sales and Marketing roles as follows:
select name from userRole where name in ({pv1})

And finally, my Composer button looked like this:

https://www.appextremes.com/apps/Conga/Composer.aspx
?SessionId={!API.Session_ID}
&ServerUrl={!API.Partner_Server_URL_210}
&Id={!$User.ID__c}
&QVar0Id=secondQueryId?pv1='Sales'|'Marketing'
&QVar0Fmt=11000
&QueryId=
firstQueryId?pv1={QVar0}~pv2=THIS_QUARTER,
firstQueryId?pv1={QVar0}~pv2=NEXT_QUARTER,
firstQueryId?pv1={QVar0}~pv2=LAST_QUARTER,
...

Note that the value returned from executing the second Conga Query that is, QVar0Id, is returned formatted per QVar0Fmt, in this case, 11000 means string-delimited, comma-separated, or, 'Sales','Marketing'

So, I expected that when Conga resolved the first query, it would take 'Sales','Marketing' and inject it into the value of the first query’s {pv1}, generating this:

select id, opportunity.owner.name, account.name
from Opportunity where closedate = THIS_QUARTER and owner.userrole.name IN('Sales','Marketing')

Valid SOQL.

But this doesn’t work.

You get this error in Conga (on the View Data workbook)
MALFORMED_QUERY:
Marketing') and closeDate = {pv2}
^
ERROR at Row:1:Column:109
Bind variables only allowed in Apex code

The Solution
You can’t use QVar0Fmt to return the result as comma-separated. It needs to be pipe-separated. In retrospect, this is not surprising as passing comma-separated values to the the pv1, pv2, … parameters of a report filter need to be pipe-separated (as described in the Conga doc). I naively thought that anything injected into a Conga (hence SOQL) query should be valid SOQL syntax but it is clear that Conga post-processes the pipes into commas before executing the SOQL.

Hence, QVar0Fmt needs to be 21000

By changing the Composer button to:

https://www.appextremes.com/apps/Conga/Composer.aspx
?SessionId={!API.Session_ID}
&ServerUrl={!API.Partner_Server_URL_210}
&Id={!$User.ID__c}
&QVar0Id=secondQueryId?pv1='Sales'|'Marketing'
&QVar0Fmt=21000
&QueryId=
firstQueryId?pv1={QVar0}~pv2=THIS_QUARTER,
firstQueryId?pv1={QVar0}~pv2=NEXT_QUARTER,
firstQueryId?pv1={QVar0}~pv2=LAST_QUARTER,
...

The results of the second query come back as 'Sales'|'Marketing', and are injected into the first query yielding this:
select id, opportunity.owner.name, account.name
from Opportunity where closedate = THIS_QUARTER and owner.userrole.name IN('Sales'|'Marketing')

but Conga clears this up before executing the SOQL and the Conga template is properly populated with data from the Opportunity