编写更优质的Apex代码
1. 使用额外的DML选项进行微调
Database.DMLOptions 实例可与DML操作一起传递,以提供微调所需的额外配置。这些额外配置包括:
- 全有或全无行为 :控制操作是否允许部分成功。如果为 true ,当任何记录导致错误时,所有更改都将回滚。
- 字段截断 :默认情况下,如果字符串值太大,DML会失败。可以通过将此属性设置为 true 来调整此行为。
- 分配规则使用 :告知对潜在客户和案例的DML使用特定的分配规则。
- 电子邮件行为 :控制在DML期间的各种事件发生时是否应发送电子邮件。
以下代码片段展示了如何使用 DMLOptions :
// DML语句
Database.DMLOptions dmlOpts = new Database.DMLOptions();
dmlOpts.allowFieldTruncation = true;
Account acc = new Account(Name = 'Abc');
acc.setOptions(dmlOpts);
insert acc;
// 通过数据库方法进行DML
Database.DMLOptions dmlOpts = new Database.DMLOptions();
dmlOpts.allowFieldTruncation = true;
Account acc = new Account(Name = 'Abc');
Database.insert(acc, dmlOpts);
2. DML操作期间的错误处理
DML指的是诸如插入、更新、删除等语句。这些语句由于各种原因容易失败,部分原因如下:
- 标记为必需的字段未填充。
- 对象上的自定义验证规则。
- 用于验证的触发器逻辑。
如果处理标准对象,要特别注意错误处理,因为代码部署的目标组织可能存在上述任何原因。例如,客户可能会创建新的验证规则。
重要的是要优雅地处理此类未知错误,这就是错误处理发挥作用的地方。理想情况下,DML应该在 try-catch 块中执行,记录错误、处理错误并将错误信息优雅地返回给用户。以下是一个示例代码:
try {
insert new Account(Name = 'XYZ corp');
} catch(DmlException dmle) {
ApexPages.addMessage(new ApexPages.message(ApexPages.severity.ERROR,'Failed to save Account, because of : ' + dmle.getMessage()));
}
不同场景下的错误处理方式如下:
- 触发器 :应停止触发器流程,并将操作回滚或作为错误返回给用户。停止给定记录的触发器执行流程的方法是使用 Sobject.addError() 方法。例如:
trigger AccountValidator on Account(before delete) {
try {
Account_Inovice__c[] childObjRecords = [Select Id from Account_Inovice__c Where AccountId__c IN: Trigger.newMap.keySet()];
if (!childObjRecords.isEmpty()) delete childObjRecords;
delete childObjRecords;
} catch(DmlException dmle) {
for (Account a : Trigger.new) {
a.addError(dmle);
}
}
}
- Apex SOAP/REST Web服务 :创建一个类型或JSON/XML响应,指示错误并包含所需的上下文信息。
3. 解析Apex描述信息
Apex提供了丰富的API来处理sObject元数据并编写可扩展的代码。以下是一些使用场景:
- 获取sObject和相关字段的信息 :通常在Visualforce的通用规则引擎中需要,该引擎允许用户从组织中所有可用的sObject中选择一个,并根据该sObject的可用字段创建规则。以下代码展示了如何生成用于在VF页面下拉窗口中显示的字段列表:
// 组织中所有对象的映射
Map<String, Schema.SObjectType> gd = Schema.getGlobalDescribe();
// 如果类型在编译时已知,可以使用 Schema.SObjectType.Account
Schema.SobjectType accType = gd.get('Account');
// 如果类型已知,另一种获取字段映射的方法
Map<String, Schema.SObjectField> fldMap = accType.getDescribe().fields.getMap();
// 在VF页面下拉列表中显示的选项
List<SelectOption> options = new List<SelectOption>();
for (Schema.SobjectField fld: fldMap.values()) {
Schema.DescribeFieldResult fldDesc = fld.getDescribe();
options.add(new SelectOption(fldDesc.getName(), fldDesc.getLabel()));
}
- 基于字段名访问/更新sObject记录 :在某些场景下,字段名在编译时未知,此时可以使用
sObject系统类的get()和put()方法。示例代码如下:
public void updateOpp(Opportunity opp, String fldName, Object val) {
// 如果值不同则更新
if (opp.get(fldName) != val) {
opp.put(fldName, val);
}
}
- 从ID获取SobjectType :在Winter ‘13版本引入
ID.getSObjectType()方法之前,实现此需求需要编写大量代码。 - 动态实例化类 :有时,所需的类名在编译时未知。例如,一个托管包提供运输税计算功能,但希望让客户提供自己的税计算逻辑。可以使用Apex的
Type类来实现,示例如下:
// 定义税计算的接口
global interface I_TaxCalculator {
Decimal calculate (String itemSku, Integer quantity, String address);
}
// 托管包中的默认税计算器
public class DefaultTaxCalc implements I_TaxCalculator {
public Decimal calculate (String itemSku, Integer quantity, String address) {
// 一些默认的税计算
return 1;
}
}
// 托管包的税计算逻辑
public static Decimal calculateTax(String taxCalcClassName, String itemSku, Integer quantity, String address) {
I_TaxCalculator calc = null;
if (taxCalcClassName == null) {
calc = new DefaultTaxCalc();
} else {
Type typ = Type.forName(taxCalcClassName);
calc = (I_TaxCalculator)typ.newInstance();
}
return calc.calculate(itemSku, quantity, address);
}
// 目标客户组织中的自定义实现
global class IndiaTaxCalc implements I_TaxCalculator {
global Decimal calculate (String itemSku, Integer quantity, String address) {
// 一些自定义的税计算
return 0;
}
}
// 上传的托管包代码示例
TaxCalcConfig__c custSett = TaxCalcConfig__c.getInstance('Default');
String taxCalcClassName = custSett.Class_Name__c;
Decimal tax = TypeForName.calculateTax(taxCalcClassName, 'SKU-234', 2, 'New Delhi, India');
4. 编写更好的触发器
编写Apex触发器时很容易出错,关键在于理解批量数据处理、重用SOQL调用以及理解触发器的事件流程。
- 理解触发器的执行顺序 :除了触发器,DML操作还会触发一些其他事件,包括验证规则、工作流、审批、分配规则等。了解这些事件的顺序以及它们之间的相互影响对于编写优秀的触发器代码至关重要。建议阅读相关指南。
- 编写处理批量数据的触发器 :不要假设 Trigger.new 或 Trigger.old 等上下文变量中只有一条记录,因为以下因素可能导致批量操作:
- Visualforce页面允许同时对多条记录执行DML操作。
- 同一个sObject可能作为Web服务(SOAP/REST)公开,接受多条记录作为DML操作的输入。
- 使用数据加载器填充sObject。
如果编写了类似以下带有硬编码索引引用的代码,肯定会遇到问题:
trigger AccountTrigger on Account (after insert) {
Contact[] cs = [SELECT Id FROM Contact WHERE AccountId = Trigger.new[0].id];
…
}
推荐阅读以下关于此主题的内容:
- Trigger and bulk request best practices
- Using maps and sets in bulk triggers
- 一个sObject上的多个触发器 :在同一个sObject上创建多个触发器文件很容易,但如果DML事件相同,处理优化、事件顺序和流程可能会很困难。每个sObject使用单个触发器文件是一个好的做法,原因如下:
- 可以一次性执行相同或相关事件的SOQL查询和DML操作。
- 使用单个触发器文件可以更好地控制事件流。
- 避免触发器递归。
以下是一些推荐的触发器模板:
- Gokubi’s template
- Mike Leach’s template
- My template based on the builder pattern
5. 在Apex中处理XML
Apex中处理XML有几种选择,各有优缺点,如下表所示:
| 选项 | 详情 | 优点 | 缺点 |
| — | — | — | — |
| XMLStreamReader 和 XMLStreamWriter | 基于Java StAX的系统类,是最古老的XML解析选项。更多详情见 链接 。 | 适合处理大型XML文件,因为它是基于令牌的流式解析器,解析大XML时占用的堆内存较少。 | 解析XML需要大量复杂的代码行。 |
| XmlDom | Force.com团队开发的基于Apex DOM的XML库,是 XMLStream 类的包装器。更多详情见 链接 。 | 为复杂的Apex XML流API提供简单的DOM接口。 | 由于是用Apex编写的,如果解析中等大小的XML,会消耗大量脚本语句。该库已被弃用,建议使用下面讨论的 Dom.Document 类。 |
| DOM类 | 系统类,通过 Dom.Document 和 Dom.XmlNode 两个核心类提供原生DOM功能。更多详情见 链接 。 | 是目前Apex中最好的XML处理库,为读写XML提供简单的DOM访问。 | 与其他DOM库(如W3C DOM模型)略有不同,可能需要一些时间来学习和使用。更多详情见 链接 。 |
| Fast XML DOM | Apex类,包装系统DOM类以暴露知名的W3C DOM模型。更多详情见 链接 。 | 确保开发人员可以利用现有的W3C DOM解析API知识快速提高工作效率,还简化了XML中的命名空间处理。 | 目前没有明显缺点,社区反馈良好。 |
6. 在Apex中处理JSON
Apex中处理JSON的选项如下表所示:
| 选项 | 详情 | 优点 | 缺点 |
| — | — | — | — |
| JSONObject | 开源的Apex类,在Apex推出原生系统类之前是处理JSON的唯一选择。更多详情见 链接 。 | 几个版本之前是解析JSON的唯一选择。 | 消耗大量脚本语句,并且存在一些稳定性问题。由于Apex有了名为 JSON 的原生系统类,该类已被弃用。 |
| JSON | 系统类,提供简单的JSON生成和解析API。更多详情见 链接 。 | 是处理JSON的最佳API,支持Apex类型的直接序列化/反序列化,从Summer ‘12版本开始支持处理松散类型或无类型的JSON。 | 目前没有缺点。 |
7. Apex的打包考虑
上传托管发布包时,需要考虑以下几点:
- global关键字 :很少使用该关键字,因为一旦类或方法被标记为 global ,在后续版本中其可见性不能降低为 public 或更低。
- 接口和虚拟/抽象类 :不能在接口或类上传到托管发布包版本后向接口添加方法或向类添加抽象方法。如果托管发布包中的类是虚拟的,添加到该类的方法也必须是虚拟的,并且必须有实现。
- final关键字 :可以移除该关键字,但不能将其添加到全局类中。
8. API版本
每个Apex类都有一个API版本,它与Force.com版本的可用功能相关。例如,如果要使用Winter ‘13的功能,API版本至少应为26。
9. 更改API版本
可以通过以下两种方式更改API版本:
- 在浏览器中编辑类 :如果通过 Setup | Develop | Apex Classes 在浏览器中打开Apex类进行编辑,点击 Version 列下的选择列表进行所需更改。
- 在Force.com IDE(Eclipse)中编辑类 :在Eclipse中更改Apex类版本同样容易,只需注意打开类下方的 Metadata 选项卡,然后将 <apiVersion> 标签值更改为所需的API版本并保存。
10. 重要提示
- 如果创建新的Apex类,强烈建议使用最新可用的API版本,这样可以启用Apex类中的所有最新Force.com功能。
- 如果编辑旧的或现有的Apex类,更改Apex版本(即升级版本)可能有风险。更改后务必进行适当的测试。
11. Apex测试提示
- 将测试数据与组织数据隔离 :这意味着创建自己的测试数据,而不依赖组织中任何记录的存在。这有助于使测试用例在不同组织之间具有可移植性。从API版本24.0开始,组织中的现有数据在测试用例中不可用。但在极少数情况下,可以使用
@isTest注释在类和方法级别启用此行为。只需用IsTest(SeeAllData=true)注释标记测试类或方法。更多详情见 链接 。 - 使用不同的配置文件进行测试 :默认情况下,Apex以系统模式运行,不考虑当前用户的权限和记录共享。应用程序可能有多个具有不同数据可见性、共享和CRUD/FLS行为的配置文件,建议通过
System.runAs()方法模拟这些情况。该方法允许使用给定用户的安全设置和权限运行测试。更多详情见 链接 。
编写更优质的Apex代码
12. 总结与实践建议
为了编写更优质的Apex代码,我们可以将前面提到的各个要点进行总结,并给出相应的实践建议,以下是一个总结表格:
| 技术点 | 核心要点 | 实践建议 |
| — | — | — |
| DML选项微调 | 使用 Database.DMLOptions 实例提供额外配置,如全有或全无行为、字段截断等 | 在进行DML操作前,根据需求合理设置 DMLOptions 的属性,避免不必要的错误 |
| DML错误处理 | 在 try-catch 块中执行DML,不同场景采用不同处理方式 | 养成在DML操作时使用 try-catch 的习惯,针对触发器、Web服务等场景编写合适的错误处理代码 |
| 解析Apex描述信息 | 利用Apex API获取sObject和字段信息、基于字段名操作记录、从ID获取SobjectType、动态实例化类 | 在通用规则引擎、动态操作记录等场景中灵活运用描述信息相关API |
| 编写触发器 | 理解执行顺序、处理批量数据、避免多个触发器带来的问题 | 仔细研究触发器执行顺序文档,编写能处理批量数据的触发器代码,参考优秀的触发器模板 |
| XML处理 | 有 XMLStreamReader 和 XMLStreamWriter 、 XmlDom 、 DOM 类、 Fast XML DOM 等选项 | 根据XML文件大小和复杂度选择合适的处理方式,优先使用 Fast XML DOM 或 DOM 类 |
| JSON处理 | 使用 JSON 系统类进行JSON生成和解析 | 优先使用 JSON 类,避免使用已弃用的 JSONObject |
| 打包考虑 | 谨慎使用 global 、 final 关键字,注意接口和类的方法添加规则 | 在开发托管包时,严格遵循打包考虑的规则,避免后续版本出现问题 |
| API版本 | 每个Apex类有API版本,与Force.com功能相关 | 创建新类使用最新API版本,升级旧类时做好测试 |
| 测试 | 隔离测试数据、使用不同配置文件测试 | 编写测试用例时,确保测试数据独立,模拟不同用户权限进行测试 |
13. 实践流程示例
下面是一个简单的mermaid流程图,展示了编写一个完整Apex功能的实践流程:
graph LR
A[需求分析] --> B[设计架构]
B --> C[编写代码]
C --> D{是否涉及DML操作}
D -- 是 --> E[添加DML选项和错误处理]
D -- 否 --> F[继续编写]
E --> F
F --> G{是否需要触发器}
G -- 是 --> H[编写触发器代码]
G -- 否 --> I[检查API版本]
H --> I
I --> J[编写测试用例]
J --> K[测试与调试]
K --> L[优化与部署]
14. 代码示例总结
以下是前面提到的一些关键代码示例的总结:
- 使用 DMLOptions 进行DML操作
Database.DMLOptions dmlOpts = new Database.DMLOptions();
dmlOpts.allowFieldTruncation = true;
Account acc = new Account(Name = 'Abc');
acc.setOptions(dmlOpts);
insert acc;
- DML错误处理
try {
insert new Account(Name = 'XYZ corp');
} catch(DmlException dmle) {
ApexPages.addMessage(new ApexPages.message(ApexPages.severity.ERROR,'Failed to save Account, because of : ' + dmle.getMessage()));
}
- 基于字段名更新记录
public void updateOpp(Opportunity opp, String fldName, Object val) {
if (opp.get(fldName) != val) {
opp.put(fldName, val);
}
}
- 动态实例化类进行税计算
global interface I_TaxCalculator {
Decimal calculate (String itemSku, Integer quantity, String address);
}
public class DefaultTaxCalc implements I_TaxCalculator {
public Decimal calculate (String itemSku, Integer quantity, String address) {
return 1;
}
}
public static Decimal calculateTax(String taxCalcClassName, String itemSku, Integer quantity, String address) {
I_TaxCalculator calc = null;
if (taxCalcClassName == null) {
calc = new DefaultTaxCalc();
} else {
Type typ = Type.forName(taxCalcClassName);
calc = (I_TaxCalculator)typ.newInstance();
}
return calc.calculate(itemSku, quantity, address);
}
15. 未来趋势与展望
随着Salesforce平台的不断发展,Apex语言也会不断进化。未来可能会有更多的优化和新功能推出,例如更高效的XML和JSON处理方式、更强大的测试工具等。开发者需要持续关注平台的更新,不断学习和掌握新的技术,以编写更加高效、稳定的Apex代码。同时,随着企业数字化转型的加速,对Apex开发者的需求也会越来越大,掌握编写优质Apex代码的技能将在职业生涯中具有重要的价值。
通过本文的介绍,相信你对编写更优质的Apex代码有了更深入的了解。在实际开发中,要不断实践和总结经验,灵活运用各种技术和方法,才能编写出高质量的Apex代码。
超级会员免费看
62

被折叠的 条评论
为什么被折叠?



