Salesforce执行顺序小口诀

  1. 页面后台两不同,
  2. 布局规则最优先,
  3. 格式长度和必填。
  4. Before Trigger触发前,
  5. 后台只将外键检。
  6. 批量插入有例外,
  7. 验证规则提前验。
  8. Before之后做验证,
  9. 自定规则和必填,
  10. 系统规则不二遍。
  11. 验证之后跑去重,
  12. 存入DB不提交。
  13. After Trigger触发后,
  14. 分配/回复/工作流,
  15. 如果字段有更新,
  16. 验证/去重不再做,
  17. Trigger仅再跑一次。
  18. PB/Flow依次跑,
  19. 数据操作从头走,
  20. Case规则在随后。
  21. 父表汇总此时算,
  22. 工作流把父表更,
  23. 共享规则重计算。
  24. 数据DB提交后,
  25. 后续邮件才发送。

// 逐句解释更新中……
解释:
行1,行2,行3:与使用apex或者api新建或者更新数据相比, 通过Page Layout新建或者更新数据的行为会少许不同。通过Page Layout的时候会根据layout的字段属性配置提前进行一次字段格式(比如邮箱类型,电话号码格式等)/字段长度/字段是否必填等检查,才会进行后续的Trigger处理。
行4,行5:对于通过Apex或者API等方式后台创建/更新数据,虽然不会提前做一次系统级检查,但会对外键做一次检查,确保没有发生引用自己的情况。
行6,行7 :按理说接下来无论是用过Page Layout还是通过后台,下一步都应该进入Trigger处理了吧?还不一定。这里有一个例外形况,就是如果发生了例如Opportunity Line Item或者Quote Line Item的批量创建,则会提前执行一次自定义的Validation Rule。

官方文档地址: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_order_of_execution.htm

5+

关于CANNOT_CASCADE_PRODUCT_ACTIVE错误

某日,一个悠闲的午后,我泡的一杯毛尖正在阳光下散发着热气。嗯,接到一个小活儿。平平稳稳,不需要经历大风大浪,不需过五关斩六将的小活儿——-导数据。

这次需要更新的数据是Product。就是那个标准SObject,Product2,三位prefix是01t。人们常说用标准SObject用多了总会遇见坑,但人生又何尝不是如此。不是
完全控制在自己手里的东西,只能去了解,去接受,最后习以为常。

果然,这次就掉坑里了。工具执行结束之后,一看Log。在1000条数据里面,有50多条报了如下的错误。
StatusCode: CANNOT_CASCADE_PRODUCT_ACTIVE
Error Message: Cannot update associated pricebook entries without the Manage Price Books permission
然后看了下报错的数据,也没什么特别明显的特征。 貌似是无差别报错。

唔。。。万事不决查文档。
很快就在《SOAP API Developer Guide》定位到了这个Status Code。
https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_concepts_core_data_objects.htm


CANNOT_CASCADE_PRODUCT_ACTIVE
An update to a product caused by a cascade can’t be done because the associated product is active.

《SOAP API Developer Guide》

咦?怎么详描述的与系统返回的信息不一样?


系统反馈给我的信息是“由于没有Manage Price Books Permission, 所以无法更新关联的Pricebook”。只是说由于我没有Manage Price Books权限才报错,但是我更新Product为什么又会在意我有没有Manage Price Books的权限?

而文档给的信息是“由于product是active,所以无法完成由联级更新引起的被关联的product。 ”这写的是啥啥啥?什么叫caused by a cascade,谁是发出动作的人?是我的话我又cascade了谁?谁又Associated了我?

既然《SOAP API Developer Guide》的描述完全不知所云,那么就从系统直接反馈的错误信息入手吧。查了一下使用的账号,Profile和Permission上确实都没有PriceBook的CRUD权限。不过。。。。如果是因为没有PriceBook的权限的话,为什么绝大部分数据还更新成功了?如果真是这样,由于我没有权限,应该一条都不会成功才对。

结合两条截然不同的信息来看。线索指向了两个关键点。
1. PriceBook的权限。
2. Product的active状态。

于是做了一个试验,在没有PriceBook编辑权限的前提下,尝试去变更Product的active状态。果然报出的相同的错误信息。
就是说,如果想更新product的active状态,需要Pricebook的编辑权限。
唔。。。。


故事当然没有到此结束,不甘心的我突发奇想的在本地硬盘搜索了一下这个status code,结果在一本上古文档里发现了这样一段描述。

CANNOT_CASCADE_PRODUCT_ACTIVE

You cannot activate or deactivate this product without also being able to edit pricebooks.

某上古文档

原来错误信息还有第三个版本。这个版本明确的指出了问题的所在。

接下来是无责任脑洞推理没有任何实锤和Evidence。

在Salesforce中,Product和Price Book是多对多的关系。一个product可以有多个Price Book,而Price Book也可以包含多个Product。Price Book Entry就是二者的Junction Object。
如果用workbench查看Price Book Entry上关联Product与Price Book的字段的关系属性,会发现该关系具有联级删除属性(cascadeDelete: true)。
所以在此推定Product与Price Book Entry,Price Book与Price Book Entry之间的关系皆为Master-Detail。
在表权限层级。

在Profile中,如果想要拥有子表的编辑权限,那么至少要拥有父表的View权限。反过来,如果连父表的view权限都没有,则子表没有任何权限。

所以,由于我没有Price Book的任何权限,导致失去Price Book Entry的所有权限。
而根据观察,Product在active状态变化的时候,会同步active状态到所有旗下的Product Book Entry。
在此推测Salesforce在Product上加了一个Trigger/Workflow/Flow/Process Builder用来用来同步active状态,而且在这个处理中特意做了权限检查。如果发现没有Price Book Entry的权限,则去检查Price Book的权限,如果也没有权限,就报错。

这也正是系统直接返回的错误信息的由来:

Cannot update associated pricebook entries without the Manage Price Books permission

至于为何错误信息出现这么多版本,我推测是由于与Product有多对多关系的不仅仅只有Price Book,所以同一个StatusCode从单一情景变为也会复用在其他的情景。所以错误信息在文档里就改成了更加泛指的版本。而这次我遇见的错误是一个很具体的场景,则出现的信息又由泛指的版本变为更加具体的版本。

脑洞完毕。我去加权限了。。

0

Dataloader与External Id

某日,又见某童鞋双手合十,指尖杵在下巴上,做福尔摩斯状。
忍不住凑了过去 。
此童鞋用眼角瞄到有人过来了,嘴角抽动了一下,欲言又止。

“别难为自己了,说说吧。”团结才是力量,对不对。
“好吧。。。”他长长的吐了一口气,手也放了下来。“事情是这样的,我有一张表叫做order。这个order有个字段叫product,会去lookup一个叫product的表。”。说着,他打开了SObject的详细页面。“现在我需要导入历史订单数据,量很大。”

我点了点头,示意他继续,因为目前为止听起来没什么。
“问题是原始数据的product字段并不是product的id,而是product的code。这样的话我没法直接导入系统。” 在我开口之前他赶紧抢先又说道,“product的种类和order的数据量都实在太大了,实在没办法用vlookup处理。已经死机好几次了。”

我弹开了他的手,接过鼠标打开了product表的定义页面。在自定义字段中找寻那个叫code的字段。唔。。。。External Id勾选了,Unique勾选了。不禁扬起了嘴角。

“看,Product的code是external Id。”我指着屏幕。
“可是我们要导入数据的是Order啊,不是Product。”
“那你先说说看,External Id在Dataloader里怎么用?”
“就是Upsert的时候,可以指定External Id,如果匹配上了就update,没匹配上就insert。”他自信的回答道。
“没错。但是还有一种用法。”
“啊?”

众所周知,lookup字段实际储存的值为ID。这也就意味着,在insert或者update数据的时候,我们要将Id写在lookup字段上。如果写的不是Id,或者Id不存在,Dataloader会告诉我们数据写入失败。
一般的情况下,如果我们只有被lookup的表中一个字段的值(Product Code),就不得不先根据这个值(Product的Code)去查出被lookup表的id(Product的Id),然后放在待更新数据的lookup字段上(Order的Product字段),才能继续执行下去(Order)。

不过,老天关上了你的门,一定会打开你的窗,毕竟你才是你生活的主角,所以有主角光环。

事实上,如果你拥有的值(Product Code)是被Lookup表的External Id(Product的Code),那么在插入数据时,可以让Dataloader根据External Id自动的将lookup字段填入对应的Id。这样就不用人工的去查找替换了。

做法如下。
1. 首先,一如往常,准备好csv,点insert,输入账号密码,选择Order,选择csv路径,下一步。
2. 然后,Create or Edit a Map, Auto-Match Fields to Columns。
3. 接下来就不一样了。在匹配完字段时候,点击Save Mapping。选择路径,填入文件名。保存sdl文件。
4. 找到刚刚保存的sdl文件,用文本编辑器打开。修改如下。保存。

#Mapping values
#Wed Apr 24 19:13:21 CST 2019
# Old 
#PRODUCT__C=product__c
# New 
PRODUCT__C=product__r\:code__c
.........

5. 回到Datalaoder,关掉Mapping Dialog窗口,这次点击Choose an Exsiting Map,选择刚刚保存的sdl文件。
6. 一路next。
7. 恭喜插入成功。(如果Product Code存在的话)

“看,你与成功之间只差一个\:”。我拍了拍了他的肩旁。
“恩人慢走”。

总结:
1. 被lookup表需要有External ID。
2. 在SDL文件中,将lookup字段的__c变成__r(标准字段去掉Id),加上“\:”,加上External Id的API Name。insert或者update的时候使用此sdl。
3. 必须仅且只仅匹配到一条记录才可以成功写入。

参考官方文档:
https://help.salesforce.com/articleView?id=000002783&type=1

后记:
有人问我说,唉,明明写的技术相关的内容,文风为什么这么不正经?
其实原因不外乎两个。第一个是想看正八经儿的说明文就应该去看官方文档,而不是等着官方文档的翻译。Salesforce的文档和知识库是很强大和全面的。第二个是每一篇文章的灵感来源都是真实被提出过的问题,我觉得自古以来,最好的承载知识的方式就是带入情景,讲故事。官方文档似的说明文是告诉你怎么一步一步的用锤子钉钉子,而我是想用故事告诉你可以用锤子来解决钉钉子的问题。
总之,想要一个工具速查手册的话,应该去看官方文档知识库才对。

1+

关于在Component中向Lightning Button传值的方法

某日,
打水途中看到某位同学眉头紧锁双手合十做沉思状,于是乎忍不住凑了过去。
哦。。。原来在写Lightning Component。隐隐约约看到密密麻麻的controller.js代码感觉不妙。
“咳。”为了避免吓到他,我轻咳了一下表示进场。
只见他先是略微抬起头,之后彻底滑落到了椅子上。
“唉,事情是这样的,” 他用沙哑的嗓音说道,“最开始他们要我做一个button,点了之后去后台更新一条数据,将其字段A__c更新为a。”
他把自己从椅子上重新支了起来,指着屏幕说,“这个很简单,我放了一个attribute来存需要更新的数据的id,然后在lightning button的onClick属性指定controller的方法,在controller的方法中cmp.get那个attribute的值,之后传给apex去更新这条数据。”
我微微点头表示赞同。“没毛病。然后呢?”

“然后,他们又说,要再加一个button,点这个button之后,要给这条数据的字段A__c更新为b。OK,我在cmp中加了又一个button,然后在controller里又为了新button新建了一个方法,与之前的方法不同的是,更新的值为空。”
“也不是不可以呀。“
“对,”接下来他双眼空洞的目视前方。“然后他们又让我加三个button,点了之后分别赋值b,c,d。这时候我意识到情况不对,打算在button的conlick事件把值传给controller中的方法,这样就不用每加一个button就新加一个方法了。结果……我发现……lightning button的onclick根本不能设参数,controller的方法参数也是固定的。我没办法了……”

“我明白你的意思了”。我按住了他的肩膀。

在常规前端开发中,在button的click事件中绑定的js函数传递不同的值是常见做法。如果点击button之后,处理相同,仅是需要处理的参数不同的情况应该对代码进行合理的复用。如下例。

let foo1 = function(param1) {
console.log(param1);
}
<div>
    <input type="button" name="Button" onClick="foo1('a')"/>
</div>

但是lightning button(官方文档)的onclick事件,并没有提供直接传递参数的方式。所以我们只能走曲线救国的路线。
其实很简单。
首先,在官方文档里提到,

You can retrieve the button that’s clicked by using event.getSource(). For example, to retrieve the label on the button, use event.getSource().get("v.label").

说明我们可以在controller的方法中拿到event的触发者——被点击的button,然后获得此button上的属性——比如Label。

那么就好办了,首先我们在lightning button上光明正大的设置value属性

<lightning:button label="button1" title="create" onclick="{!c.updateRecord}" value="a"/>

然后在controller的方法中,去get这个button的value属性,接下来正常执行业务逻辑就可以了。

({   
    updateRecord : function (component, event, helper) {
        let targetValue = event.getSource().get("v.value");
        // YOUR STATEMENT HERE ......
    }
})

好了,水凉了,还得重新接一杯。

5+

Salesforce判断字符串是否为合法Id的规则

在去年年初,我们曾经讨论过Salesforce 15位ID与18位ID的话题(《关于salesforce的15位id与18位id》)。
那时候简单介绍了Salesforce的Id的组成,比如头三位为prefix,4到6位为instance code,校检位,18位ID的特性是case safe而不是大小写不敏感,等等。
那么有没有想过,Salesforce是如何知道某个字符串是不是合法的ID的呢?

虽然Apex里有Id与String两种数据类型,但是两者之间的关系却是如此暧昧与纠缠不清。
很多时候本应当使用Id型变量储存Id值,我们用String型变量去储存,然而也不会出问题。
再比如说,在SOQL中,我们在where语句中可以这么写…WHERE Id = ‘0016F000035LFXo ‘…干脆就是用的字符串。
如果是在Apex中,我们也可以使用变量…WHERE Id =: myId …,这里的变量myId无论是Id型还是String型也都不会有问题。所以粗略的看,似乎当作是一回事也没关系。

但是,如果我将字符串”ABCDEFG”赋值给ID型的变量,会怎么样呢?
如我们所料,会报异常,“ System.StringException: Invalid id: ABCDEFG ”。
这说明ID数据类型还是会对Id做合法性检查。
所以引出了最开始的问题,Salesforce是如何知道这个字符串是不是合法的ID的呢?

继续阅读“Salesforce判断字符串是否为合法Id的规则”
7+

SOQL For Loop的效率问题

一天,有人和我抱怨说系统越跑越慢。
说当年系统刚开发出来的时候,页面唰的一下就打开了。
而现在同样的页面却要转圈转个800年。
客户天天向他抱怨。

听到这个现象我瞬间来了兴趣,随即放下了手中的工作。
这位兄台,借一步说话。

千里迢迢赶到了案发现场。兄台亲手做了演示。
确实,那个号称唰一下就打开的页面,现在要转个许久才行。
从上到下的仔细观察了一下页面(是个VF)的内容,发现有个带翻页的表格。
除了这个表格之外,其他的内容都只是简简单单的字段内容显示。
总体来讲,一眼看上去也没有什么非常不对劲的东西。

Talk is cheap, Show me the code.
兄台麻利儿的打开了VF和Controller的代码。
大概扫了一遍,格式工整,命名规范,结构清晰,注释齐备,没有任何奇葩写法。
那就奇怪了,如果是代码写法问题,那么应该从一开始就慢才对。

如果说有什么东西是随着时间不断积累的,不一定是经验和阅历,也有可能是数据量。
所以我又扫了一遍Controller里的SOQL。
发现只有在初始化表格的SOQL中使用了LIMIT 10000。
而其他的SOQL大都直接指定了ID作为检索条件。

我指了指那条SOQL,与那位兄台确认了一下眼神,不过兄台一脸迷茫。
“咳。。。我觉得是这里的问题,系统刚开始用的时候没有数据,所以这里初始化做的很快。但系统用久了之后,数据越来越多,这里要处理的数据也越来越多,所以就慢了。”
“哦。。。但是,才10000条数据,应该不至于啊。。。”

我竟一时语塞。
盯着这段代码半晌。

....................................
for(tom__c t: [SELECT Id, Name FROM tom__c WHERE RecordTypeId = 'XXXXXXXXXXXXXX' LIMIT 10000]) {
tableRowList.add(new tableRow(t));
}
.................................

求锤得锤,用实锤说话。
换测试环境,开Debug Log!
为了看到底是哪里拖慢了页面速度,我在每块处理前后都加了system.debug();
虽然做了万全之准备,结果却仍让人始料未及。
1. Debug Log的每一步的执行时间居然丢失了毫秒细节!

令人不得不吐槽的是,在Developer Console里面打开同样的一条Debug Log,执行时间的毫秒细节居然是有的!

2.由于Log内容太多,输出内容被无情截断。

所以只好换一个打法。
不再尝试执行完整transaction,
而是将每一块代码都改造成独立的代码块,
前后加上system.debug(datetime.now() + ‘ —- ‘ + datetime.now().millisecond());
逐一在匿名块中执行。

经过如同人生般漫长而又枯燥的实验后。
终于发现,耗时最长的确实是LIMIT 10000的那块代码。

那我就奇怪了,就是循环个10000条数据,也不至于那么慢啊。
难道是因为SOQL For Loop。
于是,立即动手做了下面的实验。
1. 准备一个干净的DE
2. 准备2000条Account(再多放不下)
3. 分别执行下面的匿名块

system.debug(datetime.now() + ' ---- ' + datetime.now().millisecond());
for(Account acc : [Select Id, Name from Account]){
String a = acc.Name;
}
system.debug(datetime.now() + ' ---- ' + datetime.now().millisecond());
// --------------WoShiFenGeXian----------------
system.debug(datetime.now() + ' ---- ' + datetime.now().millisecond());
List<Account> accList = [Select Id, Name from Account];
for(Account acc : accList){
String a = acc.Name;
}
system.debug(datetime.now() + ' ---- ' + datetime.now().millisecond());

4.调换顺序多次执行,以确保结果公平。因为最先跑的没有缓存,索引等数据库优化,会吃亏。所以为了严谨要排除前几轮之后取平均值。

结果如下。

SOQL For Loop写法,2061条数据循环,427-324=103毫秒。

先查询后循环写法,2061条数据循环,995-924=71毫秒。

速度提升 (103-71)/103*100%约等于31%

先查询后循环写法大比分胜出。
由于我的实验的循环内容几乎是没有任何负载,所以循环处理的消耗可以忽略不计。
但是,如果每次循环都很耗时(很不幸,这位兄台的系统就是如此),
这个百分比还会扩大。

那么为什么SOQL For Loop的写法会慢呢。
SOQL For Loop的官方文档介绍

原来SOQL For Loop的写法不仅仅是节省代码行数,为了美观。
而是改变了底层原理。
SOQL For Loop不再是将检索结果一起从数据库里取出来之后,使用迭代器一条一条处理。
而是改用SOAP API,使用Query与QueryMore从数据库里拿一批处理一批。
所以SOQL For Loop因此具有了额外的特性——————可以选择200条循环一次。

到最后就变成了一个经典的困境。
在生活中,你只能用时间去换钱,或者用钱换时间。
在算法里,必须用空间换时间,或者用时间换空间。

先查询后循环的方式,由于要先把整个结果集放到内存里,所以要使用很大的一块空间,对于viewState紧张的页面,或者已经占用了大量内存的代码,可能会成为压死骆驼的最后一根稻草。
而SOQL For Loop拿到一批,处理一批,释放一批的做法,不会占用大量的内存空间。但代价是要承担这部分性能消耗所造成的额外时间开销。
如果开启更详细的Debug log,则可以看到SOQL For Loop在循环的时候在疯狂的进行内存操作。

那么瓶颈找到了。
我们是不是把SOQL For Loop改成先查询后循环的方式就结案了呢。
不,我们最后把这个表格改成了异步加载。。。。

9+

Dataloader与Timezone

此处本没有文章。催更的人多了,便有了文章。

最近一直沉迷于做另一款浏览器插件,所以写文章的事情又耽搁了下来。
人成长的过程就是不断的完善自己的过程,就如同维护系统一样。
很多问题以前想不清楚,说不定什么机缘巧合就清楚了。

最近(4个月前)有人跑来找我,说Dataloader疯了。
我就好奇啊,好端端的Dataloader怎么就说疯就疯了呢。
那人说了,他遇到的一个问题,
就是导入日期的时候,发现总是差了一天。
比如,tom__c上有一个日期字段date__c,看数据tom1的详细画面,date__c的值为2018-06-29。
然后他把tom1的date__c的值export出来,再update到jerry1的日期字段date__c。
结果在jerry1的详细画面上,date__c的日期少了一天,变成了2018-06-28。
而另一个日期时间型字段datetime__c则没有出现此问题。

肯定事出有因啊。
我就让他看看export的csv文件,里面的日期是啥样的。

咦?date__c是与详细画面一样2018-06-29,而datetime__c则变成了2018-06-28。
然后再update回去之后,在详细画面上看,反而date__c是2018-06-28,datatime__c却是2018-06-29。

结果人家突然一拍大腿,说“我知道了!”,我就很惊诧,这就知道了?
他笃定的说:“这肯定是Dataloader有Bug!我在setting里设置的是东八区,这date型取出来没问题,datetime型取出来却是零时区。这肯定是Dataloder的Bug!我去提票了!”说罢起身就要走。
“且慢!”我赶紧叫住了他。
作为一个成熟的CRM系统,SFDC已经内置了成熟I18N解决方案,以此确保客户全球化的业务可以正常开展。
I18N除了语言的相互翻译,还有包括维护时间的一致性。比如,我在北京早上8点创建一条数据,洛杉矶的的同事应该看到创建时间是昨天的17点,而不是洛杉矶的早上8点。
为了实现这个功能,sfdc在DB中统一存放GMT时间,谁在页面上查看就换算成谁所在的时区。
但是,如果从DB查询(包括Dataloader)的话,是无视任何User个人及Dataloader的时区设定,取出来的是已经换算成零时区的时间。
相反的,不管你所在哪个时区,日期时间都会被换算成零时区之后,才会存进数据库。
所以,由于这个人设定的时区是东八区,CSV文件中datetime__c被减了八小时是正确的。
那么date__c为什么没减八小时呢?这里推测是因为在编辑页面保存的时候,系统会自动补上与时区相等的时分秒,以此保持日期不变。

“这么说的话,那就是update的时候Dataloader出bug了。要不然最后怎么差了一天。”他幽幽的说道。
我摇了摇头。
继续阅读“Dataloader与Timezone”

8+

关于编辑Converted的Lead

Salesforce真的应该对已经反悔的设定,做一个文档召回机制。

最近想修改Converted的Lead上的信息。
由于使用的是上帝账号,所以直接用Dataloader进行Update,
结果出现了“Cannot reference converted Lead”的错误。
唔?在上古时期,确实Convert之后的Lead再也不能动了,
但我确实记得某次更新之后就可以了啊。

然后开始翻找与此相关的文档,
功夫不负有心人,终于找到了在Spring’16的Release Note上,有相关的描述。
Spring’16,开启了”Set Audit Fields upon Record Creation”与”Update Records with Inactive Owners”两个权限的话,
就有了编辑Converted Lead的能力。

不过,我很确定我有这两个权限。
由于标准的System Admin Profile无法编辑User Permission,所以我特意做了一个Permission Set,并且里面确实有我自己。
八成Salesforce又吃设定了。
果然,这篇Article中明确指出,

Since Spring ’17 ‘Set Audit Fields upon Record Creation’ and ‘Update Records with Inactive Owners’ no longer grants access to converted leads.

这两个权限不再负责Converted Lead的编辑。
而是增加了一个权限叫做’View and Edit Converted Leads.’

好吧。你赢了。
然后我打开了Permission Set——————————————————没有?
再三确认之后,我确定了,没错,在Permission Set里没有并没有这个权限!!!!
目测又是一个Bug。

最后没办法,用Ant Migration Tool手动增加了下面的metadata片段,Deploy回去。

<userPermissions>
    <enabled>true</enabled>
    <name>AllowViewEditConvertedLeads</name>
</userPermissions>

之后,再用Dataloader试一次,这次可以了。

目前针对这个问题进行搜索,排名靠前的搜索结果仍然是对Spring’16 Release Note的引用。
这不是第一次,也不会是最后一次。希望Salesforce能尽早考虑实现文档的召回机制。囧

3+

Trigger的小口诀

insert没有old,delete没有new,
update俩都有,undelete不通用。
before改自己,after改别人,
flag防止死循环。
DML必须批量化,
循环数据整理完。
trigger不需要多建,
handler业务都实现。
预留开关可跳过,
运维人员笑呵呵。

解释:
行1:Trigger.old只能在update和delete中使用。Trigger.new只能在insert,update和undelete中使用。
行2:update里面可以同时用Trigger.new与Trigger.old,undetete只能用在部分Obj上(链接)。
行3:Trigger.new只能在before里更改,所以在after里等一切尘埃落定之后再去改别的表吧。
行4:Trigger中改别的表,很容易出现循环触发的情况。如果没有特殊的需求,应该加flag控制,防止Trigger之间形成死循环。
行5:不要在循环里DML,不要在循环里DML,不要在循环里DML。DML次数很宝贵的,要攒一起做。
行6:三部曲,获得数据->整理数据->插入数据。循环只用来整理数据。
行7:如果在同一个Obj上建立多个Trigger,SFDC并不保证执行顺序。所以只建一个Trigger就够了。
行8:在Trigger中不要直接写业务逻辑,应该调用handler,并安排好业务执行顺序。
行9:一定要设计某个user跳过Trigger执行的开关,除非你将来不维护它。(方法有很多,其中一个例子
行10:别让维护人员发现没有跳过Trigger开关的系统是你写的。。。如果是的话赶紧逃命吧!

7+

我的Case去哪了?

“求救。用户说看不到Case了。”
“Case看不到了?一条都看不到?如果一条都看不到的话看看Object Permission,是不是Case的CURD都没勾选?”
“看了,CRUD都有。不过。。不是所有Case都看不见。是看不见Owner是自己之外的Case。。。”
“唔。。。我之前说过的,Profile管全表,Role管数据。
“我记得的。。”(唰唰翻笔记中)
“所以,看看OWD是不是设成了Private,并且勾选了Grant Access Using Hierarchies。如果是的话,看看这个用户的Role是不是在最底层。”
“没错,OWD为Private,开启了Using Hierarchy,Role也确实在最底层。并且没有任何Sharing Rule。”
“所以他只能看见自己的Case没毛病啊。”
“好吧。。。那现在用户想看到自己的Account下面的所有的Case怎么办啊?”
“把Case的Owner都改成他不就行了。”(喝口茶)
“囧。”(囧脸)
“你先自己想想。”
“好吧。。。啊!我想到了!用Sharing Rule!”
“嗯哼,继续说。”(坏笑)
“Sharing Rule有两种,一种是Base on record Owner,一种是Base on criteria。”
“没错。那么你打算用哪种呢?”(善意的坏笑)
“当然用Owner。。。等等,不行。我不确定要把哪个User的Case分享给哪个User。Criteria的话。。。我也不能确定Share的条件。”
“那该怎么办呢?”(单纯的善意的笑)
“我把OWD改成Control by parent。”(舒了一口气)
“你确定可以改成Control by parent?”(扬起嘴角)
“我记得Contact是可以的啊,Case应该也可以吧。。。。我确认一下。。。。哎呀,只能Private和Public二选一。”
“那怎么办呢?”(翘起二郎腿抿了口茶)
Apex Sharing!”(两眼放光)
“我说过的,能用标准功能就不要写代码。”(放下茶杯,duang一声)
“可是。。。标准功能也实现不了啊。。。”(委屈脸)
“Case与Account的关系不同于Account与Contact。Case有复数个标准爹。所以对于Case的访问权限控制,需要更加精细的选项。打开那个User的Role。”
“打开了。”
“看没看到一个叫Case Access的东西。”(背手昂头)
“看到了,现在写的是Users in this role cannot access cases that they do not own that are associated with accounts that they do own。哦~~~~~User无法访问自己拥有的Account所关联的不是自己拥有的Case。”
“没错,用户想看到自己的Account下面的所有Case的话,就把这个选项改成View或者Edit就行了。”
“马上去改!”

6+