关于Salesforce的组成

小时候看CCTV播放的纪录片,里面讲抗日战争日期中国空军的事情。
到现在都能很清晰的记起其中一段。
当时是采访一位老兵,说当时根本没有时间进行系统训练,
所有的飞行员都是简简单单练练起飞降落开机枪就上天作战了,
根本没机会像现代空军一样,要系统的学习机械原理,飞机构造之类的才能上天。
简单的比喻就像开车一样,能把车开走不一定需要知道车为什么能开走,引擎是什么烧的,动力是怎么传导的。

做开发也一样。
特别是Salesforce。
作为Salesforce从业者,也许精通admin,玩转401,熟练掌握501。
但不一定需要了解Salesforce平台是由什么构成的。

对于大部分从业人员来讲,了解了这些也不一定对日常工作有什么帮助。更何况还有可能占用宝贵的娱乐时间。
而且虽然作为Salesforce从业者,仍然有大量的英语困难症患者(日语也一样),更成为了止步不前的借口。

其实还是建议所有想在Salesforce平台好好做下去的从业者,认认真真的读一读官方提供的Salesforce基本原理
毕竟想跳的更高,就要蹲的更深。

干了这些年,根据经验总结。
我把Salesforce里面所有的东西分成三类。
第一类是云计算的核心,Metadata元数据
第二类是数据
第三类是设定

Metadata是其中最好理解的,也是最难深入理解的。
有经验的人知道我们对系统做出的大部分(注意,并不是所有)改动都会在Metadata上有所体现。
我们在Deploy的时候,Deloy的具体source也是MetaData文件。
MetaData可以视为是定义了Org中的一切。
老手甚至可以通过手动编辑MetaData让系统变成自己想要的样子。

如果MetaData使用来定义这个系统是什么样子。
那么数据就是这个系统的血肉,真正让系统的存在有了意义。
做Java系统时间长了的人,总觉得系统=代码。
而Salesforce将数据的重要性提到了一个新的高度。
几乎所有的改动都在围绕着数据。
收集数据,处理数据,显示数据。
数据还要分为业务数据,功能数据,系统数据。
业务数据就是系统用户日常业务开展需要数据,客户信息啊,商品列表啊,这些也是开发人员轻易不能动的内容之一。
功能数据,就是为了实现一些功能,需要储存参数的临时表, Custom Setting,或者记录功能执行记录的数据。
这部分数据用户是看不到也不需要看到的,但是又是系统正常运转的必须部分。
最后还有大量的系统数据,这些数据默默的维持着系统的基本运转,其中有一些表甚至不对开发者开放。

在这个世界上, 事情从来不会非黑即白。如果MetaData和数据泾渭分明,世界还是很美好的。
但现实是残酷的。并不是如理想一般,这个东西不是属于MetaData,就是属于数据。
在Salesforce里面,存在这大量双轨制的东西。
比如Profile,Role,Apex,VF等等,
这些既存在于MetaData,又存在于数据中。这样就等于赋予了这些东西两种截然不同的属性。
MetaData和数据的根本区别在于什么呢?
MetaData是定义,可以通过工具部署。是骨架。
数据不需要部署,可以直接导入,直接修改。是血肉。
这些东西就如粒子一般,拥有波粒二象性。
这里跑题太严重,就不展开了。

除了MetaData和数据以外,还有一些看得见摸得着的东西,不属于任何一种。
我称呼它们为设定,它们拥有这最顽固的性质,
除了手改,无法部署,不能导入。而且存在于各个角落。
最复杂的情况比如,在Profile本身已经是双规制。
结果Session Timeout设置和Password Policies这个两个俨然应该隶属于Profile的设定,
却不存在于MetaData和数据中任何一方。
甚至Profile的Copy功能也会将这两个设定无视掉。

具体分类的名单,等整理完之后再发吧。

// Upadate1

我还是想简单了,其实Metadata和数据的界限要更模糊一点。
有些我原来以为只能通过Metadata修改的东西,最后在sfdc后台其实还是一张表。

Salesforce的SplashPage(启动页)-一天一个标准功能系列

最近有小盆友考验我,说客户想点进Tab的时候先进入一个Guide页面,然后点击继续进入list页面,点击Don’t show again下次不再显示此页面,直接进入List页面。
问我该怎么做。

我反问,”你想怎么做?”
“首先呢,建一个Tab”
“废话。哪种Tab。”
“当然是Visualforce Tabs。”
“然后呢。”
“然后我创建一个VF,并且使用标准标签和样式。”
“嗯,为了让用户觉得自己没跳进VF是么?”
“没错,然后我在VF里选中用户点的Tab,之后在页面里写上我要的内容。”
“这个简单,那俩按钮你想怎么实现呢?”
“当然是建两个Button,然后在Controller里实现两个Action。一个Button直接跳转到List页面。另一个Button。。。。”
“Don’t show again button你想怎么实现呢?”
“我在User表上加一个字段,如果用户点了Don’t show again,我就更新一下User身上的Flag,然后在VF初始化的时候就判断User身上的Flag,如果是true就直接跳转。”
“唔。。。可行,你觉得复杂么?”
“还好吧。”
“听说过Splash Page么?”
“啥?”

Splash Page,启动页,醒目页面。是Salesforce提供的标准功能之一。
其作用是在Tab上添加一个启动页面,并且原生支持Don’t show again。
其效果如图

点击Tab之后,会显示指定的内容,并提供Don’t show again与Continue按钮。
不过,我发现只有Custom Object的Tab才可以指定Splash Page,标准Object的Tab不可以。
可能Salesforce觉得标准Object应该不会像Custom Object那样复杂到需要启动页来介绍功能吧。

那么接下来介绍该如何创建一个启动页。

首先,你需要创建Custom Link,此Custom Link为Home的Custom Link,而不是该Custom Object上的Custom Link。

创建的过程与创建button或者link相同,种类有三种,JS,VF或者URL。

然后随便创建一个VF,在此选择。

值得注意的是,一定要记得关闭标准标签和标准样式,否则就会出现像下面这样奇怪的事情。

关闭标准标签标准样式的写法。

<apex:page showheader="false" standardStylesheets="false">
......
</apex:page>

Custom Link设定好之后,去Tab的设置页面,指定这个Custom Link,大功告成。

Salesforce作为云计算平台,提供了强大的自定义开发能力。但是,现代应用开发思想是轻开发,重配置。常年从事SI的前JAVA从业者,很容易就陷入开发解决一切的思维黑洞。付出高昂的成本反复造轮子,因为其价值也只是在反复造轮子上了。利用标准功能快速的实现客户需求,才是未来的发展方向。大型开发的市场会一直存在,但将将来绝对不会是主流。

Salesforce中Picklist与翻译的故事

不同与Java系统,salesforce给Select或者Drop down,whatever你叫它什么的那个元素提供了一个很好的组织方式———-Picklist。

Picklist作为一种基本数据类型,在salesforce里任何人都可以使用其轻松的创建下拉列表,并动态的控制其中的选项。要知道,在java系统里,为了达到这个目的,可是要上三大框架的。如果不能用自定义标签库,那么在不hard coding的前提下,在画面上可控的显示一个select对于一个新手来讲,将是一个堪比穿越撒哈拉沙漠的挑战。当然,大部分项目都选择直接写死,或者使用Js操纵DOM。

不过Salesforce的Picklist与我们印象中的Select元素还是不同的。

在Spring 17之前(Spring 17 release note),picklist的值只有一个value,所以表示文本也是它,值也是它。所以好多系统就顺水推舟的在代码,validation rule等地方直接用长长的value内容做判断。
突然有一天,客户想对选项的内容做一个小小的改动。。。。。灾难了。
以前的话,更改选项的值,既存数据是不会跟着变的,要手动修数据。现在已经能够做到既存的数据自动跟着变了。但是那些代码里写死的判断等就不行了。

我们知道,html中的select是要分别设定value和label的!在页面表示label,表单里传的是value。这样根据定义好的数据字典,在程序里随便判断value。无论画面表示的本文怎么改都不会影响到程序。

所以sfdc的先贤们发明了一个绝招————翻译。

比如picklist的一个项目的value是1,则在翻译控制台将1翻译成想显示的文本,比如“A”。那么就实现了HTML的Select的特性—–显示的文本和后台传值分开。

正当群众们洋洋得意的时候,Spring17来了。给我们带来了picklist选项的Api Name,参见官方博客

抱歉,说的不严谨,应该是给我们带来了Picklist选项的Label。

就是说现在Picklist的选项有两个设定值值,一个是Label,一个是Api Name。Api Name其实还是我们以前熟悉的那个选项value,老朋友。新增加的是Label。

那么好玩的地方来了。以前Picklist选项只有一个value的时候,翻译就直接那么翻译了。当Label和Api Name分开之后,翻译是针对谁来进行翻译呢?

这里直接公布答案—–是Label。

可以用下面的代码进行尝试。

 
设定好翻译之后

List<Schema.PicklistEntry> picklistValues = SomeSobject__c.SomePicklistField__c.getDescribe().getPicklistValues();
for (Schema.PicklistEntry pe: picklistValues) {
    system.debug('PicklistEntry Value:' + pe.getValue() + ' Translated Label:' + pe.getLabel());
}

value既Api Name,Label既Label

这样,我们是可以做到在Apex中获取Picklist选项的Label,而且获得的还是翻译后的Label。
可曾记起我们还有Formula呢,isPickVal函数判断的又是谁呢—————没错,还是老朋友,Api Name。
目前我还没有找到如何在formula里获取Label名的方法。

Salesforce怎么从生产环境删除Apex

一般来讲,就算是DevOps团队也很难遇到需要删除Apex的情景。
毕竟在一个系统的生命周期里,大部分时间都是不断的增加功能,或者改动现有功能。
仅剩的一点点的遗弃功能的需求,也会因为考虑到风险问题,从而选择将废弃代码放在那里不再调用。
毕竟,维护团队的准则————不动就没事,动了就要担风险。

其实,还有一种需要删除Apex操作的情景,就是回滚。
Salesforce与常规的Java项目不大一样,如果上线出现问题,很难做到完美回滚。
曾经有人很天真地和我说,将生产环境的MetaData全部备份,如果上线出问题就全都deploy回去。
java web系统确实可以这么做,一个新版war包,一个旧war包,出问题就停机覆盖回去,一切就都恢复如初了,完美。
可惜,如果Salesforce这么做就会面临一场巨大的灾难!
首先,你有很大的几率会彻彻底底的失败。因为Salesforce在deploy的时候要计算metadata之间的关系。
你会因为各种狗追尾巴似的关系依赖,导致失败,继而崩溃。
还有一些永远无法被正确Deploy上去的metadata,会将你的生产环境毁成一片废墟。
在Salesforce里,只要是稍微复杂一点的Org,就永远不要幻想将全部的Metadata都一口气deploy上去。

话说回来,Salesforce最安全的回滚方式还是逆操作。就像录像带的倒放。
但是,就算是逆操作,仍然不能保证安全的,原原本本的回到从前,尤其是回滚包含修复数据动作的时候。
所以在上线之前,针对回滚,认认真真地制定一个回滚方案是非常,非常有必要的。
当然,如果你每次上线只是加一个Field,当我没说。
不过,就算是只删一个Field的话,慎重吧。

回到正题,怎么去删除一个Apex。
有的同学说了。直接删就好了啊。
那么。。。只接触过Developer环境和Sandbox的可以出门右转了。生产环境是无法直接删除的。

那么,下面,先讲一个简单的方法。

首先,我们有一个ApexClass,叫DeleteSample。

然后在ForceIDE中,新建一个Project,将其MetaData取下。

之后打开此Apex,之后切换到Metadata标签上。

然后将Status从Active改成Deleted

然后,保存,在Apex上点右键,Force.com -> Deploy to Server。

之后填入用户名密码,下一步,强调一下,被删除的Apex会被标记成Overwrite,且默认不被选中。如果是新建的代码则会被标记成New,绿色,自动勾选。修改的代码被标记成Modify,黄色,自动勾选。这里,一定要记得钩选上。

然后无脑下一步,直到看到Success。

然后回到系统里,就发现DeleteSample已经不见了!

就这样。简单吧。

 

Salesforce用Apex判断Role Hierarchy的简单代码示例

(上密码遭人恨。。。。先开放好了)

由于role不同于Profile,带有阶层性质,所以有一些自定义功能要依赖于这种阶层的设定。这样就涉及到role hierarchy的判断问题。

我是一个绝懒之人,所以去网上搜了一下,能找到的方案都或多或少有些缺陷 。

我所提供的方案也是如此,但是想比于浪费太多SOQL查询次数来讲,role的数量不超过50000条已经是足够好了。
// 这里Update一下,其实根本不会有那么多的Role,因为默认500,提票才能达到10000。

Talk is cheap, show you the code.
PS:最近正在建设个人代码库,本身也只是一个简单的示例,之后会放出完全体版本。
也许也会放到Github上。

// @Version 0.1 Author Keal. Email: [email protected]
// @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
public class RoleHierarchyHelper {
	Boolean isEnd = false;
	Boolean compareResult = false;
	public static List<UserRole> roleList = [SELECT Id, Name, DeveloperName, ParentRoleId FROM UserRole LIMIT 50000];
	
	public RoleHierarchyHelper() {
		// pretend Salesforce spasms LOL. It happened.
		if(roleList == null || roleList.size() == 0) {
			roleList = [SELECT Id, Name, DeveloperName, ParentRoleId FROM UserRole LIMIT 50000];			
		} else {
			system.debug('=======RoleList.size()========' + roleList.size());
		}
	}
	
    public Boolean isSubordinate(String currentRoleId, String compareRoleId){
    			
	    List<UserRole> childRoleList = new List<UserRole>();
	    Map<Id, UserRole> childRoleMap = new Map<Id, UserRole>();
	    Set<Id> exeUserRoleIdSet = new Set<Id>();
	    exeUserRoleIdSet.add(currentRoleId);
	    
	    isEnd = false;
	    compareResult = false;
    	while(!isEnd) {
			childRoleMap = new Map<Id, UserRole>();
	    	for(UserRole ur: roleList) {
	    		if(ur.ParentRoleId != null && exeUserRoleIdSet.contains(ur.ParentRoleId)) {
	    			childRoleMap.put(ur.Id, ur);
	    		}
	    	}
	    	
			if(childRoleMap.size() > 0) {
				if(childRoleMap.containsKey(compareRoleId)) {
					isEnd = true;
					compareResult = true;	
				} else {
					isEnd = false;
					exeUserRoleIdSet = new Set<Id>();
					exeUserRoleIdSet.addAll(childRoleMap.keyset());			
				}
				
			} else {
				isEnd = true;
				compareResult = false;
			}
    	}
    	return compareResult;
    }
    
    public Boolean isSuperior(String currentRoleId, String compareRoleId) {
    	Map<Id, UserRole> idUserRoleMap = new Map<Id, UserRole>(roleList);
    	Id exeUserRoleId = currentRoleId;
    	UserRole exeUserRole = new UserRole();
    	
	    isEnd = false;
	    compareResult = false;
    	while(!isEnd) {
    		exeUserRole = idUserRoleMap.get(exeUserRoleId);
    		if(exeUserRole.parentRoleId == null) {
    			isEnd = true;
    			compareResult = false;
    		} else {
    			if(exeUserRole.parentRoleId == compareRoleId) {
	    			isEnd = true;
	    			compareResult = true;
    			} else {
	    			isEnd = false;
	    			exeUserRoleId = exeUserRole.parentRoleId;
    			}
    		}
    	}
    	return compareResult;
    }
    // TODO isSameLine()
}

Salesforce中避免重复执行Trigger的思路

题外话,换了效力的公司之后,对自己的冲击还是挺大的。所以一直没有精力继续写。不过,还是一鼓作气吧。

第一种思路:在trigger中声明一个Static变量,默认为false,如果执行了业务代码,在最后将此变量值执为True,然后在一个事务中,下次再进来的时候,由于此变量为true,跳过。

但是由于static的机制并不是很稳定,有可能造成意想不到的问题。而且,在早些年版本的时候,一个批次200条数据,salesforce会分两次执行。这样后100条就会被无情的跳过。

第二种思路:在目标object上新建一个隐藏的布尔字段,默认为false,第一次执行的时候将此字段的值更新成true,然后在事务结束的时候使用future方法重新更新为false。

超大量导入数据的时候应该会死。。。

第三种思路:新建一个static class,class里面包含一个set,然后每次事务开始时将自己放入此set,事务结束时remove掉。

仍然有static诡异行为或者直接失效的风险。

所以,在表设计和业务结构设计的时候还是要尽量发生重复触发的情景。让Trigger只关注与自己要做的事情就好。

之后会给出每种思路的验证结果。

// Update 1 by 2017/03/04

应群众要求,解释一下思路一。
思路一的核心就在于static修饰符。官方文档
熟悉高级编程语言的同学一定对static不陌生,比如在java中static经常与public final static连用。
Salesforce同样提供了static概念,不过于java中的static有些不同。
1. static的生存范围仅限一个transaction。所以幻想着初始化一次到处使用的,洗洗睡吧。
2. 虽然不能做到亘古不变,但是在一个transaction内穿梭于多个instance之间还是可以的。
同样,可以在class中的static代码块中进行复杂的初始化,也是能避免重复执行。

所以作为思路一的解决方案。就如同官方文档提供的例子一样。
首先找一个class声明一个flag。

public class P { 
   public static boolean firstRun = true; 
}

然后在业务代码里使用它。

trigger T1 on Account (before delete, after delete, after undelete) { 
       if(Trigger.isBefore){
          if(Trigger.isDelete){
             if(p.firstRun){
                 Trigger.old[0].addError('Before Account Delete Error');
                  p.firstRun=false;
              } 
           }
        }
}

如何在Salesforce中使用匿名块建立每小时执行一次的Scheduler

在Salesforce中建立Scheduler的方式有两种

  1. 在Develop->Apex Classes页面,点击Schedule Apex按钮之后会提供如下的面板。

    这个面板虽然能方便的决定Scheduler执行的频率,但是最高频率也只能是一天一次。
  2. 再有就是使用Apex。在匿名块中使用系统函数设定Scheduler,可以获得更高的频率。

所以,如果想设定一小时执行的Scheduler就必须使用第二种方式。
需要使用的函数为System.schedule();
官方文档对此进行了详细的描述。具体参照下列示例代码。

// Sample 1 hour 1 time
System.schedule('My Scheduler','0 0 0/1 * * ?', new MyScheduler()); 

作为一个成熟的完善的系统,定时器与后台Job是必须的组成部分。
Scheduler就是Linux Cron的强力山寨。
所以System.schedule()中的第二个参数,就等同于Cron表达式(当然,只能算一个子集)。

为什么要把一小时一次单独拎出来,因为,这是Scheduler的最小粒度。
Scheduler表达式的秒位与分位是不支持“/”符号的。

所以,想几分钟执行一次的,放弃吧。

Salesforce使用truncate清空数据库

如果想快速的清空MySQL中的表,可以使用Truncate命令。
Truncate能够快速的,对数据进行无差别的清空。

在Mysql中使用truncate的语法是
TRUNCATE TABLE [Table Name];

在Salesforce中同样提供了Truncate功能。不过只是提供一个按钮,并不提供任何代码及API调用Truncate的方式。
所以正确的应用场景,应该只是用来快速的清空临时数据或者测试数据。毕竟,无论使用Batch还是dataloader来删除全部数据都要花费大量的时间。

在Salesforce中想要看到Truncate按钮的话,需要达成如下条件,参考官方文档:

  1. Profile -> Administrative Permissions -> Customize Application -> True
  2. User Interface -> Setup -> Enable Custom Object Truncate -> True

然后在到Custom Object的详细画面,就能看到在Edit,Delete按钮旁边Truncate按钮出现了。

不过,并不是任何Object任何情况都能进行Truncate,有如下要求,参考官方文档:

  1. 不能被别的Object Lookup,或者处在Master-Detail中的Master地位。
  2. 不能被Report Snapshot参照。
  3. 不能有自定义字段是Index或者是External ID。
  4. 不能开启了skinny table。(关于Skinny Table的事情会另外写一篇)。

为什么会有如上的限制,显而易见,与Salesforce中Truncate的动作有关。
在Salesforce中,虽然Truncate功能与MySQL类似,用来清空表中的数据,但是会导致Custom Object的Id Prefix产生变化。所以所有通过三位Id Prefix引用此Custom Object的功能都会失效。如果Apex/VF/Button中有直接使用3位Id Prefix进行判断的逻辑,就会受到影响。

除此之外,另一点与MySQL不用的是,如果有一个自增字段。MySQL在Truncate后,会重置自增字段的计数器。而Salesforce则不会。没错,虽然ID变了,但自增字段会接着数并不会重新回到起始值。