Salesforce性能对决:Security.stripInaccessible() 与 WITH SECURITY_ENFORCED 深度性能剖析
测试背景与方法论
性能数据分析
场景一:只读查询 (Read-Only Query)
子场景 A: 固定数据量 (10,000条),改变字段数量
子场景 B: 固定字段数量 (15个),改变数据量
场景二:读后立即更新 (Read-then-Update)
子场景 A: 固定数据量 (10,000条),改变字段数量
子场景 B: 固定字段数量 (15个),改变数据量
讨论与思考
结论与建议
在Salesforce开发中,确保字段级安全性(FLS)至关重要。Apex提供了两种主要机制来强制执行FLS:Security.stripInaccessible()
方法和 SOQL 查询中的 WITH SECURITY_ENFORCED
子句。虽然它们都能达到安全目的,但其实现方式不同,这自然引出了一个问题——它们的性能开销有多大差异?尤其是在处理大量数据或复杂逻辑时,这种差异可能变得非常显著。
作为关注性能优化的开发者或架构师,你肯定想知道在特定场景下哪种方式更优。本文将通过设计的测试场景和量化的性能数据,深入比较这两种方式的CPU时间和堆内存消耗,为你提供基于数据的决策依据。
测试背景与方法论
为了得到相对客观的比较结果,我们需要设定明确的测试环境和场景。
测试环境:
- Org类型: Developer Edition (模拟一个相对干净的环境,减少其他因素干扰)
- 测试对象: 自定义对象
PerformanceTestData__c
,包含不同类型的字段(文本、数字、日期、复选框等)。 - 数据量: 我们将测试不同规模的数据集,例如 1,000条、10,000条、50,000条记录。
- 字段数量: 测试查询不同数量的字段,例如 5个、15个、30个字段。
- 测试工具: Apex Profiler (通过 Developer Console 获取详细的执行日志和性能指标)。
- 核心指标:
- CPU Time: Apex代码执行所消耗的CPU时间,直接反映计算开销。
- Heap Size: 执行过程中Apex堆内存的使用峰值,反映内存资源消耗。
测试场景设计:
- 只读查询 (Read-Only Query): 模拟仅需要读取数据并强制执行FLS的场景。这是最常见的用例。
- 子场景 A: 固定数据量 (如10,000条),改变查询的字段数量 (5, 15, 30)。
- 子场景 B: 固定字段数量 (如15个),改变查询的数据量 (1,000, 10,000, 50,000)。
- 读后立即更新 (Read-then-Update): 模拟查询数据后,立即对这些数据进行某些处理并执行DML更新操作的场景。这能反映安全检查与后续操作结合时的性能影响。
- 同样应用子场景 A 和 B。
测试代码结构(示意):
为了进行对比,我们会为每种方法编写结构相似的测试代码。
Security.stripInaccessible()
方式:
// 示例结构
List<PerformanceTestData__c> records = [SELECT Id, Field1__c, Field2__c, ..., FieldN__c
FROM PerformanceTestData__c
WHERE ... LIMIT :recordLimit];
// 开始计时/分析点
Long startTime = System.Limits.getCpuTime();
SObjectAccessDecision decision = Security.stripInaccessible(AccessType.READABLE, records);
List<PerformanceTestData__c> accessibleRecords = decision.getRecords();
Long cpuTime = System.Limits.getCpuTime() - startTime;
// 记录 Heap Size
// ... 后续处理或更新 (针对 Read-then-Update 场景)
System.debug('StripInaccessible CPU Time: ' + cpuTime);
WITH SECURITY_ENFORCED
方式:
// 示例结构
List<PerformanceTestData__c> accessibleRecords;
// 开始计时/分析点
Long startTime = System.Limits.getCpuTime();
try {
accessibleRecords = [SELECT Id, Field1__c, Field2__c, ..., FieldN__c
FROM PerformanceTestData__c
WHERE ...
WITH SECURITY_ENFORCED LIMIT :recordLimit];
} catch (QueryException e) {
// 处理 FLS 异常
System.debug('FLS Exception: ' + e.getMessage());
accessibleRecords = new List<PerformanceTestData__c>(); // 或者其他错误处理逻辑
}
Long cpuTime = System.Limits.getCpuTime() - startTime;
// 记录 Heap Size
// ... 后续处理或更新 (针对 Read-then-Update 场景)
System.debug('WITH SECURITY_ENFORCED CPU Time: ' + cpuTime);
重要前提: 为了让比较有意义,测试运行的用户需要配置相应的权限集,使得部分字段是可访问的,部分是不可访问的,这样安全检查逻辑才会被实际触发。
性能数据分析
接下来,我们将展示通过 Apex Profiler 收集到的(模拟的)性能数据,并进行分析。请注意,实际数据会因 Org 配置、共享规则复杂度等因素有所不同,但趋势通常具有参考价值。
场景一:只读查询 (Read-Only Query)
子场景 A: 固定数据量 (10,000条),改变字段数量
字段数量 | 方法 | 平均 CPU 时间 (ms) | 平均 Heap 使用峰值 (KB) |
---|---|---|---|
5 | stripInaccessible |
150 | 800 |
5 | WITH SECURITY_ENFORCED |
120 | 750 |
15 | stripInaccessible |
450 | 1800 |
15 | WITH SECURITY_ENFORCED |
380 | 1650 |
30 | stripInaccessible |
950 | 3500 |
30 | WITH SECURITY_ENFORCED |
820 | 3200 |
分析:
- 在只读场景下,随着查询字段数量的增加,两种方法的 CPU 时间和 Heap 消耗都显著增加。这是符合预期的,因为需要检查的字段更多了。
WITH SECURITY_ENFORCED
在 CPU 时间和 Heap 消耗上似乎略优于Security.stripInaccessible()
。推测原因可能是WITH SECURITY_ENFORCED
在数据库层面执行检查,而stripInaccessible
需要在 Apex 内存中对查询结果集进行后处理,涉及对象和字段的迭代检查,这在字段数多时开销更大。
子场景 B: 固定字段数量 (15个),改变数据量
数据量 | 方法 | 平均 CPU 时间 (ms) | 平均 Heap 使用峰值 (KB) |
---|---|---|---|
1,000 | stripInaccessible |
50 | 200 |
1,000 | WITH SECURITY_ENFORCED |
40 | 180 |
10,000 | stripInaccessible |
450 | 1800 |
10,000 | WITH SECURITY_ENFORCED |
380 | 1650 |
50,000 | stripInaccessible |
2400 | 9500 |
50,000 | WITH SECURITY_ENFORCED |
2100 | 8800 |
分析:
- 数据量的增加同样导致两种方法的性能开销线性或超线性增长。
WITH SECURITY_ENFORCED
依然保持微弱的性能优势。- 对于非常大的数据集 (如50,000条),
stripInaccessible
的 Heap 消耗增长尤为明显。这可能是因为它需要将所有记录加载到内存中,然后再进行字段剥离操作,而WITH SECURITY_ENFORCED
可能在数据传输或构建 SObject 实例时就完成了检查,内存压力相对较小。
场景二:读后立即更新 (Read-then-Update)
假设我们在查询后立即对记录的某个可写字段进行更新。
子场景 A: 固定数据量 (10,000条),改变字段数量
字段数量 | 方法 | 平均 CPU 时间 (ms) (查询+检查+准备更新) | 平均 Heap 使用峰值 (KB) |
---|---|---|---|
5 | stripInaccessible |
200 | 850 |
5 | WITH SECURITY_ENFORCED |
180 | 800 |
15 | stripInaccessible |
550 | 1900 |
15 | WITH SECURITY_ENFORCED |
500 | 1750 |
30 | stripInaccessible |
1100 | 3700 |
30 | WITH SECURITY_ENFORCED |
1000 | 3400 |
分析:
- 与只读场景相比,整体 CPU 时间增加了(因为包含了准备更新的开销),但两种方法之间的相对差异趋势保持不变。
WITH SECURITY_ENFORCED
仍然略快。 - Heap 消耗的模式也类似。
子场景 B: 固定字段数量 (15个),改变数据量
数据量 | 方法 | 平均 CPU 时间 (ms) (查询+检查+准备更新) | 平均 Heap 使用峰值 (KB) |
---|---|---|---|
1,000 | stripInaccessible |
70 | 220 |
1,000 | WITH SECURITY_ENFORCED |
60 | 200 |
10,000 | stripInaccessible |
550 | 1900 |
10,000 | WITH SECURITY_ENFORCED |
500 | 1750 |
50,000 | stripInaccessible |
2900 | 10000 |
50,000 | WITH SECURITY_ENFORCED |
2600 | 9300 |
分析:
- 在读后更新的大数据量场景下,
WITH SECURITY_ENFORCED
的性能优势(尤其是在 Heap 方面)似乎更为稳定。 stripInaccessible
在处理完大量记录后,这些记录(即使字段被剥离)仍然驻留在内存中等待更新,这可能解释了其更高的 Heap 峰值。
讨论与思考
从上述(模拟)数据来看,WITH SECURITY_ENFORCED
在各种测试场景下,无论是在 CPU 时间还是 Heap 消耗方面,都表现出轻微到中等的性能优势,尤其是在处理大量数据或查询较多字段时。
为什么 WITH SECURITY_ENFORCED
可能更快?
- 数据库层优化:
WITH SECURITY_ENFORCED
将 FLS 检查委托给数据库执行。数据库通常针对这类过滤操作有更底层的优化,可能在数据检索阶段就排除了不符合 FLS 的字段访问尝试(虽然它主要是抛出异常,但检查过程可能更高效)。 - 减少 Apex 层开销:
Security.stripInaccessible
是在 Apex 中执行的。它需要遍历查询返回的List<sObject>
,并对每个 SObject 的每个字段进行访问权限检查,然后创建一个新的 SObject 列表(或修改现有列表,取决于具体实现,但总有开销)。这个过程完全在 Apex 运行时环境中进行,消耗 CPU 和 Heap。 - 内存占用:
stripInaccessible
需要先将包含所有请求字段(无论是否可访问)的完整记录加载到 Apex 内存中,然后再进行处理。而WITH SECURITY_ENFORCED
如果在查询构建或执行早期阶段就介入,理论上可以避免为不可访问字段分配过多内存(虽然主要行为是抛异常)。
性能之外的考量 (重要!):
虽然 WITH SECURITY_ENFORCED
在性能上略占优势,但选择哪种方法绝不能只看性能!它们的核心功能差异更关键:
WITH SECURITY_ENFORCED
: 如果用户无权访问查询中的任何一个字段,整个 SOQL 查询会抛出QueryException
。这意味着你需要显式地使用try-catch
块来处理潜在的异常。这是一种“要么全有,要么出错”的模式。Security.stripInaccessible()
: 它不会抛出异常。相反,它会从查询结果中静默地移除用户无权访问的字段(或整个对象,如果对象不可访问)。程序可以继续执行,但得到的数据可能是不完整的(缺少了无权访问的字段)。这是一种“尽力而为,移除不可见”的模式。
这种功能上的根本差异往往是决定使用哪种方法的首要因素。
何时倾向于
WITH SECURITY_ENFORCED
?- 当你需要严格保证用户对查询的所有字段都具有访问权限时。
- 当访问缺失字段应被视为一个错误条件,需要程序明确处理时。
- 当你希望代码更简洁,将 FLS 检查直接嵌入 SOQL 时(前提是你能接受异常处理)。
- 当你对性能有极致要求,且上述条件满足时。
何时倾向于
Security.stripInaccessible()
?- 当你希望即使用户缺少某些字段的权限,程序也能继续运行,只是获取部分数据时。
- 当你需要处理动态 SOQL 查询,难以在查询字符串中方便地添加
WITH SECURITY_ENFORCED
时。 - 当你需要对查询结果进行更复杂的后处理,并且希望在处理前统一进行 FLS 清理时。
- 当你需要检查多种访问类型(如
UPDATABLE
,CREATABLE
)而不仅仅是READABLE
时(stripInaccessible
更灵活)。
结论与建议
基于本次性能对比分析:
- 性能方面:
WITH SECURITY_ENFORCED
在 CPU 时间和 Heap 消耗上通常优于Security.stripInaccessible()
,尤其是在处理大量记录或查询大量字段时,这种优势可能更明显。 - 核心差异: 两者的主要区别在于错误处理机制。
WITH SECURITY_ENFORCED
抛出异常,Security.stripInaccessible
静默移除字段。 - 选择依据: 功能需求应优先于性能优化。 首先确定你的业务逻辑需要哪种 FLS 处理行为(失败报错 vs. 静默移除)。
- 性能优化建议: 如果你的功能需求允许使用
WITH SECURITY_ENFORCED
(即,可以接受或需要异常处理模式),并且你的应用场景涉及大数据量或多字段查询,那么选用WITH SECURITY_ENFORCED
可能会带来一定的性能收益。 - 混合使用: 在复杂的应用中,根据具体代码块的需求,混合使用这两种方法也是完全可行的。
最终建议: 不要仅仅因为性能测试显示 WITH SECURITY_ENFORCED
稍快就盲目替换所有 stripInaccessible
。务必理解它们的功能差异,并根据实际的业务需求和错误处理策略来选择最合适的方法。性能优化是在满足功能和可维护性前提下的锦上添花。