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+

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进行检索。这是怎么回事呢?

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

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

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

3+

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

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

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

1+

关于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(官方介绍文章)到现在还没推广开呢。

// 未完待续

1+

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”

2+

如何优雅的干掉VF的autofocus

How to remove VF input field’s autofocus?
最近在写StandardSetController Demo的时候遇到了一个逼死完美主义者的问题。
在VF加载之后,SFDC会非常Nice的,热心的,把第一个文本输入框设为焦点。
我推测sfdc的产品设计逻辑是下面这样的。
嗯。。。你设置一个输入框,代表着用户一定得输入点什么才能继续。
既然用户必须输入点什么的话,那么就必须用鼠标点一下这个输入框。
那么让我们来做点什么吧,帮用户先把输入框点上怎么样,节省用户的操作。
看呐,如果用户双手都在键盘上,他们连右手离开键盘的工夫都省下来了,用户一定会非常满意的。

对于大部分业务场景,这个贴心的小设计确实很有用。但有的时候会起反作用。
比如我的StandardSetController的Demo。
在List View画面,一般只有在数据列表最下方才有输入框,为了输入Page Size或者Page No。
但是不碰这几个输入框并不影响我使用List View。
不过,当默认Page Size非常大,画面超出一屏的时候,由于sfdc自动把第一个输入框设成焦点,导致画面滚动条直接被拖到了滚动条最下面。
还有当VF的第一个input项目是Date型的时候,sfdc的自动设置焦点机制,会触发datepicker,就像大晴天自动弹开的雨伞一样,造成另一场灾难。

研究了半天,虽然标准List View并没有这个问题,但还是没有找到比较官方的解决方法。
只好Hack一下了。

在VF中插入

   <script>
        function setFocusOnLoad() {}
    </script>

问题完美解决。

1+

关于Salesforce的15位ID与18位ID

众所周知,Salesforce的Id有15位和18位两种。
18位ID的前15位与15位版本相同。
比如,有一条Account,其URL上的Id为0016F00002Dbbt5。
其中头三位001为Account的prefix(关于prefix,参照《salesforce的prefix和suffix》)
四到六位的6F0为Org Id的第4到6位。由于OrgId的唯一性,所以每个ID在整个SFDC世界都是唯一的。

同样是这条Account,使用工具取得的Id为18位的0016F00002Dbbt5QAB。
前15位与15位版本的ID相同,后3位则是根据前15位计算得来。

插个题外话,Org Id为00D开头的15或18位ID,第四位代表org所属的Instance。
比如说,我的OrgId为00D6F0XXXXXXXXX,那么此org所属的Instance是6对应的NA4。
第四位代码与Instance的对应关系表,请参照《Instance代码对照表
通过这个对照表,可以轻松的通过Org Id知道其所属的Instance。

包括某些官方文档在内,都将15位ID称为15位大小写敏感ID(the 15-character case-sensitive ID),18位ID称为18位大小写不敏感ID(the 18-character case-insensitive ID)。
那么问题来了,假如18位ID大小写不敏感,那是否就意味着
1. 0016F00002Dbbt5QAB
2. 0016F00002DBBT5QAB (全大写)
3. 0016f00002dbbt5qab (全小写)
代表同一条数据吗?
继续阅读“关于Salesforce的15位ID与18位ID”

4+