关于window.postMessage

很久没写东西了,偶尔会对着本文编辑器发呆,不知道自己想写什么,不知道应该写什么。又觉得什么都不应该写,什么也都不值得写。能静下心坐在电脑前写点什么是越来越奢侈的事情。也许,too many mind。

我自己说的

我相信每一个接触过前端开发的程序员都被跨域问题折磨过。

那么什么是跨域问题?
经历过的人脱口而出,“跨域问题就是处于安全考虑,当前域名不允许访问另一个域名的资源”。
这样说其实并不正确。仔细回忆一下,在曾经访问过的网页中,是否包含非本域名的CSS,JS和图片?
实际上获取这些CSS,JS和图片的请求已经跨域了,可是并没有出错。

CORS定义 点这里。严谨点说,是只有通过脚本发出的跨域请求才会被禁止,而通过标签发出的资源请求则可以顺利完成。并且进行拦截动作的,是浏览器本人。
原因显而易见————脚本运行在浏览器,而不是服务器。所以通过服务器,或者非浏览器的客户端(或者魔改版浏览器),可以随意的向任意域名发出请求。
为了绕开这个机制,各路神仙也是各显神通,开了各种各样的脑洞。

继续阅读“关于window.postMessage”

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改成先查询后循环的方式就结案了呢。
不,我们最后把这个表格改成了异步加载。。。。

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”

关于编辑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能尽早考虑实现文档的召回机制。囧

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开关的系统是你写的。。。如果是的话赶紧逃命吧!

我的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就行了。”
“马上去改!”

Salesforce的Number类型的小坑

我们知道,在Salesforce中,Number型的字段,整数位数与小数位数加一起,最多18位。
如果我们不要小数位,则可以存放18位整数。(18, 0)
假设这个整数为123,456,789,012,345,678的话,
来,跟我一起念,
十二京三千四百五十六兆七千八百九十亿一千一百二十三万四千五百六十七
这数字有多大,很大。

接下来,我们在画面输入这个数字。
然后在Developer Console里查询一下这个字段会发生什么呢?

咦?最后两位怎么回事?

好吧,我们在where语句里加一个条件,看看到底数据库里存的数字到底是多少。

Unknown error parsing query?解析query出错了?
马上拿去咨询了大神一下,大神淡淡的回答道:“加个.0”。

果然加.0就可以了!
这里推测是SFDC的SOQL解析器的锅。

我猜SFDC的SOQL解析器是这么处理的——————————
解析器在拿到字符串”123456789012345678″之后,
首先找一下包不包含小数点。
如果不包含小数点,则放到int里,
包含小数点,则放到double里。
由于SFDC里的Integer与JAVA里Int的最大值(INT_MAX)一样,都为2,147,483,647。
所以String to Int失败。

下面我们来验证一下这个脑洞,如果确实是遇到不带小数点的数字就放int里,那么理论上2,147,483,648就也会报同样的错误。
而2,147,483,647则不会报错。

脑洞证实。
所以当number字段里的值大于INT_MAX的时候,我们不得不手动加一个.0来告诉解析器,“把我放double里,谢谢。”
不过在Apex里倒是应该不会出现这个问题,因为你要查询一个大于INT_MAX的数字,首先就不能用Integer型的变量。。。

那么,Unknown error parsing query问题解决之后,我们也发现虽然查询结果显示的是123456789012345680,但仍然需要使用123456789012345678进行检索。这是怎么回事呢?

我换了一个工具进行检索。

原来由于位数太大,被科学计数了。

总之。虽然某些国家的货币金额之类的可能数字比较大,但能应用到这么多位数的情景应该不多,
能用其他类型就尽量使用其他类型,以免掉坑。

惊!List型Custom Setting惨遭SFDC抛弃,上位者居然是……

一夜之间,List型Custom Setting惨遭抛弃,SFDC发布冷血声明。
上位者从默默无闻变成人尽皆知。
SFDC开发者不禁议论纷纷。
到底是人性的扭曲,还是道德的沦丧。
接下来请看————————————谁杀死了List型Custom Setting?

继续阅读“惊!List型Custom Setting惨遭SFDC抛弃,上位者居然是……”

关于Stub API

Spring’17,Salesforce为开发者提供了一个强大的工具,Apex STUB API

看完官方文档提供的例子之后,感觉确实很美好,但又觉得没什么机会用。
不同于Java项目对框架和设计模式的极致追求。
我接手的Apex代码均为意识流散文诗式写法。根本没有抽象化,解耦等操作。

Stub Api是基于Java的Mocking Framework
mockito开发的。
使用Mocking Framework的前提是代码必须进行解耦,就是所谓的依赖注入(Dependency Injection)。
要求代码不能按业务直接写成流水账,而是将模块之间的强依赖解开。

比如,A类的构造强依赖与B类,如果没有B则没有A。

A a = new A();
//-----------------------------------
Class A {
    private B b;
    public A() {
        b = new B();
    }
    public String getName() {
        return b.getName();
    }
}

Class B {
   public String getName() {
       return "I'm the B";
   }
}

那么,问题来了,如果我有一个C类,返回“I’m the C”,我就要为C类新建一个A1类,在A1类中将A1与C进行强依赖。
如果需求越来越多,代码也随时越来越膨胀。
那么怎么改变这个局面呢?

B b = new B();
A a = new A(b);
-----------------------------------------------
Class A {
    private CustomObject co;
    public A(CustomObject cObj) {
        this.co = cObj;
    }
    public String getName() {
        reutrn co.Name;
    }
}

Class B extends CustomObject {
    public String getName() {
        return "I'm the B";
    }
}
abstract Class CustomObject {
    abstract public String getName();
}

这样一来,A和B的强依赖就解耦了,不再是每次new一个A,就必然得到B的内容,而是根据我传进去的B来决定A返回的内容。
如果想返回“I’m the C”,就不用再去新建一个A1类,想返回“I’m the D”,不用再去新建一个A2类。

只有依赖注入的写法才能进行方便的Mocking。

Stub Api可以做到的是,在解耦之后,为A类mock一个B类,C类。做到完美的单元测试(模块隔离)。
Whatever,用不上。至今还没见到过有控制反转思想的的Apex。
说句题外话,apex-enterprise-patterns(官方介绍文章)到现在还没推广开呢。

// 未完待续

Salesforce的StandardSetController使用Demo

先放Github地址
https://github.com/Kealthals/Salesforce-StandardSetControllerDemo.git

事情起因,是有人问我为什么StandardSetController无法保存Selected状态。
我觉得既然标准List View可以,那么使用StandardSetController应该也可以。
虽然这个Class我用的也不多,但还是动手写个一个小Demo验证了一下我的想法,虽然花了些小心思,但证明确实可行。
验证完毕,转念一想,既然已经动手了,就干脆把所有的methods都演示一遍吧。权当给自己留一个财富。

StandardController大家都比较熟悉,用来处理单条数据。模拟的是Create/Edit页面和Detail页面。
StandardSetController用的不多,因为模拟的是List View页面。标准List View页面已经很强大了,很少会遇到这种需求。
这个Demo也就是尽量做一个和标准List View类似的页面,时间有限,肯定做不到标准页面一般的好看和完美。
最低目标是将这个Class提供的method都使用一遍,除了一个不明所以的method之外,最低目标我觉得是完成了。
功能上,也提供了基本的
1. 罗列数据
2. 翻页
3. 选择数据(Selected)
4. 选择List View
5. 改变Page Size
6. 跳转指定页
7. Inline Edit(还不完美)
作为一个独立功能来讲,勉强达到了凑合着用的程度。
在功能实现的时候,又不断遇到了一些或新或旧的小问题,在大家的帮助下都找了比较好的小方案解决掉了这些小问题。
比如SFDC的autofocus,inputtext的输入类型限制等等。

继续阅读“Salesforce的StandardSetController使用Demo”