关于example.com

每当刷完Sandbox,Salesforce为了防止从Sandbox发出的邮件使用户混肴,
所以将所有User的邮箱的@替换为=,然后加上@example.com
比如,User A的邮箱为[email protected],则变换为[email protected]

如果向这种邮箱发出邮件,邮件会发到哪去呢?

有同学说了,这个邮箱肯定不存在的,所以谁也收不到。
这可未必。如果了解SMTP服务器的话,就知道有种catch all账号,可以收到所有发往此邮件服务器的邮件,无论收件地址是否存在。
所以用公司邮箱乱发东西的同学们注意了,千万别用公司邮箱乱说话呦。

所以我们就来看看这个example.com
首先用浏览器访问,还真的能直接访问,白白的背景上,正中间方方正正的写着

Example Domain

This domain is established to be used for illustrative examples in documents. You may use this domain in examples without prior coordination or asking for permission.

More information…

大概意思是说,这个域名就是给你在文档里做说明示例用的。随便用,不用经过我同意。
呦!谁这么大方?

顺手Whois了一下这个example.com
注册商是RESERVED-Internet Assigned Numbers Authority

这个Internet Assigned Numbers Authority是哪路神仙?
原来是大名鼎鼎的IANA(互联网号码分配局)。
什么?没听过,那么ICANN总听过吧,
Internet Corporation for Assigned Names and Numbers,互联网名称与数字地址分配机构。
如果购买域名的话,每年都要向ICANN交管理费的。
这个IANA就是ICANN的下属机构。
这就是说,这个域名居然还是官方所有的。

随后,点了一下页面上的More information…
了解到了整个事情的始末。

原来根据RFC2606
一级域名
.test
.example
.invalid
.localhost
二级域名
example.com
example.net
example.org
都为特殊目的所保留。

RFC6761则规定了具体的做法。

// 小插曲,很Interesting的是,RFC2606的作者是IBM雇员 Donald E. Eastlake 3rd与Aliza R. Panitz, 而RFC6761的作者为Apple Inc.的雇员Stuart Cheshire与Apple Inc.的雇员Marc Krochmal

网页中明确的说明了Certain domains are set aside, and nominally registered to “IANA”, for specific policy or technical purposes.
甚至提供了IDN的保留域名。

就像好莱坞电影里以555区号开头的电话一样,互联网也保留了这种安全域名用来举例子。

根据RFC6761中的提到的,

IANA currently maintains a web server providing a web page explaining the purpose of example domains.

所以只有http请求才会被受理以展示那个说明页面。
你的邮件无法找到example.com的MX记录,
虽然可以落到A记录,顺利的拿到IP,但IANA并没有建立监听25端口的SMTP服务,
所以,邮件发送会失败。

Salesforce关于Update Records with Inactive Owners权限

// 老鸟直接去下面看重点

Salesforce作为平台,无论是Standard Object还是Custome Object,都提供了默认的系统字段。
这些字段普通用户无法直接更新与编辑,只有系统才可以修改,以此作为审计信息,保证数据的可靠性。

但是,由于并不是每个使用Salesforce的组织都刚刚步入信息化。
所以,从Legacy System迁移到我们全球最先进的CRM系统——Salesforce的时候,必然会涉及到数据迁移。
有的同学说了,数据迁移简单啊,把数据从原来的系统抽出来,然后插入到Salesforce里。
我们知道,如果直接导出,再插入的后果是导致创建日期等一众字段都会变成数据导入那天。
那么,如果有功能是基于创建先后顺序的时候该怎么办,五年前的数据将会比昨天创建的数据更靠前。

为了应对这种情况,Salesforce提供了Set Audit Fields upon Record Creation与Update Records with Inactive Owners两个权限。
开启的方法为:
1. Setup -> Build -> Customize -> User Interface -> Enable “Set Audit Fields upon Record Creation” and “Update Records with Inactive Owners” User Permissions 勾选
2. 创建一个PermissionSet将Set Audit Fields upon Record Creation与Update Records with Inactive Owners分别勾选。将此PermissionSet分配给需要的User。
3. 在Custom Profile里将Set Audit Fields upon Record Creation与Update Records with Inactive Owners分别勾选。
这里需要注意的是,System Admin是标准Profile,所以只能使用PermissionSet。

Set Audit Fields upon Record Creation顾名思义,就是在Insert数据的时候,可以设定Audit Fields。一次操作成型,不能再Update。
继续阅读“Salesforce关于Update Records with Inactive Owners权限”

Salesforce的Prefix和Suffix

行家一出手,就知有没有。
判断一个Salesforce从业者是不是老手的一个标准,
就是能不能对Governor Limit,Sobject的Prefix和Suffix倒背如流。

Governor Limit之前已经介绍过了,
Salesforce说了,当你觉得Governor Limit限制了你,那么意味着你的业务流程没有达到最优。

那么Prefix是什么呢。
Prefix就是俗称的3位码。
每个SObject都有一个3位编码,每一条该SObject的数据的头三位,都是以此3位编码开头。
如果把三位编码放在salesforce的url后面,可以快速的访问SObject的ListView页面。
https://[your instance name].salesforce.com/[prefix]

标准SObject的prefix都是固定的,
比如,Account一定是001
Contact一定是003
Lead一定是00Q
而自定义SObject就没有固定的编码了,但都以aXX开头。

Salesforce提供了一本速查手册
列出了所有标准SObject的prefix。
如果想得到一个全体SObject的花名册,可以去实现一下tooling api。
所有标准的,自定义的,还有不可见的SObject的Prefix都列出来了。

那么,Suffix呢,大家最熟悉的莫过于”__c”,
所有的自定义SObject的Suffix,所有自定义字段的Suffix,都是它。
那么除了__c之外,还有哪些呢。
国外大神总结在此
防丢失转载如下:

__c Custom Object or Custom field
__r Custom relationship field as used in a SOQL query to traverse the relationship
__ka KnowledgeArticle
__kav KnowledgeArticleVersion
__Feed Article Type Feed or Custom Object Feed
__ViewStat KnowledgeArticleViewStat
__VoteStat KnowledgeArticleVoteStat
__DataCategorySelection Article Type__DataCategorySelection
__x External Object
__xo Salesforce-to-Salesforce (S2S) spoke/proxy object
__mdt Custom Metadata Type
__Share Custom object sharing object
__Tag Salesforce Tags
__History Field History Tracking for Custom Objects
__pc Custom Person Account Field
__pr Used for traversing custom Person Account relationship fields
__hd Historical Data
__hqr, __hst Start/End of the Datetime range
__b BigObject
__latitude__s Geolocation Latitude Coordinate
__longitude__s Geolocation Longitude Coordinate
__e Platform Events – a.k.a. EventBus event (Winter ’17 pilot)
__p Custom Person Object (Spring ’15 pilot)

————By Daniel Ballinger

Salesforce的Field Id与URL Hack

默认值是个好东西。
别说是普通用户,就算是开发者,在面对满满一屏的空白字段时,也是要不禁皱皱眉头的。

设置得当的默认值可以极大的提高业务流畅度与用户满意度。
反之,每次都不得不改的默认值,会带来加倍的苦恼。(甚至会惹恼用户)

当然,Salesforce提供了字段的Default Value功能。
我们可以为大部分字段类型设置默认值。

Default Value必须为公式,并且不能使用其他字段的值,只可以使用预设的系统变量和环境变量。
因为这样的限制,使得我们在使用自定义button或者link跳转到Object标准编辑画面的时候,无法根据情况显示不同的默认值。

举个例子,比如Case上面有个字段用来储存Case的类型,然后在Case的详细画面上放置一个自定义Button,这个button用来新建一个order。
为了提高Agent的工作效率,弹出order的新建页面之后,我们希望order的类型是Case上所选择的值,并且与Case关联同一个Account。

这种场景下,字段的Default Value已经无法满足需求。
我们需要使用一种Salesforce并不提倡的技巧———-URL Hack。

URL Hack就是通过在Salesforce的URL上加上额外的参数,来实现我们的需求。
其中一种技巧,就是给Standard Pagelayout赋值。

那么让我们来看看URL Hack到底是什么样子。
首先,点击Case标签里的新建Button。
跳转到Case的新建画面之后,观察地址栏的URL如下
无RecordType: https://[InstanceName].salesforce.com/500/e?retURL=%2F500%2Fo
有RecordType: https://[InstanceName].salesforce.com/500/e?retURL=%2F500%2Fo&RecordType=0126F000000v348&ent=Case

其中,500代表Case,每个Object都有一组三位码,同时也意味着所有的数据的Id都是由这三位码开头。
e代表编辑页面。 500/e代表新建数据,500XXXXXXXXX/e代表修改既存数据。
retURL代表回调页面,当点击取消Button之后,会返回到指定的页面。在例子中,%2F代表“/”,所以会返回500/o。
有RecordType的URL多了一个RecordType参数,值0126F000000v348就是recordType的Id。

所以,RecordType以外的字段,我们也可以通过同样的方式进行赋值。

首先,与RecordType不同的是,我们需要用的并不是字段的API名,而是Field ID。
如果不使用工具的话,Field Id并没有快捷的查看方式,所以应急的话,可以直接打开编辑画面,然后用浏览器的F12开发者工具直接查看。

标准字段是字母+数字的形式,自定义字段则是以00N开头的ID。
就拿上面的例子来说,如果将“123456”赋给Account Number,URL应写为
https://[InstanceName].salesforce.com/500/e?acc5=123456&retURL=%2F500%2Fo

如果是Lookup字段的话,上面那样写就行不通了,要在Field Id后面加上_lkid后缀
标准字段的话,我们以Account的Parent Account为例
https://[InstanceName].salesforce.com/001/e?acc3_lkid=001XXXXXXXXX

自定义字段的话,Lookup字段还需要在00N前加上CF
https://[InstanceName].salesforce.com/001/e?CF00NXXXXXXXXX_lkid=001XXXXXXXXX

有的时候只写lkid结尾的参数看起来并不管用,在编辑页面上,Lookup字段仍然是空白的,但实际上保存之后是已经被赋值的。
如果遇到这种现象,就要加额外加一个参数
https://[InstanceName].salesforce.com/001/e?acc3_lkid=001XXXXXXXXX&acc3=RecordName
https://[InstanceName].salesforce.com/001/e?CF00NXXXXXXXXX_lkid=001XXXXXXXXX&CF00NXXXXXXXXXX=RecordName

最后记住,一定要养成随时设置retURL的好习惯。

上述所有的值在Custome Button或者Link中,都可以用表达式替换,比如
00NXXXXXXXXXXXX={!Case.Status}等。

如果想了解URL Hack还能做哪些事情,之后会写一篇专访。

Salesforce的TestClass

虽然Salesforce从业人员应该尽量使用标准功能来实现需求,
但仍不可避免的要用到代码。

标准功能的话,Salesforce会自己负责质量(虽然Bug频发。。。),
对于自定义功能,Salesforce则制定了质量标准,
比如,如果总体代码覆盖率不到75%,无法Deploy。

一般的Apex Code,就像那些出现在Trigger里,出现在Controller里的代码,
测试类都很好写,就按照
1. 准备测试数据
2. 执行业务逻辑
3. 断言执行结果
按套路打就行了。

除此之外,另一些apex code,需要特别的测法。
Salesforce作为CRM系统,无法避免的要与其他系统进行数据交互。
大部分情况,我们只需要把salesforce的标准集成文档和权限已经配置妥当的账号,提供给对方就好,
但是,当我们需要将业务封装起来的时候,就需要自己建WebService。
况且,Salesforce主动调用外部系统接口的时候,又要区分是Rest还是SOAP。

对于Apex Rest Callout可以使用下面三种方式(代码示例点击链接)
1. HttpCalloutMock
比较常见的写法,有极大的可拓展性(毕竟连响应结果都要自己Code进去)。
2. StaticResourceCalloutMock
LoadData是一对好兄弟,都是在Test Class中利用StaticResource,可以很好的做到测试数据与测试代码分离。
3. MultiStaticResourceCalloutMock
StaticResourceCalloutMock的复数URL版。

对于Apex SOAP Callout,Salesforce也提供了一种方式。
1. WebServiceMock

以上是Callout的测试类写法,原理均是构造一个Mock类,然后提供一个假的response。
你总不能期待每次跑测试类的时候,让人家集成方的服务器真的去响应你吧?

那么Callin呢。
首先,处理Callin的Apex Class一定是WebService。
WebService也分为Rest Webservice和SOAP WebService。
对于Rest WebService,Salesforce并没有提供类似与Mock类的工具类,所以我们直接去构造上下文环境。
在实际情况种,上下文环境是由系统进行构造的,这里我们代替系统。

        .................
        RestRequest request = new RestRequest();
        request.requestURI = 'https://[InstanceName].salesforce.com/services/apexrest/xxxxxx/';
        request.httpMethod = 'GET';
        RestContext.request = request;
        /** Call your WebService like below **/
        MyWebService.MyFoo();
        .................

对于SOAP Webservice就简单多了。因为不需要处理上下文环境的参数等内容,直接处理方法中的参数就好,
所以就当作普通的Apex Code处理就可以,不需要特殊的操作。

除此之外。

虽然目前我还没见有人用过,但是Salesforce还是提供了一个Stub接口,用来做Mock框架。
它的好处是可以实现单元测试的完全解耦,对各个小模块进行单独的测试。
就目前市面上各个大公司的状况来看。。。能好好写个断言就不错了。(捂脸)(尴尬)

Salesforce的Rollback与SavePoint

之前对于Salesforce的RollBack机制没有进行深入的思考。
那天,闲来无事,有人问了我一个问题。

问说,下面的代码在匿名快执行完之后插进去了几条数据。

ObjectA__c objA = new ObjectA__c();
objA.Name = "TestA";
insert objA;

List<ObjectB__c> objBList = new List<ObjectB__c>
for(Integer i = 0; i < 200; i++) {
    ObjectB__c objB = new ObjectB__c();
    objB.Name = "TestB" + i;
    insert ojbB;
}

我大概扫了一眼,问的是Governor Limit嘛,答案是150,送分题,不谢。
但又隐约的觉得不大对劲,既然拿来问我,肯定有坑。
立马打开Developer Console试了一下。
看到了结果倒吸了一口冷气。。。。。。。。0,一条都没插进去。全都RollBack了?

咦?不对啊,那一定是匿名块做了特殊的设置。试试Controller。
要不然我以前在Controller里写个什么劲儿的SavePoint。
VF:

<apex:page controller="TestRollbackClass">
    <apex:form>
    	<apex:commandButton action="{!save}" value="Button"/>  
    </apex:form>
</apex:page>

Apex Class:

public class TestRollbackClass {

    public void save() {     
        ObjectA__c oa = new ObjectA__c();
        oa.Name = 'TestA';
        insert oa;

	ObjectB__c ob = new ObjectB__c();
        ob.Name = 'testB';
        insert ob;
    }
}

我在ObjectB上加了一个Validation Rule,保证新建ObjectB记录的一定会报错。
这次总该如预期一般只有ObjectA被插进去了吧。

结果一跑,唉? 还是都RollBack了?
瞬间怀疑人生。既然都自动RollBack了,那我以前为什么必须写SavePoint?
去翻了下Release Note,也没找到相关的变动。

没办法,一定是写法问题,我试着按照Coding Standard写一次

public class TestRollbackClass {

    public void save() {
	ObjectA__c oa = new ObjectA__c();
	oa.Name = 'Test2';
        insert oa;
        try {
            ObjectB__c ob = new ObjectB__c();
        	ob.Name = 'test1';
        	insert ob;
        } catch(Exception e) {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, e.getMessage()));
        }
    }
}

再次执行,第一条数据ObjectA居然插进去了。
我瞬间明白了点什么。

Salesforce的机制应该是这样的,
如果Transaction被异常中断,则Rollback全部的数据变动。
如果异常被捕获,则已经正常commit的数据变动会落实。出错的部分无效。

按照开发潜规则,应该展现给User更Friendly的错误信息,而不是可怕的Trace Log信息。
所以我以前的公司要求捕获所有可能的异常并替换成友好的错误信息。
这样的话,Transaction一定不会被异常中断,所以自动全部Rollback的机制从来没有启用过。
那么为了防止在出错时插入脏数据,Salesforce提供了SavePoint。
用法如下:

public class TestRollbackClass {

    public void save() {
        SavePoint sp = Database.setSavepoint();
		ObjectA__c oa = new ObjectA__c();
		oa.Name = 'Test2';
        insert oa;
        try {
            ObjectB__c ob = new ObjectB__c();
        	ob.Name = 'test1';
        	insert ob;
        } catch(Exception e) {
            Database.rollback(sp);
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, e.getMessage()));
        }
    }
}

如果ObjectB插入出错,已insert的ObjectA也会回滚。达到目的。

Salesforce的字段类型转换

如果维护/接手别人的烂摊子,
有很大几率碰到字段类型转换问题。

Salesforce的字段类型转换并不是100%安全。
如果符合下列规则,则会丢失所有数据。

Date -> Datetime
DateTime -> Date
Any -> Percent
Any -> Number
Any -> Currency
Checkbox -> Any
MultiPicklist -> Any
Any -> MultiPicklist
PickList -> multiPickList value retains if exsited in multipicklist
AutoNumber -> Any(Exclude Text)
Any (exclude Text) -> AutoNumber
Text -> PickList
TextArea(Long) -> Any(exclude Email, phone, Text, Text Area, URL)

常规操作顺序,
应该先在sandbox上演练一下,
确认结果与预期一致。
然后,
先将生产环境的数据备份,
再在生产环境上进行实际操作。

官方文档
https://help.salesforce.com/articleView?id=notes_on_changing_custom_field_types.htm&type=5

Salesforce关于Debug

做Salesforce开发,最让人头疼的事情,莫过于Debug。

不同于Java开发,已经有了一系列成熟的Debug工具,
Salesforce开发者最常用的手段只有输出log,就如同Javascript开发一样。
不过随着现代浏览器的兴起,就算是Javascript开发也可以debug了。

对于一个老手来说,添加System.debug()然后查log,虽然已经足够。
但是,面对巨型transaction的时候,就要面临Debug Log的限制
1. log总体大小不能超过3MB,否则随机截断(不管官网怎么说,我觉得是随机的,囧)
2. log只能存留七天
3. 15分钟内产生的log不能超过250MB

为了回避此问题,Salesforce提供了Class和Trigger的Debug Log级别自定义功能。官网点此
启用这个功能,可以在Class和Trigger的详细页面,找到TraceFlag,在此设定需要的Debug log级别,(一般全开最详细,或者全关)
自定义完成后,transaction中class和trigger产生log时的级别设定,就会覆盖默认的debug log级别设定。
使用此功能可以极大的减少不需要的log输出,从而避免被随机截断。

但是,System.debug必然不是万能的。

所以在DeveloperConsole中,Salesforce给开发者提供了CheckPoint功能,注意,不是BreakPoint,是CheckPoint。
Checkpoint不同于BreakPoint那样可以互动。只提供了信息查看功能。
就是说,在代码中打上Checkpoint之后,程序执行并不会暂停,而是执行到结束,但Salesforce会将Checkpoint标记位置的内存状态保留下来供开发者查看。
需要值得注意的是,Checkpoint全局只能打5个。

如果代码真的复杂到checkpoint和Debug log都无法搞定,
Salesforce提供了一个真正的Debug工具——Apex Debugger
有了Apex Debugger,Salesforce开发者也可以像Java程序员一样debug了。
不过越强大的武器,限制越多。
Apex Debugger只提供了一个免费的License,并且是全Org通用,就说Production和旗下所有sandbox合起来同时只能有一个人用。

不管使用哪种Debug方式,都应该从设计伊始就避免设计出如此巨大的自定义功能。

Salesforce的Sharing Rule中Criteria Rule如何判断空值

某日,客户提了一个需求,
说想把某个字段是非空的数据Share给另一个部门。

看起来是个很简单的需求,
就是一个Sharing Rule的事儿。

然后,立马新建了一个Sharing criteria rule,
设定条件为[ 字段 not equals to 空值 ],保存。唉?报错了。
那么空格呢?也不行。

Error: Empty value

难道,Sharing Rule不能判断空值?
正在百思不得其解之时,经高人指点,说空值的位置写成[“”]就可以了。

赶紧试了一下,果然成功了。

保存后的效果是这样的。

Salesforce如何用Import Wizard将空值插入PicklistValue字段

如果Picklist Value没有定义为必填字段。
则多出一个–None–选项可以选,
在DB里存为空。

我们知道Dataloader默认会无视所有值为空的字段,
除非我们在设置里将[Insert Null Values]选中。

但是在Import Wizard中并没有提供类似的选项。
如果尝试将字段的值留空,一样会被无视,空格等一切空白字符也会被无视。

解决方法是,将想设置成空的字段的值改为[#N/A]。
之后再次导入就可以了。