Salesforce中常用技能总结(纯粹干货,深度积累)图解

时刻分享,时刻感恩!

154、【在User Interface启用Custom Object Truncate】:Truncating Custom Objects | Manage Deleted Custom Objects

对于Admin而言,SF提供了Mass Delete标准对象功能,如果需要mass hard delete诸如BatchJobLog / Integration Log等自定义对象数据,可以考虑此功能。

You can’t truncate standard objects or custom objects that:

  • Are referenced by another object through a lookup field or that are on the master side of a master-detail relationship
  • Are referenced in a reporting snapshot
  • Have a custom index or an external ID
  • Have activated skinny tables

Truncating a custom object erases:

  • All records currently sitting in the custom object’s Recycle Bin
  • The custom object’s history
  • Related events, tasks, notes, and attachments for each deleted record

153、【String缩略显示超长字符串】:abbreviate

String message = '12345678901';
System.debug(message.abbreviate(10));

// 14:45:40:003 USER_DEBUG [2]|DEBUG|1234567...

142、【查找All Communities关键信息】:其中portal_id可被用作SAML2.0的Portal SSO必要输入

SELECT Id, Name, UrlPathPrefix, Status, OptionsSiteAsContainerEnabled FROM Network 

141、【字符串常用操作】:

#1. '["A", "B"]'字符串转数组("A", "B")

// 方法一:正则替换
String s = '["A", "B"]';
System.debug(LoggingLevel.INFO, '*** s: ' + s);
String result = s.replaceAll('\\[', '').replaceAll('\\]', '');
System.debug(LoggingLevel.INFO, '*** result: ' + result);
List<String> aList = result.split(',');
System.debug(LoggingLevel.INFO, '*** aList: ' + aList);

// 方法二:字符串截取
String s = '["A", "B"]';
System.debug(LoggingLevel.INFO, '*** s: ' + s);
String result = s.substring(1, s.length()-1);
System.debug(LoggingLevel.INFO, '*** result: ' + result);
List<String> aList = result.split(',');
System.debug(LoggingLevel.INFO, '*** aList: ' + aList);

140、【判断当前用户是否为标准的系统管理员】:由于系统管理员可以自行修改语言,如果简单的使用System Administrator作判断会造成图示的问题 - 取不到值

SELECT Profile.Name FROM PermissionSet WHERE IsCustom = FALSE AND Profile.UserType = 'Standard' AND Profile.PermissionsAuthorApex = TRUE

139、【SOQL中使用聚合函数返回结果过滤数据】:HAVING

HAVING is an optional clause that can be used in a SOQL query to filter results that aggregate functions return.

注意如果聚合函数返回结果很多,需要使用LIMIT 2000加以限制

SELECT Floor_Unit__c, COUNT(Id) totalSize FROM Unit__c WHERE Floor_Unit__c != null GROUP BY Floor_Unit__c HAVING COUNT(Id) > 1

138、【String Split的特别用法】:String Split function fails if using a dot as criteria

// 调整前
String rfieldStr = 'Owner.Email;
List<String> fields = rfieldStr .split('.');
System.debug(fields);// empty result

// 调整后
String rfieldStr = 'Owner.Email;
List<String> fields = rfieldStr .split('\\.');
System.debug(fields);// result as we expected

137、【v50.0及以上版本Apex中的安全导航符?.用例】:How to Use Safe Navigation Operator (?.) in Salesforce Apex | Winter 21 Release Notes

作为Salesforce Winter 21发行版的一部分,salesforce引入了安全导航操作符(?.)来避免Apex中的空指针异常。这对于开发人员而言非常有用。如果我们需要检查某些内容(是否为对象,
map,list ..)不应为null,然后在末尾写(?.)。如果expression(?.)的左侧为null,则不评估右侧,结果将为null。

// #1 示例1
if (object!= null){
    String s = object.fieldName;
}

// 使用v50.0及以上api版本,可改写为
String s = object?.fieldName;

// #2 示例2
String accountName;
if(accountIdAccountMap <> null && accountIdAccountMap.get(accId) <> null)
    accountName = accountIdAccountMap.get(accId).Name;

// 使用v50.0及以上api版本,可改写为
String accountName = accountIdAccountMap?.get(accId)?.Name;

136、【SOQL中的Date Literals】:Date Formats and Date Literals

说明:Date Literals中的日期表达式代表的是一个时间段(时间范围),而非时间点,以下是以today = 2020-12-23做的测试。

135、【SOQL中的FIELDS功能】:SP21 DE Registration | SP21 Release Notes | FIELDS()

Bounded and Unbounded Queries

The API distinguishes bounded queries, which have well-defined sets of fields, from unbounded queries, which have sets of fields that the API can’t determine in advance. For example, because the number of custom fields for an object is not predetermined, FIELDS(CUSTOM) and FIELDS(ALL) are considered unbounded. This table shows the support for FIELDS() in bounded and unbounded queries:

 Bounded – FIELDS(STANDARD)Unbounded – FIELDS(ALL) and FIELDS(CUSTOM)
Apex (inline and dynamic)SupportedNot supported
Bulk API 2.0SupportedNot supported
CLISupportedSupported only if the result rows are limited. See Limiting Result Rows.
SOAP API and REST APISupportedSupported only if the result rows are limited. See Limiting Result Rows.

134、【SOSL用法示例】:
a. Developer Console用法:

FIND {United Oil*} 
IN ALL FIELDS  
RETURNING Account(Id), Contact(Id), Lead(Id)

b. Apex用法 - SOSL数据绑定:

public class CustomSearchController {
    @AuraEnabled
    public static List<String> searchForIds(String searchText) {
        List<List<SObject>> results = [FIND :searchText IN ALL FIELDS  RETURNING Account(Id), Contact(Id), Lead(Id)];
        List<String> ids = new List<String>();
        for (List<SObject> sobjs : results) {
            for (SObject sobj : sobjs) {
                ids.add(sobj.Id);
            }
        }
        return ids;
    }
}

c. Apex用法 - Dynamic SOSL:

Public with sharing class SOSLController{
    Public List<Opportunity> optyList{get;set;}
    Public List<contact> conList{get;set;}
    Public List<account> accList{get;set;}
    
    Public String searchStr{get;set;}
    Public SOSLController() {

    }
    
    Public void soslDemo_method() {
        optyList = New List<Opportunity>();
        conList = New List<contact>();
        accList = New List<account>();
        if(searchStr.length() > 1) {
            String searchStr1 = '*'+searchStr+'*';
            String searchQuery = 'FIND \'' + searchStr1 + '\' IN ALL FIELDS RETURNING  Account (Id,Name,type),Contact(name,email),Opportunity(name,StageName)';
            List<List <sObject>> searchList = search.query(searchQuery);
            accList = ((List<Account>)searchList[0]);
            conList  = ((List<contact>)searchList[1]);
            optyList = ((List<Opportunity>)searchList[2]);
            System.debug('dk..'+searchList +':account:'+accList );
            if(accList.size() == 0 && conList.size() == 0 && optyList.size() == 0) {
                apexPages.addmessage(new apexpages.message(apexpages.severity.Error, 'Sory, no results returned with matching string...'));
                return;
            }
        }else {
            apexPages.addmessage(new apexpages.message(apexpages.severity.Error, 'Please enter at least two characters...'));
            return;
        }
    }
}

133、【SF15位ID VS 18位ID】:15 or 18 Character IDs in Salesforce.com | 详解 Salesforce 15 和 18 位的 Record IDs

Salesforce.com IDs are case-sensitive so if you wanted to export data to Excel to be used as a lookup table using the VLOOKUP formula, you’ll need to be aware of this. If you happened to have records with IDs of 001A0000006Vm9r and 001A0000006VM9r the VLOOKUP formula, not being “case-aware”, would always find the first record, regardless of the case of the lookup value. Salesforce.com realized the potential problem with this and instigated the 18-character ID.

132、【使用Tooling API进行系统Health Check】:SecurityHealthCheckRisks
支持的RiskType

  • HIGH_RISK
  • MEDIUM_RISK
  • MEETS_STANDARD

支持的SettingRiskCategory

  • HIGH_RISK
  • MEDIUM_RISK
  • LOW_RISK
  • INFORMATIONAL
SELECT RiskType, Setting, SettingGroup, OrgValue, StandardValue FROM SecurityHealthCheckRisks where RiskType='HIGH_RISK'

SELECT Setting, SettingGroup, StandardValue, SettingRiskCategory FROM SecurityHealthCheckRisks

效果:

131、【使用String.format(template, args)处理邮件模板占位】:
效果预览:

代码示例:

String template = '{0}, has updated the following new task: \n\n';
template+= 'Subject - {1}\n';
template+= 'Status - {2}\n';
template+= 'Priority - {3}\n';

List<String> args = new List<String>();
//args.add(tsk.LastModifiedBy.name);
//args.add(tsk.Subject);
//args.add(tsk.Status);
//args.add(tsk.Priority);

args.add('Tom');
args.add('Lead01');
args.add('Completed');
args.add('Normal');

String formattedHtml = String.format(template, args);
System.debug('......formattedHtml: ' + formattedHtml);

拓展示例:

public static String SOQL_TEMPLATE = 'SELECT {0} FROM {1} WHERE {3} in :masterIds {2} GROUP BY {3}';

String soql = String.format(SOQL_TEMPLATE, new String[]{'COUNT(Id)', 'Contact', 'AccountId', 'LeadSource'});
System.debug('SOQL is ' + soql);

130、【SOQL中使用NOT LIKE】:
用例:比如想查看sandbox中哪些Active的User的Email不包含invalid,以便添加invalid标识,这时需用到NOT LIKE;
用法:(NOT Field__c LIKE '%invalid')

List<User> uList = [SELECT Id, Name, Email, UserRole.Name FROM User WHERE IsActive = TRUE AND (NOT Email LIKE '%invalid') ORDER BY Email];

129、【初识OpenActivity & ActivityHistory】:OpenActivity | ActivityHistory
a. 这两个对象包含Task和Event两部分数据;
b. 这两个对象只读,不能更新字段(不能编辑),也不能删除
c. 这两个对象不支持单独查询,必须使用父查子的方式才能查询:示例如下:

128、【Send Notification when Task Created From Apex】:DmlOptions.EmailHeader Class | DMLOptions does not work | Task Email Notifications via APEX not working, could be a Bug in platform?

List<Task> myTasks = new List<Task>();
for(integer i = 0; i < 5; i ++){
    Task newTask = new Task();
    newTask.Subject='Automated Task';
    newTask.OwnerId = ;
    myTasks.add(newTask);
}
Database.DMLOptions notifyOption = new Database.DMLOptions();
notifyOption.EmailHeader.triggerUserEmail = true;

//we need to use the Database.insert() with the DMLOptions to insert the task instead of using the standard insert DML command
Database.insert(myTasks, notifyOption);

使用经验:如果套用上述方法创建Task后没收到Email,可能是Task的Statuis设定为:Complete了。因此,请务必确保Task在Open Activities非Activity History栏,这时该Owner才能收到通知。另外,Email中发件人视环境而定(Internal -> 代码上次修改人;Community -> Community Administration中的Email),Email Body包含Task标准字段信息:Subject,WhoId,Priority,Comments等标准字段信息,若Comments没值,不会显示。

127、【为Classic的Internal User启用Global Header for Communities】:官方Article | 博客
目的:make internal user to easily switch back and forth between internal and the external communities.

126、【RecentlyViewed对象】:

SELECT Id, Name FROM RecentlyViewed WHERE Type ='Account'

125、【JS文件下载】:JS弹出下载对话框以及实现常见文件类型的下载

124、【Custom URL Button for Community】:Creating Custom Button Code for Partner Communities & Salesforce Internal 
场景:需要在Community中应用URL自定义Button,并且URL不受环境影响 - 避免Hard Code。
方案1Sample:{! URLFOR($Site.Prefix + '/apex/FinancialPlanningForm?rid=' + Lead.Id) }
方案2Sample:{! URLFOR( $Label.DST_Community+"/FinancialPlanningForm?rid="+Lead.Id ) }
评价:方案1在Lightning当前页面打开,嵌套在当前页;方案2不管Console/Internal/Community都用指定都Community Domian打开。对于类似业务,更倾向使用方案2
其他资源Allow URLFOR() in Formula Fields to Support Consoles and Communities

123、【File Upload and Download Security】:Configure the setting 'File Upload and Download Security'

122、【根据记录ID获取Sobject Type的2种方法】:

Id recId = '00Q0l000005pF5A';
String sobj1 = String.valueOf(recId.getSObjectType());
String sobj2 = recId.getSObjectType().getDescribe().getName();

121、【Pass URL Parameters to remote action in apex】:
不能从Apex中标记Remote Action的方法里边获取URL参数,因为Remote Action是无状态的。
解决思路:从VFP的JS中获取参数并通过JS Remoting传参到该方法;

120、【Organization对象】:

SELECT Id, Name, Country, DefaultLocaleSidKey, TimeZoneSidKey, LanguageLocaleKey, 
       ReceivesInfoEmails, ReceivesAdminInfoEmails, 
       UiSkin, TrialExpirationDate, OrganizationType, NamespacePrefix, 
       InstanceName, IsSandbox, IsReadOnly, 
       MonthlyPageViewsUsed, MonthlyPageViewsEntitlement 
FROM Organization

119、【新建Org My Domain默认设置问题】:
201222更新:今天申请了Spring ’21 Pre-Release Org,想体验下SOQL FIELDS(ALL/Custom/Standard)功能,用SF默认分的username登入后尽管domain被预设了,但仍然可以自定义。

201110更新:时隔1年后到了20年11月10日,注册了个Developer版本的Org,发现My Domain不能Rename了,刚好使用了我注册时的username后缀。如:username为wilson@data-architecture.com, my domain变成了如下形式:

After Winter'21 release, My Domain is configured by Salesforce and it cannot be editable after deployment. - My Domain in Winter 21 - Developer Edition

问题:在19年11月22日时,公司新购买了一个Org,突然发现My Domain已经被初始化了。
思维定势:早前我们知道,一旦My Domain被设定后,就不能更改。
解决方案:不要担心,初始化的My Domain是可以被修改的,这是Salesforce为了推广Lightning的一种方式。

118、【App and Tab Describe Information】:示例

List<Schema.DescribeTabSetResult> dtsrList = Schema.describeTabs();
for(DescribeTabSetResult dtsr : dtsrList){
    String appLabel = dtsr.getLabel();
    System.debug('App label: ' + appLabel);
    
    if(appLabel == 'Sales'){
        List<Schema.DescribeTabResult> dtrList = dtsr.getTabs();
        for(Schema.DescribeTabResult dtr : dtrList){
            System.debug('Tab label: ' + dtr.getLabel());
            System.debug('Tab icon url: ' + dtr.getIconUrl());
            System.debug('Tab url: ' + dtr.getUrl());
        }
    }
}

117、【sf标准报表定时导出到第三方文件服务器】:

String rid = [SELECT Id, Developername FROM Report WHERE Developername = 'ContactExportReport' LIMIT 1].Id;
PageReference file = new PageReference('/'+rid+'?export=1&enc=UTF-8&xf=csv');
Http http = new Http();
            
HttpRequest request = new HttpRequest();
string sURL=label.File_Server_Domain+'/hornet/backup2?suffix=csv&module='+umoduleName;
request.setEndpoint(sURL);
request.setMethod('POST');
request.setHeader('Content-Type','application/csv');
if(!test.isrunningtest()) request.setBodyAsBlob(file.getcontent());

request.setTimeout(120000);
            
HttpResponse response = http.send(request);

Map<String, Object> result = (Map<String, Object>)JSON.deserializeUntyped(response.getBody());


116、【获取当前时间所属的星期简写】:Sun / Thu

datetime.now().format('E');

115、【Salesforce Stock Images】:Sample Image Link Formulas
該類資源可以不經login校驗,直接被使用:https://slds-deloitte-dev-ed.my.salesforce.com/img/samples/light_green.gif


114、【Dynamic Query -  String Join的用法】:join(iterableObj, separator)

List<String> fNames = new List<String>{'Name', 'Industry', 'Type'};
String objectName = 'Account';
String query = 'SELECT Id'+ (fNames.size() > 0 ? ',' + String.join(fNames,',') : '') +' FROM '+ objectName;
System.debug(query);

113、【关于修改User Email】:又见107
Partner User: 直接修改无需验证;
Internal User:需要新邮箱确认才能被修改成功 - 如果想让新Email绕过旧邮箱验证立即生效,可以同时勾上Generate new password and notify user immediately

112、【获取sandbox name / community站点URL】:

ConnectApi.CommunityPage cc = ConnectApi.Communities.getCommunities();
System.debug(cc.communities[0].siteUrl);

111、【禁用双重身份验证】:
第一次登录,输入用户名密码点登录就会要求输入验证码,可用以下方法解决:
方法1:设置IP安全范围0.0.0.0 - 255.255.255.255;
方法2:在Profile里面禁用以下开关:


110、【数值加千分符方法】:Decimal / Integer / Double / Long.format(); | Salesforce Apex各大数据类型Format工具类模板

109、【User上的Mobile User字段API】:
我们知道User表上有个Mobile User的checkbox,它的API Name不是UserPermissionsMobileUser而是UserPreferencesHideS1BrowserUI;
如果Mobile User为true,那么UserPreferencesHideS1BrowserUI查出的结果为false,值刚好想反;

108、【Date / DateTime Format公共方法】:

/*---------Demo--------*/
System.debug(String.valueOf(System.now().format('yyyy/MM/dd hh:mm')));

Date dt = Date.today().addDays(15);
Datetime dtDateTime = dt; // Implicit cast
System.debug(dtDateTime.format('yyyy/MM/dd'));


/*---------公共方法--------*/
public static String dateOrTimeFormat(String type, Object dateOrTime) {
    Datetime dt = (Datetime) dateOrTime;
    return 'Date'.equals(type) ? dt.format('yyyy/MM/dd') : dt.format('yyyy/MM/dd HH:mm');
}

/*---------应用--------*/
Test__c t = [SELECT Id, Date__c, DateTime__c FROM Test__c WHERE Id = 'a0F7F00000DdKfe'];
System.debug('before convert...');
System.debug(t.Date__c);
System.debug(t.DateTime__c);

System.debug('after convert...');
Datetime dtDateTime = t.Date__c;
System.debug(UtilityClass.dateOrTimeFormat('Date', dtDateTime));
System.debug(UtilityClass.dateOrTimeFormat('DateTime', t.DateTime__c));

效果预览:

107、【Password - 修改Email及重置密码】:又见113
修改邮箱:
Internal User: 确认邮件将发送到旧邮箱,确认后,新邮箱生效 - 如果想让新Email绕过旧邮箱验证立即生效,可以同时勾上Generate new password and notify user immediately
Partner Community License User: 直接修改,无确认修改邮件。

重置密码:
一旦用户或为用户重置密码后,需要通过邮箱链接重置密码 - 也可使用System.setPassword SOAP API。

106、【Dynamic Query - 获取父查子类型的子元素】:

String query = 'SELECT Id, Name, (SELECT Id, Name FROM Contacts) FROM Account LIMIT 2';
List<SObject> parents = Database.query(query);
for(SObject parent : parents) {
    for(SObject child : parent.getSObjects('Contacts')) {
        system.debug('Child=' + child);
    }
}

项目实战
Case1: 有子记录:


Case2: 无子记录:

105、【String.leftPad(length, strNo)用法】:Salesforce数字字符串移位 - Sample:00520

String countString = '520';
System.debug(countString.leftPad(5, '0'));

104、【如何使用Custom Label自定义标间 - 换行】:

<!-- make sure escape=”false”
Thanks & Regards, <br/>
sfdcsrini.blogspot.com Team <br/>
Testing by sfdcsrini.blogspot.com <br/>
 -->
<apex:outputText value="{!$Label.Test_With_Line_Breaks}" escape="false" />

103、【将Set转成List合并使用102 Demo】:

Set<String> codes = new Set<String>{'001', '002', '003'};
//String mergeSet = String.join(codes, ';');// 语法错误
String mergeStr = String.join(new List<String>(codes), ';');
//System.debug(mergeSet);
System.debug(mergeStr);


102、【将List转成按分隔符隔开的String】:join

List<String> sl = new List<String> {'a','b','c'};
system.debug(sl);
String s = String.join(sl, ';');
system.debug(s);



101、【将List集合转成Set集合】:

List<String> sLst = new List<String> {'1', '2', '1'};
System.debug(new Set<String> (sLst));

100、【Company Profile的Fiscal Year在SOQL中的应用】:前往

99、【Using Attachment to Storage Logs - Attachment的存与取】:
Case:在做数据集成(类似Data Backup那种)时,非增量的同步往往会超出callout限制,如异步body size限制12MB,同步6MB,这个时候同步到第三方由于数据不合规被skip掉的也不少,之前在没有确定竟然是全量同步的情况下,本身使用自定义对象来存储log,后面skip掉的那些log直接将长文本13w+个字符限制都给超了,无奈在同事的建议下放弃了加字段来分开存储的可能,直接上了Attachment来存,这个可能是Best Practice。
Sample
加入存储的exception log长下面这样:

15 Listing(s) Skipped - {"a060k000005XppaAAC":["Listing's [E&V ID] doesn't start with 'EVHK': 是的發生分散","Listing's [Description] invalid content length: For description at least 30 words and maximum 2500 words"],"a060k000005fAKjAAM":["Listing's [E&V ID] doesn't start with 'EVHK': 435325","Listing's [Property Type](Complexd) doesn't match the property type those set in Squarefoot: Apartment公寓, Stand-alone building 單橦式大廈, Estate 屋苑, Public / HOS flat 公共屋邨 /居屋, Village house 村屋, House / Villa 獨立屋 / 洋房 / 別墅, Car park 停車場, Boat遊艇 / 船.","Listing's [Description] is missing"],"a060k000005YYwoAAG":["Listing's [E&V ID] doesn't start with 'EVHK': sdfsfsf","Listing's [Property Type](Complexd) doesn't match the property type those set in Squarefoot: Apartment公寓, Stand-alone building 單橦式大廈, Estate 屋苑, Public / HOS flat 公共屋邨 /居屋, Village house 村屋, House / Villa 獨立屋 / 洋房 / 別墅, Car park 停車場, Boat遊艇 / 船.","Listing's [Description] is missing"],"a060k000005fC7cAAE":["Listing's [Property Type] is missing","Listing's [Description] is missing"],"a060k000005LopTAAS":["Listing's [E&V ID] doesn't start with 'EVHK': 33252","Listing's [Property Type] is missing","Listing's [Description] is missing"],"a060k000005fBtQAAU":["Listing's [E&V ID] doesn't start with 'EVHK': 34534","Listing's [Description] is missing"],"a060k000005MCLfAAO":["Listing's [Description] is missing"],"a060k000005fDTzAAM":["Listing's [E&V ID] doesn't start with 'EVHK': 324566","Listing's [Description] is missing"],"a060k000005eWhPAAU":["Listing's [E&V ID] doesn't start with 'EVHK': 23552352435235","Listing's [Property Type] is missing","Listing's [Description] invalid content length: For description at least 30 words and maximum 2500 words"],"a060k000005f9spAAA":["Listing's [E&V ID] doesn't start with 'EVHK': 1233321","Listing's [Description] is missing"],"a060k000005fBtaAAE":["Listing's [E&V ID] doesn't start with 'EVHK': 4333228","Listing's [Description] is missing"],"a060k000005dAThAAM":["Listing's [Property Type](Complexd) doesn't match the property type those set in Squarefoot: Apartment公寓, Stand-alone building 單橦式大廈, Estate 屋苑, Public / HOS flat 公共屋邨 /居屋, Village house 村屋, House / Villa 獨立屋 / 洋房 / 別墅, Car park 停車場, Boat遊艇 / 船.","Listing's [Description] invalid content length: For description at least 30 words and maximum 2500 words"],"a060k000005XpjEAAS":["Listing's [E&V ID] doesn't start with 'EVHK': ewetw","Listing's [Description] is missing"],"a060k000005fAQhAAM":["Listing's [E&V ID] doesn't start with 'EVHK': 23423","Listing's [Description] is missing"],"a060k000005cmyGAAQ":["Listing's [Property Type] is missing","Listing's [Description] is missing"]}

关于存:那么我们使用Attachment存只需要使用下面几个关键字段:Name/ParentId/Body(blob)。

List<Integration_Log__c> logs = [SELECT Id, Excepition__c  FROM Integration_Log__c WHERE Id = 'a080k00000DPLgG' LIMIT 1];
System.debug(logs[0].Excepition__c.length());
String content  = logs[0].Excepition__c;
Attachment att = new Attachment();
att.Parentid = 'a080k00000DPLgG';
att.Name = 'SfdcSyncListingsTosquarefoot partial success!';
att.Body = blob.valueOf(content);
insert att;

关于取:我们将Blob使用Blob.toString()转以下就好了。

List<Attachment> atts = [SELECT Id, ParentId, Name, ContentType, Body, Description FROM Attachment WHERE Id = '00P0k000002yNQm'];
Blob contentBlob = atts[0].Body;
String str = contentBlob.toString();
System.debug(str);

98、【获取Salesforce User默认图片技巧】:
domain + profilephoto/005/T
获取后如下:T(45 x 45)

97、【Apex Email编码问题处理】:
在使用Apex发Email的时候,常常会忽视编码,很明显,下图中文部分变形了:
------------------------------------------------------------------------------------------------

------------------------------------------------------------------------------------------------
解决方法mail.setCharset('UTF-8');
解决后review:
------------------------------------------------------------------------------------------------

------------------------------------------------------------------------------------------------

96、【Schedule a Job every 10min】:每天10分钟跑一次Schedule Job
当我们在按分制定周期性计划时,如下图:

会出现如下错误:
System.StringException: Seconds and minutes must be specified as integers: 0 0/10 * * * ? *
解决方案如下

/**********************周期性执行*********************/
ScheduledListingSyncToEVJob sc1 = new ScheduledListingSyncToEVJob();
String cronExp1 = '0 0 * * * ?'; 
String jobID1 = System.schedule('ScheduledListingSyncToEVJob0', cronExp1, sc1);

ScheduledListingSyncToEVJob sc2 = new ScheduledListingSyncToEVJob();
String cronExp2 = '0 10 * * * ?'; 
String jobID2 = System.schedule('ScheduledListingSyncToEVJob10', cronExp2, sc2);

ScheduledListingSyncToEVJob sc3 = new ScheduledListingSyncToEVJob();
String cronExp3 = '0 20 * * * ?'; 
String jobID3 = System.schedule('ScheduledListingSyncToEVJob20', cronExp3, sc3);

ScheduledListingSyncToEVJob sc4 = new ScheduledListingSyncToEVJob();
String cronExp4 = '0 30 * * * ?'; 
String jobID4 = System.schedule('ScheduledListingSyncToEVJob30', cronExp4, sc4);

ScheduledListingSyncToEVJob sc5 = new ScheduledListingSyncToEVJob();
String cronExp5 = '0 40 * * * ?'; 
String jobID5 = System.schedule('ScheduledListingSyncToEVJob40', cronExp5, sc5);

ScheduledListingSyncToEVJob sc6 = new ScheduledListingSyncToEVJob();
String cronExp6 = '0 50 * * * ?'; 
String jobID6 = System.schedule('ScheduledListingSyncToEVJob50', cronExp6, sc6);


95、【Remote Site Setting】:
如果站点是ip,且含端口号的,需要一并将端口号加上,不能仅仅添加ip。

94、【Email-to-Case那点坑】:
在做如下需求时:

email 2 case

TARGET:
do search in both contact and lead and populate the lead or contact value on case.

LOGIC:
1. email match sigle client -> relate to client
2. email match sigle lead   -> relate to lead
3. email match more than one lead or client or <both 1 lead + 1 contact> -> not relate
4. no match lead or contact -> no relate

RECORDTYPE:
case      -> Enquiry Case Record Type
contact   -> all<contact + personal contact>

Note:
SuppliedEmail on case will be the end client's email;
End clients will send email to the service email not the real email when using email to case;

Trigger Logic Design:
1. before insert

遇到了一个坑,当单个Lead和单个Contact的Email都相同时,根据上述逻辑3, 理应Account/Contact值不应被填充,发现神奇的被填充上了,当时检查了原有的所有apex code和workflow/flow/process builder,都没发现有这个逻辑,最后在同事Don的帮助下,最终解决了这个问题。

原因sf标准的Email-to-Case,如果有一个Contact Email匹配上了邮件发送人的Email,就会自动填充Account和Contact;如果有多个Contact都含有相同的Email,那么就不填充。所以上述逻辑中尽管单个Lead和单个Contact的Email都相同,但是由于Contact为单个的缘故,执行了标准功能的自动填充。

解决方案:在before insert trigger的else语句中清空Account和Contact即可。

93、【使用count()计数统计记录】:
我们在使用SOQL时,用的最多的是select count() from sObject,这种形式默认是根据记录Id来进行聚合统计,那假如我们使用count(field api name),我们就能够统计某字段值不重复的所有记录数了,如下是我们统计系统中Lead的Phone唯一的记录数的code snippets:

SELECT count(Phone) FROM Lead


92、【访问Metadata Coverage Report】:https://yourOrgUrl/mdcoverage/report.jsp


91、【Sobject之getPopulatedFieldsAsMap用法】:getPopulatedFieldsAsMap

User currentUser = [SELECT Id, Name, Email, Contact.Phone, Profile.Name FROM User WHERE Id = :UserInfo.getUserId() LIMIT 1];
Map<String, Object> tmap = new Map<String, Object>(currentUser.getPopulatedFieldsAsMap());
tmap.put('Oid', UserInfo.getOrganizationId());
List<Listing__c> lsts = [SELECT Id, Landlord__r.Email 
                         FROM Listing__c 
                         WHERE Is_Main_Record__c = TRUE AND 
                               IsDeleted__c = FALSE AND 
                               Sole_Agency__c = TRUE AND 
                               Agency_End_Date__c = TOMORROW AND 
                               Listing_Status__c = 'Active'  
                         ORDER BY Company_ID__c];
for(Listing__c lst : lsts) {
    Map<String, Object> aMap = lst.getPopulatedFieldsAsMap();
    System.debug(LoggingLevel.INFO, '*** aMap: ' + aMap);
    for(String rfield : aMap.keySet()) {
        if(rfield.endsWithIgnoreCase('__r') ) {
            SObject obj = (SObject)aMap.get(rfield);
            String email = (String)obj.get('Email');
            System.debug(LoggingLevel.INFO, '*** Email: ' + Email);
        }
    }

    System.debug(LoggingLevel.INFO, '*** lst: ' + lst);
    System.debug(LoggingLevel.INFO, '*** lst.Landlord__r.Email: ' + lst.Landlord__r.Email);

    /*
    System.SObjectException: SObject row was retrieved via SOQL without querying the requested field: Listing__c.Landlord__r
    String email = (String)lst.get('Landlord__r.Email');
    */
}

[System.SObjectException: SObject row was retrieved via SOQL without querying the requested field:]解决方案

90、【Admin Log in as Any Uses】:Admin




89、【为Campaign启用New按钮】:Admin | New Campaign

针对上图,只需要为该User启用Marketing User即可。


88、【系统管理员默认Setup Landing Page】:Admin
My Settings -> Advanced User Details -> Make Setup My Default Landing Page


87、【Personal Account相关问题总结】:
a、不能直接删除Contact,必须删除Account才能将Account和Contact一起删掉;

86、【在公式里面获取Org URL】:
如:需要在画面上用公式字段展示Email Template超链接:
"https://cs57.salesforce.com/00X28000000OJJH?setupid=CommunicationTemplatesEmail"
就需要获取"https://cs57.salesforce.com/",方法如下:

HYPERLINK(LEFT($Api.Partner_Server_URL_260, FIND( '/services', $Api.Partner_Server_URL_260))+ Email_Template_ID__c +'?setupid=CommunicationTemplatesEmail', Email_Template_Name__c )



85、【Semi-Joins sub selects with IN】:Semi-Joins with IN and Anti-Joins with NOT IN

SELECT Id, Name From School__c WHERE Network_Code__c IN (SELECT Name From School_Networks__c WHERE Location__c = 'a015D000003SVcz')

上述查询将会出现异常Exceptionsemi join sub selects can only query id fields, cannot use: 'name'
原因是:SOQL语法只支持The subquery can filter by ID (primary key) or reference (foreign key) fields.

84、【系统空指针相关测试】:

测试1:

测试2:

List<Account> accs;
for(Account acc : accs) {
    System.debug('check point1');
}
System.debug('check point2');


总结:测试1Line6虽然查出来结果为空,但是赋值给accs时,其实是一个空数组类型,这和空指针是截然不同的两个概念,而测试2Line1声明变量时未实例化,则默认为空。

83、【字段历史跟踪Field History Track父对象子查询格式】:
Example:比如我们想查询Account及相关的AccountHistory,格式如下:

SELECT Id, Name,Recordtype.Name, Account_Status__c, (SELECT Field, OldValue, NewValue, CreatedById, CreatedDate FROM Account.Histories WHERE Field = 'RecordType') FROM Account


82、【Process Builder创建定时任务】:

81、【启用多币种Multi-Currency和启用高级货币管理】:
17年在实施Panasonic项目时,启用多币种需要联系客服,那么到今年18年,在准备Sales Cloud Consultant Exam时,发现现在可以自助启用。
那么首先需要在Company Information里面启用多货币,之后你所有相关记录都会默认成组织默认币种,当然你也可以维护静态货币转化率,随后你可以直接启用高级货币管理来维护Dated Currency Rate。

80、【如何控制Product共享规则?】:
我们查看Sharing Settings,会发现产品相关的对象中只能设置Price Book的记录共享,包含(No Access/Use/View Only),那么产品绑定在Price Book上就能实现RLS了 - 该方法适用于Classic,对于LEX,Price Book画面的Sharing按钮被移除了。

79、【初识Opportunity Stage - 如何为Opportunity Stage赋初始值?】:
Stage是一个比较特殊的字段,即:
记录类型里面没有Stage字段,另外创建记录类型前需先创建Sales Processes

那么要实现赋初始值不能使用Tirgger(before insert),而需要通过覆盖New按钮(在自定义page里面执行action方法跳链接)或者自定义Page实现:

public class StagePickListController {
    private static String DEFAULT_STAGENAME = 'S0 - Pre-Opportunity';
    private final Opportunity opp;
    public List<SelectOption> stages {get; set;}

    public StagePickListController(ApexPages.StandardController stdController) {
        this.opp = (Opportunity)stdController.getRecord();
        initFields();
    }

    private void initFields(){
        this.opp.StageName = DEFAULT_STAGENAME;
        initStages();
    }

    private void initStages(){
        stages = new List<SelectOption>();
        // it is better to not hard-code picklist values
        stages.add(new SelectOption('S0 - Pre-Opportunity','S0 - Pre-Opportunity'));
        stages.add(new SelectOption('Qualified','Qualified'));
        stages.add(new SelectOption('Closed Lost','Closed Lost'));
    }
}
<apex:page standardcontroller="Opportunity" extensions="stagePickListController" tabstyle="Opportunity">
<apex:form id="Form">   
    <apex:sectionHeader title="Opportunity Edit" subtitle="{!if(Opportunity.Id==null,'New Pre-Opportunity',Opportunity.Name)}"/>  
    <apex:pageBlock mode="edit" id="oppPB" title="Opportunity Edit">
        <apex:pageBlockButtons >
            <apex:commandButton action="{!save}" value="Save"/>
            <apex:commandButton action="{!cancel}" value="Cancel"/>
        </apex:pageBlockButtons>
        <apex:pagemessages/>
        <apex:pageBlockSection id="OpportunityInformation" title="Pre-Opportunity Information">
            <apex:inputField value="{!Opportunity.OwnerId}"/>
            <apex:inputField value="{!Opportunity.AccountId}" required="true"/>
            <apex:inputField value="{!Opportunity.CloseDate}" required="true"/>
            <apex:selectList value="{!Opportunity.StageName}" size="4" multiselect="false">
                <apex:selectOptions value="{!stages}"/>
            </apex:selectList>
            <apex:inputField value="{!Opportunity.Name}" required="true"/>
        </apex:pageBlockSection>
    </apex:pageBlock>    
</apex:form>


78、【Data Loader导入时间问题解决方案】:

77、【使用getValue()处理System.SelectOption数据结构】:

(System.SelectOption[value="00X0I000001qK51UAE", label="Bonus 10% CN", disabled="false"], 
System.SelectOption[value="00X0I000001qK4mUAE", label="Bonus 10% EN", disabled="false"], 
System.SelectOption[value="00X0I000001qK56UAE", label="Bonus 20% CN", disabled="false"], 
System.SelectOption[value="00X0I000001qK4rUAE", label="Bonus 20% EN", disabled="false"], 
System.SelectOption[value="00X0I0000019TWnUAM", label="PDF Bonus Policy for IB IC CN", disabled="false"], 
System.SelectOption[value="00X0I0000019TWsUAM", label="PDF Bonus Policy for IB Imperial CN", disabled="false"], 
System.SelectOption[value="00X0I0000019TWxUAM", label="PDF Bonus Policy for IB Welcome CN", disabled="false"], 
System.SelectOption[value="00X0I0000019TX2UAM", label="PDF Bonus Policy for Loyalty CN", disabled="false"], 
System.SelectOption[value="00X0I0000019TX7UAM", label="PDF Multibank Advantages CN", disabled="false"], 
System.SelectOption[value="00X0I0000019TXCUA2", label="PDF Relatives and friends bonus", disabled="false"])
emailTemplate = emailTemplateOptions[0].getValue();

76、【List<Sobject>的set和get & Object的get】:Sobejct可以做DML操作,Object不行

List<Sobject> originalParents = Database.query(queryStr);
System.debug('originalParents: ' + originalParents);

for(Sobject obj : originalParents) {
    for(String s : allWritableFields) {
        if(!exceptionSet.contains(s)) {
            if(obj.get(s) != parentId2ChildMap.get(obj.id).get(s)) 
                obj.put(s, parentId2ChildMap.get(obj.id).get(s));

            if('version_no__c'.equals(s))
                obj.put(s, String.valueOf(Integer.valueOf(obj.get(s)) + 1));// 父级版本号+1
        }
    }
}


75、【Dynamic SOQL特别注意事项】:
a、Dynamic SOQL with an IN Clause and map.KeySet()

MAP<ID,Opportunity> os = new MAP<Id,Opportunity>([Select ID from Opportunity]);
SET<ID> keys = os.keyset();// 此处必须转一下,不能直接在soql里面使用
String s = 'SELECT  RecordTypeId, COUNT(Id) os FROM Opportunity Where Id IN ';
s+=':keys GROUP BY RecordTypeId';
AggregateResult[] ars = Database.query(s);
System.debug(ars[0].get('RecordTypeId'));

b、实战Sample1

Set<Id> recordIdSet = new Set<Id>();
switch on sobjectType {
	when 'lead'    {recordIdSet = leadIdSet;}
	when 'contact' {recordIdSet = contIdSet;}
}

String queryStr = 'select id, email from ' + sobjectType + ' where id in :recordIdSet';
List<Sobject> sobjList = Database.query(queryStr);

c、实战Sample2

Boolean isConfig = 'config__c'.equals(sobj.toLowerCase());
String queryStr = (isConfig ? 'SELECT Value__c n' : 'SELECT Name n') + ' FROM ' + sobj + ' WHERE Id = \'' + recId + '\' GROUP BY ' + (isConfig ? 'Value__c' : 'Name');

// Note: 部分時候會出現使用'Id = '\' + recId + '\'過濾未生效,需要變更成:'Id = :recId';

NOTEdynamic SOQL cannot use bind variable fields in the query string. It will yield 'variable does not exist' error. However, it can be used in regular assignment statements.

74、【Organization-Wide Email Addresses】:
在Email Service使用该ID时,sandbox和production环境的ID是一样的。

73、【记录去重最佳实践】:
创建一个Unique字段,使用Workflow以特定维度更新该值,如:待售的房子不能重复,那么如果改房子所在的小区+单元+地理位置+房号就能断定是否为重复记录。

72、【判断字符串是否以特定值结尾】:

field.toLowerCase().endsWith('id');


71、【判断字符串是否为Salesforce ID】:Salesforce sObject Id Validation in Apex

public static Boolean isValid(String stringValue, Schema.SObjectType sObjectType) {
    Id sObjectId;
    if(isId(stringValue)) sObjectId = (Id)stringValue;
    return isValid(sObjectId, sObjectType);
}
 
public static Boolean isValid(Id sObjectId, Schema.SObjectType sObjectType) {
    return !(sObjectId==null || sObjectId.getSObjectType()!=sObjectType);
}
 
public static Boolean isId(String stringValue) {
    return stringValue InstanceOf Id;
}



70、【在SOQL中使用toLabel限制】:


69、【在SOQL中使用Alias】:Using Aliases with GROUP BY | field Field_Name __c cannot be grouped in a Query call

public static String getConvertedNameById(Id recId) {
	String sobj = String.valueOf(recId.getsobjecttype());
	Boolean isConfig = 'config__c'.equals(sobj.toLowerCase());

	String queryStr = (isConfig ? 'SELECT Value__c n' : 'SELECT Name n') + ' FROM ' + sobj + ' WHERE Id = \'' + recId + '\' GROUP BY ' + (isConfig ? 'Value__c' : 'Name');
	System.debug('queryStr: ' + queryStr);
	return String.valueOf(Database.query(queryStr)[0].get('n'));
}

优化:

// 根据Id获取相关Name值 - 用于字段历史跟踪转义
// 注意:由于Listing Name为自动编号,所以不可Group
public static String getConvertedNameById(Id recId) {
    String sobj = String.valueOf(recId.getsobjecttype());
    Boolean isConfig = 'config__c'.equals(sobj.toLowerCase());
    Boolean isListing = 'listing__c'.equals(sobj.toLowerCase());

    String queryStr = (isConfig ? 'SELECT Value__c n' : (isListing ? 'SELECT Name_as__c n' : 'SELECT Name n')) + ' FROM ' + sobj + ' WHERE Id = \'' + recId + '\' GROUP BY ' + (isConfig ? 'Value__c' : (isListing ? 'Name_as__c' : 'Name'));
    System.debug('queryStr: ' + queryStr);
    return String.valueOf(Database.query(queryStr)[0].get('n'));
}

是否可Group查询参考:

Schema.DescribeFieldResult grp = CustomObject__c.CustomField__c.getDescribe();
system.debug('Can CustomField__c be groupable?'+grp.groupable);

68、【将字符串转化成ID原始数据类型】:

Id myId = Id.valueOf('001xa000003DIlo');

67、【History对象字段 - ParentId】:
我们知道标准对象和自定义对象都有相应的History对象用于存储字段历史跟踪,那么在做动态查询时是有差异的。
Standard Object:为SobjectId;
Custom Object:  为ParentId;

String suffix = 'History';

Id recId = (Id)parmmap.get('recordid');
String sobj = String.valueOf(recId.getsobjecttype());//根据记录id获取对象api name
Boolean isCustomObj = sobj.contains('__c');

System.debug('recId\'s sobject type is: ' + sobj);

String sobjectHistory = (isCustomObj ? sobj.left(sobj.length() - 1) : sobj) + suffix;
String parentType = isCustomObj ? 'ParentId' : (sobj + 'Id');

System.debug('sobjectHistory: ' + sobjectHistory);
System.debug('parentType: ' + parentType);

List<sobject> histories = Database.query('SELECT Id, IsDeleted, ' + parentType + ', CreatedById, CreatedDate, Field, OldValue, NewValue FROM ' + sobjectHistory + ' WHERE ' + parentType + ' = \'' + recId + '\' ORDER BY CreatedDate');

System.debug('histories: ' + histories);


66、【在Lead上显示Convert按钮】:
当我们使用Lead时,明明将Convert按钮添加到了页面布局,但是用户一直看不到转化按钮,这时需要在Profile里面启用Covert Leads,如下图:

65、【如何让<apex:commandButton>仅仅执行js客户端事件,不执行服务端方法?】:eg. οnclick="jsFunction();return false;"

<apex:commandButton value="New" onclick="navigateToUrl('/setup/ui/recordtypeselect.jsp?ent=Lead&retURL=%2F00Q%2Fo&save_new_url=%2F00Q%2Fe%3FretURL%3D%252F00Q%252Fo',null,'new');return false;"/>


64、【几种数据导入工具比较】:
Data Loader:使用API Name映射,仅支持CSV文件,External ID类型字段才能关系匹配;
Import Wizard:使用Label匹配,仅支持CSV文件,空控制是否触发workflow和process builder;
Salesforce Inspector:使用API Name映射,支持Excel/CSV文件,支持External ID匹配关系触发Trigger等自动化流程。
结论
使用
210数据做测试,单独启用Process Builder和单独启用Trigger在插入或更新数据时,都生效。
注意:使用Import Wizard时,注意启用Trigger workflow and process builder才会更新记录。

63、【自定义设置Custom Settings可以像Sobject一样被导入导出】:
a、自定义设置能在Developer Console中被soql查询,也能直接做dml操作;
b、自定义设置能通过Salesforce Inspector导入和导出数据;
c、自定义设置能使用Data Loader处理记录;
d、自定义设置能使用Import Wizard导入记录;

62、【在Visualforce中使用Hierarchy Type Custom Setting】:HIERARCHY CUSTOM SETTINGS

{!$Setup.CustomSettingName__c.CustomFieldName__c}

61、【Inspecting Code Coverage】:
Salesforce提供了两种查询方式检索Unit Test覆盖情况:ApexCodeCoverageAggregate/ApexCodeCoverage,使用该对象查询需要启用Use Tooling API,查询模板如下:

SELECT ApexClassOrTrigger.Name, NumLinesCovered, NumLinesUncovered 
FROM ApexCodeCoverageAggregate 
WHERE ApexClassOrTrigger.Name = 'TaskUtil'
SELECT ApexTestClass.Name,TestMethodName,NumLinesCovered,NumLinesUncovered 
FROM ApexCodeCoverage 
WHERE ApexClassOrTrigger.Name = 'TaskUtil'

【Sample】:


60、【格林威治时间GMT问题】:
最近在做CIL(Custom Interaction Log)时发现如下问题:将System.Now()/DateTime.Now()赋值给<apex:inputField type="Text"/>和<apex:inputField type="DateTime"/>时,时间不一致,相差8h

Solution: 使用format()转一下时间后,再赋值给Text文本即可解决。
Exp: 'Last save at ' + DateTime.now().format();
总结:用纯文本接收时间就会按照格林威治时间显示,需要使用format()转化,才会显示Local时间


59、【为Salesforce启用多币种Multi-Currency功能】Enable Multi-Currency using the Legacy method for my organization
a、对于DEV Edition可以直接在Company Information中启用,对于sandbox和production需要提交一个Case给Salesforce客户支持,启用过程会花费3个工作日
b、启用后,你在Quick Find里面就能看到Manage Currencies功能,在这你可以新增多个货币,默认的为你启用前Company Information里边的币种。


58、Log日志最大容量为250MB,删除后才能继续打印日志文件。


57、【Custom Settings】:如下自定义设置API Name为ScheduleDate__c,则我们可以使用obj.getAll()方法获取key为Name字段,value为ScheduleDate__c类型的map,注意Sobject无getAll()方法。Custom Settings Methods


56、当启用个人客户后,创建一条account就copy了份account给contact,其中不管两边哪一边有字段值变动,两边都会同步。Exp:contact有字段isActive,如果更新这个字段为true,那么account记录上的该字段跟着变为true。
启发:更新Account可能会触发Contact的Trigger,反之亦然。


55、【使用Trigger.oldMap()过滤进入update/delete trigger的条件】:Trigger Context Variables | Salesforce检查某字段前后是否被更改的方法 - Trigger & Validation Sample
在使用Trigger.New和Trigger.Old相关方法时,最好强转下类型:
Trigger.New      => (List<Sobject>)Trigger.New
Trigger.Old       => (List<Sobject>)Trigger.Old
Trigger.oldMap => (Map<Id, Sobject>)Trigger.oldMap
...
否在将会出现下图错误:


54、15位Id和18位Id能直接判断,不需要使用String.left(15)来截取出相同的长度来比较了,测试如下:


53、【为Custom Setting启用List Type】:Setup -> Schema Settings


52、【在Apex中使用合并统计查询-AggregateResult】官方文档 | blog 参考


51、【如何在Js/Formula中正确使用Opportunity的HasOpportunityLineItem字段】:在apex中该字段值为Boolean类型

var hasLine = '{!Opportunity.HasOpportunityLineItem}';

alert('{!Opportunity.HasOpportunityLineItem}');0
alert(hasLine);0
alert('hasLine == 0: ' + hasLine == 0);false
alert('hasLine == 1: ' + hasLine == 1);false
alert('hasLine === 0: ' + hasLine === 0);false
alert('hasLine === 1: ' + hasLine === 1);false
alert(hasLine == false);true

50、【获取Label值】

{!$ObjectType.ACC_Return_Order_Product__c.LabelPlural}
{!$ObjectType.ACC_Return_Order_Product__c.Fields.ACC_Product__c.Label}

49、【使用soql查询记录条数】
select count() from DTT__Geographic_Region__c

48、使用Salesforce Inspector导出数据并导入到新环境 - 若存在junction对象的数据导入,则需要mapping关系字段,查询时可通过关系查询查Name。
Example】:导入PriceBookEntry数据到UAT - 先导入PriceBook和PriceBookEntry中需要的Product,后导入PriceBookEntry,之后copy csv数据时,该数据的关系字段查询不要使用Id(Product2Id,Pricebook2Id),而选择查对应的Name,后面导入时,再针对Name做Mapping。
【Note】:导入非标准价格手册的PriceBookEntry前,需要先导入标准的PriceBookEntry,只需Care5个字段:CurrencyIsoCode、Product2Id、Pricebook2Id、UnitPrice、isActive。
Addition】:用inspector导地理区域(自己lookup自己)时,ExternalId是关系的核心,可先将省份单独导入后,再导入其他的数据,如果后续导入报错,再拆开单独导入可以避免意料之外的错误。不要忘录关系字段值


48、使用Mavensmate同步DEV与UAT环境中简档的字段级别权限:在环境迁移时,部分元数据会存在丢失,导致两环境存在差异,比如简档中档FLS。
Step by Step】:先登陆到DEV环境,利用Mavensmate retrieve Profile和Custom Object(标准对象也在里面),然后勾选某对象如Order的fields和简档明细,点击Update后,在Sublime中打开文件;使用同样档方法将UAT相关文件下载到本地,然后直接复制粘贴简档文件档元数据覆盖UAT即可。

47、如何配置实现超级管理员下次登录salesforce org不需要输入验证码:Profile -> System Administrator -> Login IP Ranges,设置IP Start Address:0.0.0.0,IP End Address:255.255.255.255即可。

46、在apex中启用记录锁来锁定或解锁记录

select count() from DTT__Geographic_Region__c
select count() from DTT__Geographic_Region__c
select count() from DTT__Geographic_Region__c
select count() from DTT__Geographic_Region__c
select count() from DTT__Geographic_Region__c

45、问题:System Administrator的Profile中Home的Tab Settings显示Default On,在Tab面板和Tab的customize列表中并没有看到Home?- 先禁用profile的enhanced profile user interface,之后在profile的Tab settings中勾上下图1,保存即可。
参见资料:https://help.salesforce.com/articleView?id=Home-Page-Tab-is-Missing-for-Users&language=en_US&type=1

44、启用或禁用内联编辑 - 有时候我们希望禁用用户默认的行内编辑功能,直接去user interface中将Enable Inline Editing关掉即可。

43、去重Sample - 看似怪诞,实则大有用处:

Map<String, String> accIDs = new Map<String,String>();
for(Quote qt: (List<Quote>)Trigger.New){
    if(!accIDs.containsKey(qt.Quote_To_Account__c))
        accIDs.put(qt.Quote_To_Account__c,'');
}
Map<String, Account> accs = new Map<String, Account>([SELECT id,BillingStreet,BillingCity, BillingState, BillingPostalCode, BillingCountry FROM Account WHERE Id IN : accIDs.keySet()]);

42、System.debug('result: ' + rtMap.get(myRt).equals(qt.RecordTypeId) + '--qt.RecordTypeId: ' + qt.RecordTypeId + '--rtMap.get(myRt): ' + rtMap.get(myRt));Id为18位。

41、Opportunity和Quote为Master-Detail关系,在导入历史数据时,Opportunity和Quote的Owner未同步,事后同步时,不可使用apex方法更新Quote Owner,错误信息为:"Can't change quote owner. Enable quotes without parent opportunities."。

40、如果记录锁定,用户需要更新记录,则必须使用Webservice,比如:报价锁定非系统管理员需自动同步报价时,我们需要在Trigger里面调用Webservice,这时就需要使用到@future异步方式了。

39、Lead Home Page调整经验之谈

a、没有配置方法实现Home页面布局调整,比如将Tools和Reports换个位置;

b、无法用自定义报表链接替换Reports里面的标准报表链接-无法编辑标准报表,无法将自定义报表移至Lead Reports Folder;

c、如果必须实现只能重写标准Lead Tab按钮;

38、Salesforce布局特性:如果相关列表上无任何按钮,那么相关列表面板只有在有记录时才会显示

37、Salesforce获取某个日期所在月份的天数

Date d = System.today();
Integer numberDays = date.daysInMonth(d.Year(), d.Month());
System.debug(numberDays);

36、在为输入元素赋初始值时,我们经常会使用url将参数写入该输入元素的Name属性,我们通常会担心,在Org迁移时,这些Name值是不是会改变,从而增加我们的维护负担?经调研,Vf画面的Name不会随着Org的改变而改变

35、使用apex获取IP地址ApexPages.currentPage().getHeaders().get('X-Salesforce-SIP');

34、封装Map<String, List<Sobject>>技巧 + 去重 | 参考资源:How to Send More than 10 E-mails

Map<Id, List<Case>> userCaseMap = new Map<Id, List<Case>>();
List<Case> allCaseLoggedToday = new List<Case>();
List<Id> salesIds = new List<Id>();
List<User> salesRep = [SELECT Id , Name , Email , ManagerId 
                       FROM User 
                       WHERE Profile.Name = 'System Administrator'];
for(User u : salesRep) {
  salesIds.add(u.Id);  
}
allCaseLoggedToday = [SELECT Id, CaseNumber,CreatedById, Owner.Name , Account.Name , Contact.Name  
                      FROM Case 
                      WHERE CreatedDate = TODAY AND CreatedById in : salesIds];
for(Case c : allCaseLoggedToday) {
  if(userCaseMap.containsKey(c.CreatedById)) {
    //Fetch the list of case and add the new case in it
    List<Case> tempList = userCaseMap.get(c.CreatedById);
    tempList.add(c);
    //Putting the refreshed case list in map
    userCaseMap.put(c.CreatedById , tempList);
  }else {
    //Creating a list of case and outting it in map
    userCaseMap.put(c.CreatedById , new List<Case>{c});
  }
}

33、salesforce soql中不能对公式字段使用Group By;

32、Partner User对Quote Tab不可见,Quote无Sharing Rule,它的共享受Opportunity的控制,如果对Partner User的Quote的OLS设置为View且Opportunity页面布局的Quote放出来了,如果共享Opp的Owner没有创建Quote记录,Partner User不可见Quote相关列表,需要创建一条Quote,才可以看见。

31、Apex中List、Map、Set集合总结:
List:有序、可重复;
Map:无序,key重复则value覆盖;
Set:无序,不可重复;即使重复了,取前面的值,如:

Set<Integer> s = new Set<Integer> {1,2,3,2,1};
system.debug('s: ' + s);

DEBUG INFO:s: {1,2,3}
Sample:

List<Account> accs = [select id, name from account limit 3];
map<Id, Account> m = new map<Id, Account>();
String lastId = '';
if(accs != null && !accs.isEmpty()) {
    Integer i = 0;
    for(Account a : accs) {
        System.debug('a['+i+'] : ' + a);
        lastId = a.Id;
        m.put(a.Id, a);
        i++;
    }
}
// 验证List有顺序,Map无顺序
System.debug(m);
System.debug(m.get(lastId));
Map<String, String> m1 = new Map<String, String> {
    'key1' => 'value1',
    'key2' => 'value2',
    'key1' => 'value2',
    'key2' => 'value3',  
    'key1' => 'value3'
};
System.debug('m1: ' +m1);
System.debug('m1.key1: ' + m1.get('key1'));

30、创建Task:

任务:
Task task = new Task();
task.Subject = ‘A信息修改申请';
task.status = 'open';
task.priority = 'High';
task.whatId = '0017F00000CfcdsQAB';// 相关项目,这里是供应商Id
task.ownerId = '0057F000000pe6uQAA';// 被分配人,这里是UserId
// 以下两行是设置提醒
task.IsReminderSet = true;
task.ReminderDateTime = System.now();
insert task;

29、根据自定义地理定位数据类型字段API名表示经纬度

现有API名称为:Geographic_Coordinates__c的地理位置坐标字段,要表示经度使用:Geographic_Coordinates__Longitude__s, 要表示纬度使用:Geographic_Coordinates__Latitude__s

Select Id, Address__c, Geographic_Coordinates__Longitude__s, Geographic_Coordinates__Latitude__s From Store__c

28、Live Agent配置:step by step resource

1、先启用live agent - setup -> live agent settings -> enable

2My Settings -> Advanced settings -> Live Agent User(勾选上)

27、使用Database.query()查询数据集合:

filterStr = 'AND Type__c = \'Text\' ';// 注意写成转义字符形式
query = 'SELECT Name, Type__c, Message__c, Msgpic__c, Mediaid__c, VioceRecognition__c, VoiceFormat__c, VoiceSrc__c ' + 
        'FROM BD_CaseDetail__c ' + 
        'WHERE Case__c = :caseId ' + filterStr + 'ORDER BY CreatedDate ASC LIMIT 500';
caseDetailList = Database.query(query);
caseDetailList = [SELECT Name, Type__c, Message__c, Msgpic__c, Mediaid__c, VioceRecognition__c, VoiceFormat__c, VoiceSrc__c
                  FROM BD_CaseDetail__c
                  WHERE Case__c = :caseId AND Type__c = 'Text' ORDER BY CreatedDate ASC LIMIT 500];

26、Apex实例化一个内部类:

public class OuterClass {
    public class InnerClass {
        String name = '';
        Blob body = '';
    }
}
// 实例化内部类
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = new OuterClass.InnerClass();
/*接口类实例化*/
global class SyncInterface {    
    // 封装数据结构 - 返回结果  
    global class SyncResults{  
        global List<Response> responses;  
    }  
      
    global class Response{  
        public String TYPE;  
        public String MSG;  
        public String BUSINESS_ID;  
        public String SALESFORCE_ID;  
        public String PROC_TYPE;              
    }  
}

// 实例化接口response list
SyncResults syncResponseList = new SyncResults();  
syncResponseList.responses = new List<Response>();

25、限制点击自定义按钮的简档:

/**
    功能说明:判别用户是否为项目比选相关专员
    参数说明:用户Id
    返回值:true/false
    作者:Wilson Xu
    日期:2017-07-26
    **/   
    public static Boolean isValidUser(String userId){
        Set<String> profileSet = new Set<String>{'品牌专员','事件行销专员','物料制作专员','线上媒介专员','线下媒介专员','展览展示专员','子公司企划专员'};
		String profileName = [SELECT Profile.Name FROM User WHERE Id = :userId].Profile.Name;        
        return profileSet.contains(profileName);
    }
// 验证该用户是否为专员简档用户
    	if(!isValidUser(UserInfo.getUserId())){
            result.code = '1';
            result.msg = '只有相关专员才能发起定向谈判!'; 
            return JSON.serialize(result);
        }

24、使用apex处理日期:

a. DateTime in Apex

DateTime nowTime = System.now();
String now_date = nowTime.format('yyyy-MM-dd');
String now_dateTime = nowTime.format('yyyy-MM-dd HH:mm:ss');
System.debug(now_date+'<------>'+now_dateTime);

debug信息:

14:23:00:002 USER_DEBUG [4]|DEBUG|2017-07-26<------>2017-07-26 14:23:00

b. Date in Apex - String to Date | Date Class valueOf(stringDate)

String strDate1 = '2018-6-6';
String strDate2 = '2018-06-06';
String strDate3 = '2018/6/6';
Date myDate1 = date.valueOf(strDate1);
Date myDate2 = date.valueOf(strDate2);
//Date myDate3 = date.valueOf(strDate3);//System.TypeException: Invalid date: 2018/6/6
System.debug('myDate1: ' + myDate1);
System.debug('myDate2: ' + myDate2);
//System.debug('myDate3: ' + myDate3);


23、DateTime.getTime()获取时间戳,单位为ms;
22、Decimal.valueOf(ApexPages.currentPage().getParameters().get('st')),将字符串形式转换成Decimal格式
21、ApexPages.currentPage().getUrl().contains('st'),apex查看当前页面url是否包含某个字符串
20、图解批准进程approval process,理解队列,多级审批:http://blog.csdn.net/itsme_web/article/details/73250952
19、pageBlockTable中的column组件设置宽度不生效的解决方案:
a、嵌套关系1: form>pageBlock>pageBlockTable>column,可以直接为column增加width的style或者styleClass,需要的话pageBlockTable设置个width:100%;
b、嵌套关系2: form>pageBlock>pageBlockSection>pageBlockTable>column,这个时候添加style和styleClass是不生效的,此时只需要在pageBlockTable中设置columnsWidth="width1,width2...";

18、创建visualforce的4种方法:
a、导航法 - setup->developer->visualforce page;
b、developer console;
c、IDE - eg. sublime text / eclipes - 安装eclipes并配置forceIDE插件视频 - https://www.youtube.com/watch?v=ufe62nGecMg
d、打开developer mode(两种方式:1. 在User里面-Edit-Development Mode;2. 在My Setting-Personal-Advanced User Details-Edit-Development Mode)后,通过url方式创建

17、apex:page组件与apex:sectionHeader组件属性的相互影响:
apex:page组件中比较少用到的属性:

<!--
setup="true";show sidebar as setup format;
renderAs="pdf";the body you development will show as pdf;
action="someMethods";excute the action first before render the page;
-->
<apex:sectionHeader title="Section Header Practice" subtitle="Home" description="draw your page using code" help="https://baidu.com" printUrl="https://baidu.com"/>
<span style="color:#ff0000">如果page中用到了setup="true"属性,那么sectionHeader中就不能显示对象图标。</span>

16、在Js中两种方法获取组件id
a、{!$Component.idName} - eg.  var el1 = document.getElementById('{!$Component.name}').value;// 该方法不需要写嵌套的id,若要指明则:$Component.bk.age
b、pg:fm:bk:name - eg. var el2 = document.getElementById('pg:fm:bk:name').value;// 该方法每个组件均需要指明id,并按嵌套关系依次书写,id间以:隔开
当然我们也希望在css中应用id选择器,我们可以:
<style>[id*=ip2]: color: f00;</style>

<apex:page id="pg">
    <apex:form id="fm">
        <apex:inputText id="name" onchange="show()"/>
        <script>
            function show() {
                var el1 = document.getElementById('{!$Component.name}').value;
                var el2 = document.getElementById('{!$Component.bk.age}').value;
                alert('name: '+el1+', age: '+el2);
            }
            function demo() {
                // getting specific dom element by this format, you shold write every level id.
                var el1 = document.getElementById('pg:fm:name').value;
                var el2 = document.getElementById('pg:fm:bk:age').value;
                alert('name: '+el1+', age: '+el2);
            }
        </script>
        <apex:pageBlock id="bk">
            <apex:inputText id="age" onchange="demo()"/>
            <apex:pageBlockSection id="bks1">
                <apex:inputText id="ip1"/>
            </apex:pageBlockSection>
            <apex:pageBlockSection id="bks2">
                <apex:inputText id="ip2"/>
                <apex:outputText id="op"/>
            </apex:pageBlockSection>
            <apex:pageBlockButtons >
                <apex:commandButton value="populateAutomaticly" onclick="al()" oncomplete="populateAutomaticly()"/>
            </apex:pageBlockButtons>
            <script>
                function populateAutomaticly() {
                    var el = document.getElementById('{!$Component.bks1.ip1}').value;
                    document.getElementById('pg:fm:bk:bks2:ip2').value = el;
                    document.getElementById('pg:fm:bk:bks2:op').innerHTML = el;
                }
                function al() {
                    alert('Testing...');
                }
            </script>
        </apex:pageBlock>
    </apex:form>
</apex:page>

15、15位Id与18位Id的区别?

C:Q:: what is the difference 18 digit id and 15digit?

Ans: When we create record in the object salesforce will create 18 digit unique to recognize it.

This is a case insensitive

Same 18 digit is represented as 15 digit id as case sensitive

Id 001—9000001—1o2Of in this 15/18 digit id first 3digits indicate Object and last 5 digits indicate record

===========================排序策略调整<最近的在最前面>==========================

1、使用SOQL语句查询时,字符串类型的只能使用‘单引号’,否则报错:Unknown error parsing query;

eg:SELECT Id, Subject, CaseNumber, Status, Priority 

        FROM Case 

        WHERE CaseNumber = '00001036' //注意此处autoNumber类型数字为String类型

使用SOQL其他注意事项:

a、尽量使用Order By,保证查询结果的稳定性;
b、使用LIMIT,防止数据量超过50000而报错;
c、使用Offset偏移时,一定放在该查询语句的最后面,其限制及应用场景见:https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_offset.htm?search_text=offset

2、标准字段的API Name即为该标准字段的Field Name;

eg:Case标准对象的Subject API Name即为 Subject

3、计算两个日期之间相隔的天数:

eg:TODAY() - DATEVALUE( CreatedDate )

        Implementd__Date__c - DATEVALUE( CreatedDate )

注意:只有当日期为标准字段时,才使用DATEVALUE()来转化

4、在terminal中使用curl方式查看json数据(通常使用workbench>utilities>REST Explorer),通常会用到sessionId,获取方法如下:

String sessionID = UserInfo.getSessionId();
System.debug(sessionID);

5、完整版REST services demo

@RestResource(urlMapping='/Cases/*')
global with sharing class CaseManager {
    @HttpGet
    global static Case getCaseById() {
        RestRequest req = RestContext.request;
        String caseId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1);
        Case result = [SELECT CaseNumber, Subject, Status, Origin, Priority
                       FROM Case
                       WHERE Id = :caseId];
        return result;
    }
    /*
HttpGet步骤:
1、创建RestRequest类型的req对象(RestContext.request的返回值类型就是RestRequest)
2、通过req对象的requestURI属性利用字符串检索技术拿到caseId
3、创建Case对象result,并将通过caseId查到的记录赋值给该对象,注意“WHERE Id = :caseId”
4、返回Case对象
*/
    @HttpPost
    global static ID createCase(String subject, String status,
        String origin, String priority) {
        Case thisCase = new Case(
            Subject=subject,
            Status=status,
            Origin=origin,
            Priority=priority);
        insert thisCase;
        return thisCase.Id;
    }
    /*
HttpPost步骤:
1、声明并创建一个Case类型对象thisCase,并为该对象的标准字段赋值
2、将自定义对象插入到Case表中形成一条记录
3、返回一个新纪录的类型为ID的变量Id用于查找新纪录
*/
    @HttpDelete
    global static void deleteCase() {
        RestRequest req = RestContext.request;
        String caseId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1);
        Case thisCase = [SELECT Id FROM Case WHERE Id = :caseId];
        delete thisCase;
    }
    /*
思路:
要删除某一条记录首先要找到该记录,而方法可以是利用soql语言查找到某一记录的主码,这里是Id(使用rest服务请求获取到uri后从uri中取得的id)
HttpDelete步骤:
1、创建ResrRequest对象req
2、声明caseId,并将rest请求到的uri截取/后的值赋给该变量
3、利用soql语句查到Id = :caseId的那条记录
4、删除该记录
*/
    @HttpPut
    global static ID upsertCase(String id, String subject, String status, String origin, String priority) {
        Case thisCase = new Case(
        Id = id,
            Subject = subject,
            Status = status,
            Origin = origin,
            Priority = priority
        );
        upsert thisCase;
        return thisCase.Id;
    }
    /*
HttpPut步骤:
1、声明并创建一个Case类型对象thisCase,并为该对象定义标准字段赋值
2、将自定义对象插入到Case表中形成一条记录或者更新Id为id的记录
3、返回一个新纪录的类型为ID的变量Id用于查找新纪录
*/
    @HttpPatch
    global static ID updateCaseFields() {
        RestRequest req = RestContext.request;
        String caseId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1);
        Case thisCase = [SELECT Id FROM Case WHERE Id = :caseId];
        Map<String, Object> params = (Map<String, Object>)JSON.deserializeUntyped(req.requestBody.toString());
        for(String fieldName : params.keySet()) {
            thisCase.put(fieldName, params.get(fieldName));
        }
        update thisCase;
        return thisCase.Id;
    }
    /*
HttpPatch步骤:
1、创建RestRequest类型的req对象(RestContext.request的返回值类型就是RestRequest)
2、通过req对象的requestURI属性利用字符串检索技术拿到caseId
3、创建Case对象,并把按Id查到的Case表记录赋值给该对象
4、将请求获得的requestBody转化成字符串后,反序列化为对象强制转化为Map<String, Object>后赋值给Map变量params
5、遍历对象的key,并在通过id找到的Case对象thisCase中写入key-value
6、更新记录
7、返回记录的id
*/
}
/*
共性:
1、每个对象系统自带一个Id属性,它是系统自动分配的;
2、每一种Http方法均为global static
3、@HttpPut与@HttpPost的区别(upsert,insert)
*/

 

区别:put vs patch

the same:You can update records with the put or patch methods.

the difference: put can either create new resouce or update the existing resource; patch can update the existing resouce exclusively.

6、Apex中在使用类继承时需要使用到的关键字:extends,super,virtual,override.跟Java继承不同的是,超类必须使用virtual修饰,子类使用override和extends修饰,如果需要重写父类的方法,父类中该方法需要用virtual修饰,子类需要使用override。另外如果子类需要使用超类的域或者方法则需要使用super关键字,注意构造方法的复用不需要用成对的virtual和override关键字修饰超类的构造方法和子类的构造方法。

7、利用公式字段插入图片:IMAGE(path,img_title,height,width)

8、在report中使用The "Power of One" technique来统计不重复的数据

9、在Apex中使用静态资源加载jquery:

<apex:page>
    
    <!-- Add the static resource to page's <head> -->
    <apex:includeScript value="{! $Resource.jQuery }"/>
    
    <!-- A short bit of jQuery to test it's there -->
    <script type="text/javascript">
        jQuery.noConflict();
        jQuery(document).ready(function() {
            jQuery("#message").html("Hello from jQuery!");
        });
    </script>
    
    <!-- Where the jQuery message will appear -->
    <h1 id="message"></h1>
    
</apex:page>

10、简单的使用vf标准控制器来展示Contact列表,并实现点击名称跳转详细页面。方法有三种:

<apex:page standardController="Contact" recordSetVar="contacts">
	<apex:pageBlock title="Contact List">
		<apex:repeat value="{!contacts}" var="ct">
			<li>
				<apex:outputLink value="/{!ct.Id}">{!ct.Name}</apex:outputLink>
				<!-- <apex:outputLink value="{!URLFOR($Action.Contact.View,ct.id,[retURL=''])}">{!ct.Name}</apex:outputLink> -->
				<!-- <apex:outputLink value="https://componentstest-dev-ed.my.salesforce.com/{!ct.Id}">{!ct.Name}</apex:outputLink>  -->
			</li>
		</apex:repeat> 
	</apex:pageBlock>
</apex:page> 

11、增强lookup查找功能:我们通过lookup进行对父记录Name进行搜索时,通常默认只能使用Name来搜索,有时候比如我们在子记录中想要通过Phone来搜索Account的Name,这个时候可以在setup->enter 'Search Settings' in Quick Search Box中即可增强搜索

12、将使用html编辑的前端网站放到force.com平台上的方法:将做好的网站,比如shangpinhui/Bootstrap所有文件打包成zip上传到salesforce的Static Resources中,比如拿shangpinhui为例,目录结构为:shangpinhui->images/js/css/index.html,打包上传后命名为ShangpinhuiZip(名字不能一数字开头),之后在Visualforce Pages中编辑如下代码:

<apex:page docType="html-5.0" sidebar="false" showHeader="false" standardStylesheets="false" 
      action="{!URLFOR($Resource.ShangpinhuiZip, 'shangpinhui/index.html')}">
</apex:page>

在action里面通过URLFOR表达式来将页面加载进去。这样就不用考虑修改网站页面资源引用的路径了,注意在Developer Edition里面由于每个账号限制只允许放一个网站绑定一个url,所以要实现多个网站同时上传作为作品展示,可以再做一个列表,分别通过超链接映射到相应的网站上,这样就可以将您的所有作品都绑定在一个页面上分别访问。

13、在Company Information中可以查看User Licence的使用次数,如下图:

14、recordSetVar与<apex:variable value="{!}" var=""/>的适用场景比较:
recordSetVar保存的是SOBject的List,一般会与<apex:pageBlockTable>以及<apex:page>的熟悉standController(即可以传标准对象也可以传自定义对象)连用,常用于输出性质的组件,而对于输入性质的组件,若强行使用需要加[0],这种场景推荐使用<apex:variable>标签,来将比较长的api名称用变量存储。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页