public static List<String> toAddresses = new List<String>();
/** to cache transaction data and reduce SOQLs
*/
public static List<String> getNotificationEmailAddress() {
if( toAddresses == null || toAddresses.size() == 0 ) {
Profile sysAdminProfile = [SELECT Id FROM Profile WHERE Name = 'System Administrator' limit 10];
List<User> sysAdmins = [SELECT id, Email FROM User WHERE ProfileId = :sysAdminProfile.id];
for( User sysAdmin : sysAdmins ) {
toAddresses.add ( sysAdmin.Email );
}
}
return toAddresses;
}
public static void sendMail(String subject, String body, List<String> recipients) {
try {
Messaging.SingleEmailMessage message = new Messaging.SingleEmailMessage();
message.toAddresses = recipients;
message.optOutPolicy = 'FILTER';
message.subject = subject;
message.plainTextBody = body;
Messaging.SingleEmailMessage[] messages = new List<Messaging.SingleEmailMessage> {message};
Messaging.SendEmailResult[] results = Messaging.sendEmail(messages);
if (results[0].success) {
System.debug(LoggingLevel.INFO, 'The email was sent successfully . to ' + recipients + ' subject ' + subject );
} else {
System.debug(LoggingLevel.ERROR, 'The email failed to send: ' + results[0].errors[0].message + ' to ' + recipients + ' subject ' + subject );
}
} catch(Exception e) {
System.debug(LoggingLevel.ERROR, 'The email failed to send: ' + ' to ' + recipients + ' subject ' + subject + ' body ' + body );
System.debug( e.getMessage() + '\n' + e.getStackTraceString() );
}
}
sendMail('test email','test body', new List<String> { 'chintanjshah@gmail.com' } );
sendMail('test email 2','test body 2', getNotificationEmailAddress() );
Monday, August 14, 2017
Salesforce send admin email for error/success
Simple reference code for sending success/failure email in Salesforce
Monday, August 7, 2017
Trigger framework with hierarchical kill switches
An enhancement on existing matured trigger framework from Hari K.
It is very common scenario to disable trigger logic on certain user (e.g. batch) or profile or for entire org. I have enhanced the trigger framework and source code is available at :
https://github.com/c-shah/trigger-framework
How to use it :
The framework already comes with one of the hiearchical setting called : TriggerFrameworkSettings__c.AllTriggersDisabled - this is false by default. If you need to disable all triggers, you can just check this checkbox. and all triggers will be disabled for a given user/profile/org.
This method can also be extended for individual sObject.
You can add <sObject>TriggerDisabled (e.g. AccountTriggerDisabled) custom setting under TriggerFrameworkSettings__c, and that setting will be dynamically be read for that sObject for a given user/profile/org. The trigger at individual sObject will be enabled/disabled based on flag value.
It is very common scenario to disable trigger logic on certain user (e.g. batch) or profile or for entire org. I have enhanced the trigger framework and source code is available at :
https://github.com/c-shah/trigger-framework
How to use it :
The framework already comes with one of the hiearchical setting called : TriggerFrameworkSettings__c.AllTriggersDisabled - this is false by default. If you need to disable all triggers, you can just check this checkbox. and all triggers will be disabled for a given user/profile/org.
This method can also be extended for individual sObject.
You can add <sObject
Friday, June 23, 2017
Post Install Script Framework
As an ISV, many times we have to write post install scripts for upgrade. Salesforce provides facility to do that : https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_install_handler.htm
However are quite a few limitations with this:
Hence we tried to implement same approach using change set and integrated with Salesforce Post Install Handler. Changeset is industry wide practice used for a long time and here are some benefits:
With above points in mind, we created below framework, where ISV can just plugin in any post install script with minimal effort:
Framework
1. Entry point class - which implements Salesforce interface InstallHandler
This class just calls PostInstallService.startService
2. PostInstallService.startService
This class scans for all the classes which extends PostInstallScriptTemplate in current namespace
It inserts them into PostInstallScript__c object, if it doesn't exist already.
3. PostInstallService.startService Calls PostInstallService.executeNextScript
4. PostInstallService.executeNextScript
Based on the data in PostInstallScript__c, it will call next PostInstallScriptTemplate(N)
PostInstallScriptTemplate is batch interface so execution will be done asynchronous fashion
[Note: There will be callback to PostInstallService, when PostInstallScriptTemplate(N) is completed/errored]
5. Once the PostInstallScriptTemplate(N) is completed/errored
It will update the PostInstallScript__c object with Status and Execution Log
6. PostInstallScriptTemplate(N) will call back the framework PostInstallService.executeNextScript
7. PostInstallService.executeNextScript
based on data in PostInstallScript__c in, it will either:
a) Halt execution (if error)
b) Move on to the next script
c) Move on to finish if all of post install scripts are successfully completed
8. A user interface to display currently pending/Completed/Errored Scripts along with Execution Logs
9. A user interface provides facility to resubmit if Errored
Post Install Scripts
ISV can write Post Install script in two ways:
1) If Batch context is not needed, we can write Post install script as below,
description, sequence number and execution log is stored in database.
Actual post install logic is in executeScript method
2) If we need to query 50k+ records or update 10k+ records, we have to use Batch context and here is another way to write post install Script. In this case, we will need to override start and finish methods just like we do for any Salesforce Batch
Source code
1. It can be found at github : https://github.com/c-shah/PostInstallScriptFramework
2. Or as unmanaged package : https://login.salesforce.com/packaging/installPackage.apexp?p0=04tf400000099bW
However are quite a few limitations with this:
- It is hard to make sure if post install code is already executed or not
- When developing upgrade script, we don't know what version it is going to be when it gets published
- Hard to stop the execution or do the retry
Hence we tried to implement same approach using change set and integrated with Salesforce Post Install Handler. Changeset is industry wide practice used for a long time and here are some benefits:
- Multiple Post Install Scripts
- Execution of Post Install Scripts in Order - and only once
- On Error in any Script
- Stop/Halt the execution
- Send Error Email
- On Successful completion of all scripts
- send summary email of all scripts
- Each script gets full set of governance limit
- In case of Salesforce, entire Batch is devoted to a given script
With above points in mind, we created below framework, where ISV can just plugin in any post install script with minimal effort:
Framework
1. Entry point class - which implements Salesforce interface InstallHandler
This class just calls PostInstallService.startService
2. PostInstallService.startService
This class scans for all the classes which extends PostInstallScriptTemplate in current namespace
It inserts them into PostInstallScript__c object, if it doesn't exist already.
3. PostInstallService.startService Calls PostInstallService.executeNextScript
4. PostInstallService.executeNextScript
Based on the data in PostInstallScript__c, it will call next PostInstallScriptTemplate(N)
PostInstallScriptTemplate is batch interface so execution will be done asynchronous fashion
[Note: There will be callback to PostInstallService, when PostInstallScriptTemplate(N) is completed/errored]
5. Once the PostInstallScriptTemplate(N) is completed/errored
It will update the PostInstallScript__c object with Status and Execution Log
6. PostInstallScriptTemplate(N) will call back the framework PostInstallService.executeNextScript
7. PostInstallService.executeNextScript
based on data in PostInstallScript__c in, it will either:
a) Halt execution (if error)
b) Move on to the next script
c) Move on to finish if all of post install scripts are successfully completed
8. A user interface to display currently pending/Completed/Errored Scripts along with Execution Logs
9. A user interface provides facility to resubmit if Errored
Post Install Scripts
ISV can write Post Install script in two ways:
1) If Batch context is not needed, we can write Post install script as below,
description, sequence number and execution log is stored in database.
Actual post install logic is in executeScript method
/** * Created by cshah on 5/30/2017. */ public with sharing class PostInstallScript1 extends PostInstallScriptTemplate { private String executionLog; public override void executeScript(Database.BatchableContext bc, List<SObject> sObjects) { System.debug('PostInstallScript1 : execute : hoping to get executed only once '); executionLog = 'script 1 done. '; } public override Integer getSequenceNumber() { System.debug('PostInstallScript1 : getSequenceNumber '); return 1; } public override String getExecutionLog() { System.debug('PostInstallScript1 : getExecutionLog '); return executionLog; } public override String getDescription() { System.debug('PostInstallScript1 : getDescription '); return 'Script 1 Description '; } }
2) If we need to query 50k+ records or update 10k+ records, we have to use Batch context and here is another way to write post install Script. In this case, we will need to override start and finish methods just like we do for any Salesforce Batch
/** * Created by cshah on 5/30/2017. */ public without sharing class PostInstallScript3 extends PostInstallScriptTemplate { private String executionLog; private Integer processedCount = 0; public override void executeScript(Database.BatchableContext bc, List<SObject> sObjects) { System.debug('PostInstallScript3 : execute : hoping to get executed only once '); executionLog = 'script 3 done. '; processedCount += sObjects.size(); } public override Integer getSequenceNumber() { System.debug('PostInstallScript3 : getSequenceNumber '); return 3; } public override String getExecutionLog() { System.debug('PostInstallScript3 : getExecutionLog '); executionLog = ' Processed ' + processedCount + ' records '; return executionLog; } public override String getDescription() { System.debug('PostInstallScript3 : getDescription '); return 'Script 3 Description '; } public override Integer getBatchSize() { System.debug('PostInstallScript3 : getBatchSize '); return 1; } public override Database.QueryLocator startScript(Database.BatchableContext bc) { System.debug('PostInstallScript3 : startScript '); return Database.getQueryLocator('select id, name from account limit 201'); } public override void finishScript(Database.BatchableContext bc) { System.debug('PostInstallScript3 : finishScript '); } }
User Interface
User interface allows to view the post install script, and their execution log.
It allows to resubmit in case of error
Source code
1. It can be found at github : https://github.com/c-shah/PostInstallScriptFramework
2. Or as unmanaged package : https://login.salesforce.com/packaging/installPackage.apexp?p0=04tf400000099bW
Monday, May 1, 2017
OData/Heroku with Salesforce - Integrate differently
As we usually come across, below is standard pattern when we integrate external app talking to Salesforce or Salesforce talking to external application.
We use different API to talk to Salesforce and use workflow outbound, or rest/soap calls to make outbound call.
Below is different approach using OData, and in many cases it can make the integration very simple and minimal to no code on salesforce.
What is OData?
It is a standard way to represent data. Detail can be found at : http://www.odata.org/documentation/ however, at very high level it is web service way of representing data like relational database. Main features:
Metadata:
There is metadata ($metdata) to get information about all schemas, tables, columns, and procedures.
SQL like operation
We could do SQL like operation instead of creating new operation for each type (e.g. get employee by first name, by last name, etc..)
Here is naming convention (left: Classic Relational Database, right: OData 4.0 naming)
Heroku
Heroku is very well known and a lot of documentation could be found at https://www.heroku.com/, so would be focus only on two items:
Heroku Connect
Single or bi directional link to Salesforce tables/fields to Heroku tables/fields. Any changes to Salesforce is migrated to Heroku Postgres database over extremely fast and efficient SQL link. And if bidirectional link is configured, any changes on Heroku is posted back to Salesforce.
Heroku App Engine
We could host custom Java/Node and other supported language application on Heroku with just click of a button. Hence, I wrote custom Java app using Apache Olingo to host on Heroku platform.
This app generates metadata and connects to heroku postgres to get data and exposes everything as OData service using Apache Olingo framework.
Notes:
Putting everything together
On Salesforce side, it would be just providing URL and it will automatically list down all objects and it will be able to create objects as selected.
Final Take
Code for Apache Olingo implementation can be found at :
https://github.com/spring-work/odata
Heroku App:
http://odata-cshah.herokuapp.com/odata.svc/
Below is different approach using OData, and in many cases it can make the integration very simple and minimal to no code on salesforce.
What is OData?
It is a standard way to represent data. Detail can be found at : http://www.odata.org/documentation/ however, at very high level it is web service way of representing data like relational database. Main features:
Metadata:
There is metadata ($metdata) to get information about all schemas, tables, columns, and procedures.
SQL like operation
We could do SQL like operation instead of creating new operation for each type (e.g. get employee by first name, by last name, etc..)
Here is naming convention (left: Classic Relational Database, right: OData 4.0 naming)
Heroku
Heroku is very well known and a lot of documentation could be found at https://www.heroku.com/, so would be focus only on two items:
Heroku Connect
Single or bi directional link to Salesforce tables/fields to Heroku tables/fields. Any changes to Salesforce is migrated to Heroku Postgres database over extremely fast and efficient SQL link. And if bidirectional link is configured, any changes on Heroku is posted back to Salesforce.
Heroku App Engine
We could host custom Java/Node and other supported language application on Heroku with just click of a button. Hence, I wrote custom Java app using Apache Olingo to host on Heroku platform.
This app generates metadata and connects to heroku postgres to get data and exposes everything as OData service using Apache Olingo framework.
Notes:
- Had to use Tomcat (instead of http://sparkjava.com) as Olingo requires servlet
- Need to implement two interfaces
- Metadata Interface (to render schema, entity, entity set)
- Data Processor (to fetch and return the data)
- Had to remove http header accept, as causing issue with Apache Olingo
Putting everything together
- Once it is exposed as OData Service, Salesforce can connect and all the EntitySet exposed in OData would be available as external object on Salesforce (ends with __x)
- We can do SOQL, SOSL, indirect lookup on those Salesforce Object
- This would be zero code on Salesforce and on Heroku side, we can get data from Cache, PostGres or External App using Rest or SOAP api
On Salesforce side, it would be just providing URL and it will automatically list down all objects and it will be able to create objects as selected.
Final Take
- Reiterating the first diagram, Heroku Connect provides alternative to accessing and updating data via API and it is super helpful if app is living on Heroku or would like to connect directly to postgres database
- Salesforce can connect to external app via Odata on heroku, and that would reduce the code on Salesforce org and promote more click over code approach
Code for Apache Olingo implementation can be found at :
https://github.com/spring-work/odata
Heroku App:
http://odata-cshah.herokuapp.com/odata.svc/
Thursday, February 2, 2017
Call Salesforce REST API from Apex
Nothing new but just below example helps make call to Salesforce REST API from Apex. If you need to know Org limits at run time, most them are available via Limits call, but some are available via rest api at : /services/data/v37.0/limits (e.g. daily async limits, email or bulk email limits, etc.) and it is easy to get those information from workbench.developerforce.com, however if you need to add this to the code, below is the code:
Add your Org URL to remote site setting
Note: if you don't know, then either you can look at browser or via below call:
System.debug( URL.getSalesforceBaseUrl().toExternalForm() );
Run Anonymous block below, which is broken down into three pieces
1. Get the base URL
2. Get Auth Token
3. Actual HTTP Request
/* 1. get base URL */
public static String getSalesforceInstanceUrl() {
return URL.getSalesforceBaseUrl().toExternalForm();
}
public static String getRestResponse(String url) {
HttpRequest httpRequest = new HttpRequest();
httpRequest.setEndpoint(url);
httpRequest.setMethod('GET');
/* 2. set the auth token */
httpRequest.setHeader('Authorization', 'OAuth ' + UserInfo.getSessionId());
httpRequest.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionID());
try {
Http http = new Http();
/* initiate the actual call */
HttpResponse httpResponse = http.send(httpRequest);
if (httpResponse.getStatusCode() == 200 ) {
return JSON.serializePretty( JSON.deserializeUntyped(httpResponse.getBody()) );
} else {
System.debug(' httpResponse ' + httpResponse.getBody() );
throw new CalloutException( httpResponse.getBody() );
}
} catch( System.Exception e) {
System.debug('ERROR: '+ e);
throw e;
}
return null;
}
System.debug(' -- limit method code block -- : start ');
String baseUrl = getSalesforceInstanceUrl();
System.debug(' -- baseUrl-- : ' + baseUrl );
String limitsUrl = baseUrl + '/services/data/v37.0/limits';
System.debug(' -- limitsUrl-- : ' + limitsUrl );
String response = getRestResponse(limitsUrl);
System.debug(' -- response-- : ' + response );
Tuesday, November 15, 2016
Salesforce SOQL tricks
General Governor Limits
For testing, we have 200,000+ records in EventQueue__c table
Scenario 1
Issue
Limit
Scenario 2
Issue
Limit
Scenario 3
Issue
Limit to 10k
Scenario 4
Issue
| Max query records | 50,000 |
|---|---|
| Max update records | 10,000 |
| Max Batch Size | 2,000 |
| SOQL for loop | 200 |
| Sensitive Selection Threshold | 200,000 |
| Number of SOQL (Sync) | 100 |
| Number of SOQL (Batch) | 200 |
For testing, we have 200,000+ records in EventQueue__c table
Query Editor
Anonymous Block
Select id from EventQueue__c
Anonymous Block
List<EventQueue__c> eventQueues = [Select id from EventQueue__c];
Issue
- The Query from Query Editor would work fine
- Apex anonymous block will error out as it will return more than 50k records.
Possible Fix
Limit
List<EventQueue__c> eventQueues = [Select id from eventQueue__c limit 50000];
SOQL for loop
It is better as it batches up in 200. Internally it uses batch api if you look at the logs
It is better as it batches up in 200. Internally it uses batch api if you look at the logs
for(List eventQueues : [Select id from EventQueue__c limit 50000] ) {
System.debug(' eventQueues ' + eventQueues.size() );
}
Scenario 2
Query Editor
Anonymous Block
select count() from EventQueue__c
Anonymous Block
Integer eventCount = Database.countQuery('select count() from EventQueue__c');
Issue
- The Query from Query Editor would work fine
- Apex anonymous block will error out as count() has to work against 50000+ rows
Possible Fix
Limit
Integer eventCount = Database.countQuery('select count() from EventQueue__c limit 50000');
Scenario 3
Anonymous Block
List eventQueues = new List();
for(List eventQueuesBatch : [Select id from EventQueue__c limit 50000] ) {
eventQueues.addAll( eventQueuesBatch );
}
update eventQueues;
Issue
- Apex anonymous block will error out on update statement because we can not update more than 10k records in DML.
Possible Fix
Limit to 10k
List eventQueues = new List();
for(List eventQueuesBatch : [Select id from EventQueue__c limit 10000] ) {
eventQueues.addAll( eventQueuesBatch );
}
update eventQueues;
Scenario 4
Query Editor
Anonymous Block
select count() from EventQueue__c where deleteFlag__c = true limit 1
Anonymous Block
Integer eventCount = Database.countQuery('select count() from EventQueue__c where deleteFlag__c = true limit 1');
- The Query from Query Editor would work fine
- Apex anonymous block will error out because when object has more than 200,000+ records, select clause has to be to efficient. E.g. in above query deleteFlag__c is not indexed and more than half of the records have deleteFlag__c.
- Error Message : Non-selective query against large object type (more than 200000 rows). Consider an indexed filter or contact salesforce.com about custom indexing. Even if a field is indexed a filter might still not be selective when: 1. The filter value includes null (for instance binding with a list that contains null) 2. Data skew exists whereby the number of matching rows is very large (for instance, filtering for a particular foreign key value that occurs many times)
Possible Fix
As suggested, Use indexed column (e.g. lookup field - foreign key to constraint the return results) as suggested in error.Thursday, October 27, 2016
Apex Describe Methods
In dynamic SOQL, it is very critical to check if Object exists and related field also exists. Nothing fancy, but below code comes quite handy to check if Object and field exists:
To Verify:
Results:
public static boolean fieldExists(String objectName, String fieldName) {
try {
Schema.SObjectType salesforceObject = Schema.getGlobalDescribe().get(objectName);
Map<String, Schema.SObjectField> fields = salesforceObject.getDescribe().fields.getMap();
for(String field : fields.keySet() ) {
if( field.equalsIgnoreCase(fieldName) ) {
return true;
}
}
} catch(Exception e) {
System.debug(e);
return false;
}
return false;
}
public static boolean relationshipExists(String objectName, String relationshipName) {
try {
Schema.SObjectType salesforceObject = Schema.getGlobalDescribe().get(objectName);
Map<String, Schema.SObjectField> fields = salesforceObject.getDescribe().fields.getMap();
for(String field : fields.keySet() ) {
if( fields.get(field).getDescribe().getType() == Schema.DisplayType.Reference && fields.get(field).getDescribe().getRelationshipName() != null && fields.get(field).getDescribe().getRelationshipName().equalsIgnoreCase(relationshipName) ) {
return true;
}
}
} catch(Exception e) {
System.debug(e);
return false;
}
return false;
}
To Verify:
Boolean b1 = relationshipExists('Account','Owner');
Boolean b2 = relationshipExists('Quote','Owner');
Boolean b3 = relationshipExists('ACCOUNT','OWNER');
Boolean b4 = relationshipExists('ACCOUNT','OWNER-');
System.debug(' b1 ' + b1 );
System.debug(' b2 ' + b2 );
System.debug(' b3 ' + b3 );
System.debug(' b4 ' + b4 );
Results:
b1 true
b2 false
b3 true
b4 false
Subscribe to:
Posts (Atom)





