salesforce-apex-quality

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Salesforce Apex Quality Guardrails

Salesforce Apex 质量防护规则

Apply these checks to every Apex class, trigger, and test file you write or review.
将以下检查项应用到你编写或评审的每一个Apex类、触发器和测试文件中。

Step 1 — Governor Limit Safety Check

步骤1 — 治理限制安全检查

Scan for these patterns before declaring any Apex file acceptable:
在判定任意Apex文件合格前,先扫描是否存在以下问题模式:

SOQL and DML in Loops — Automatic Fail

循环中使用SOQL和DML — 自动不通过

apex
// ❌ NEVER — causes LimitException at scale
for (Account a : accounts) {
    List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :a.Id]; // SOQL in loop
    update a; // DML in loop
}

// ✅ ALWAYS — collect, then query/update once
Set<Id> accountIds = new Map<Id, Account>(accounts).keySet();
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
    if (!contactsByAccount.containsKey(c.AccountId)) {
        contactsByAccount.put(c.AccountId, new List<Contact>());
    }
    contactsByAccount.get(c.AccountId).add(c);
}
update accounts; // DML once, outside the loop
Rule: if you see
[SELECT
or
Database.query
,
insert
,
update
,
delete
,
upsert
,
merge
inside a
for
loop body — stop and refactor before proceeding.
apex
// ❌ NEVER — 规模较大时会触发LimitException
for (Account a : accounts) {
    List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :a.Id]; // SOQL in loop
    update a; // DML in loop
}

// ✅ ALWAYS — 先收集数据,再执行一次查询/更新
Set<Id> accountIds = new Map<Id, Account>(accounts).keySet();
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) {
    if (!contactsByAccount.containsKey(c.AccountId)) {
        contactsByAccount.put(c.AccountId, new List<Contact>());
    }
    contactsByAccount.get(c.AccountId).add(c);
}
update accounts; // DML once, outside the loop
规则:如果你在
for
循环体中发现
[SELECT
Database.query
insert
update
delete
upsert
merge
关键字,请先暂停后续操作,完成代码重构后再继续。

Step 2 — Sharing Model Verification

步骤2 — 共享模型验证

Every class must declare its sharing intent explicitly. Undeclared sharing inherits from the caller — unpredictable behaviour.
DeclarationWhen to use
public with sharing class Foo
Default for all service, handler, selector, and controller classes
public without sharing class Foo
Only when the class must run elevated (e.g. system-level logging, trigger bypass). Requires a code comment explaining why.
public inherited sharing class Foo
Framework entry points that should respect the caller's sharing context
If a class does not have one of these three declarations, add it before writing anything else.
每个类都必须显式声明其共享意图。未声明共享模式的类会继承调用方的共享设置,会导致不可预测的行为。
声明适用场景
public with sharing class Foo
所有服务类、处理类、选择器类和控制器类的默认选项
public without sharing class Foo
仅当类必须以提升的权限运行时使用(例如系统级日志、触发器绕过),需要添加代码注释说明原因。
public inherited sharing class Foo
框架入口点,需要遵循调用方的共享上下文时使用
如果一个类没有以上三种声明之一,请在编写其他代码前先添加声明

Step 3 — CRUD / FLS Enforcement

步骤3 — CRUD / FLS 强制校验

Apex code that reads or writes records on behalf of a user must verify object and field access. The platform does not enforce FLS or CRUD automatically in Apex.
apex
// Check before querying a field
if (!Schema.sObjectType.Contact.fields.Email.isAccessible()) {
    throw new System.NoAccessException();
}

// Or use WITH USER_MODE in SOQL (API 56.0+)
List<Contact> contacts = [SELECT Id, Email FROM Contact WHERE AccountId = :accId WITH USER_MODE];

// Or use Database.query with AccessLevel
List<Contact> contacts = Database.query('SELECT Id, Email FROM Contact', AccessLevel.USER_MODE);
Rule: any Apex method callable from a UI component, REST endpoint, or
@InvocableMethod
must enforce CRUD/FLS. Internal service methods called only from trusted contexts may use
with sharing
instead.
代表用户读写记录的Apex代码必须验证对象和字段的访问权限。平台不会在Apex中自动强制执行FLS或CRUD校验。
apex
// Check before querying a field
if (!Schema.sObjectType.Contact.fields.Email.isAccessible()) {
    throw new System.NoAccessException();
}

// Or use WITH USER_MODE in SOQL (API 56.0+)
List<Contact> contacts = [SELECT Id, Email FROM Contact WHERE AccountId = :accId WITH USER_MODE];

// Or use Database.query with AccessLevel
List<Contact> contacts = Database.query('SELECT Id, Email FROM Contact', AccessLevel.USER_MODE);
规则:任何可从UI组件、REST端点或
@InvocableMethod
调用的Apex方法必须强制执行CRUD/FLS校验。仅在可信上下文调用的内部服务方法可以使用
with sharing
替代。

Step 4 — SOQL Injection Prevention

步骤4 — SOQL 注入防护

apex
// ❌ NEVER — concatenates user input into SOQL string
String soql = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';

// ✅ ALWAYS — bind variable
String soql = [SELECT Id FROM Account WHERE Name = :userInput];

// ✅ For dynamic SOQL with user-controlled field names — validate against a whitelist
Set<String> allowedFields = new Set<String>{'Name', 'Industry', 'AnnualRevenue'};
if (!allowedFields.contains(userInput)) {
    throw new IllegalArgumentException('Field not permitted: ' + userInput);
}
apex
// ❌ NEVER — concatenates user input into SOQL string
String soql = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';

// ✅ ALWAYS — bind variable
String soql = [SELECT Id FROM Account WHERE Name = :userInput];

// ✅ For dynamic SOQL with user-controlled field names — validate against a whitelist
Set<String> allowedFields = new Set<String>{'Name', 'Industry', 'AnnualRevenue'};
if (!allowedFields.contains(userInput)) {
    throw new IllegalArgumentException('Field not permitted: ' + userInput);
}

Step 5 — Modern Apex Idioms

步骤5 — 现代Apex惯用写法

Prefer current language features (API 62.0 / Winter '25+):
Old patternModern replacement
if (obj != null) { x = obj.Field__c; }
x = obj?.Field__c;
x = (y != null) ? y : defaultVal;
x = y ?? defaultVal;
System.assertEquals(expected, actual)
Assert.areEqual(expected, actual)
System.assert(condition)
Assert.isTrue(condition)
[SELECT ... WHERE ...]
with no sharing context
[SELECT ... WHERE ... WITH USER_MODE]
优先使用当前版本的语言特性(API 62.0 / Winter '25及以上):
旧模式现代替代写法
if (obj != null) { x = obj.Field__c; }
x = obj?.Field__c;
x = (y != null) ? y : defaultVal;
x = y ?? defaultVal;
System.assertEquals(expected, actual)
Assert.areEqual(expected, actual)
System.assert(condition)
Assert.isTrue(condition)
[SELECT ... WHERE ...]
with no sharing context
[SELECT ... WHERE ... WITH USER_MODE]

Step 6 — PNB Test Coverage Checklist

步骤6 — PNB测试覆盖检查清单

Every feature must be tested across all three paths. Missing any one of these is a quality failure:
每个功能都必须覆盖以下三类测试路径,缺少任意一项都属于质量不达标:

Positive Path

正向路径

  • Expected input → expected output.
  • Assert the exact field values, record counts, or return values — not just that no exception was thrown.
  • 预期输入 → 预期输出。
  • 断言准确的字段值、记录计数或返回值 — 不能仅验证没有抛出异常。

Negative Path

负向路径

  • Invalid input, null values, empty collections, and error conditions.
  • Assert that exceptions are thrown with the correct type and message.
  • Assert that no records were mutated when the operation should have failed cleanly.
  • 无效输入、空值、空集合和错误场景。
  • 断言抛出的异常类型和消息符合预期。
  • 断言操作正常失败时没有任何记录被修改。

Bulk Path

批量路径

  • Insert/update/delete 200–251 records in a single test transaction.
  • Assert that all records processed correctly — no partial failures from governor limits.
  • Use
    Test.startTest()
    /
    Test.stopTest()
    to isolate governor limit counters for async work.
  • 在单个测试事务中插入/更新/删除200–251条记录
  • 断言所有记录都被正确处理 — 没有因治理限制导致的部分失败。
  • 使用
    Test.startTest()
    /
    Test.stopTest()
    隔离异步任务的治理限制计数器。

Test Class Rules

测试类规则

apex
@isTest(SeeAllData=false)   // Required — no exceptions without a documented reason
private class AccountServiceTest {

    @TestSetup
    static void makeData() {
        // Create all test data here — use a factory if one exists in the project
    }

    @isTest
    static void givenValidInput_whenProcessAccounts_thenFieldsUpdated() {
        // Positive path
        List<Account> accounts = [SELECT Id FROM Account LIMIT 10];
        Test.startTest();
        AccountService.processAccounts(accounts);
        Test.stopTest();
        // Assert meaningful outcomes — not just no exception
        List<Account> updated = [SELECT Status__c FROM Account WHERE Id IN :accounts];
        Assert.areEqual('Processed', updated[0].Status__c, 'Status should be Processed');
    }
}
apex
@isTest(SeeAllData=false)   // Required — no exceptions without a documented reason
private class AccountServiceTest {

    @TestSetup
    static void makeData() {
        // Create all test data here — use a factory if one exists in the project
    }

    @isTest
    static void givenValidInput_whenProcessAccounts_thenFieldsUpdated() {
        // Positive path
        List<Account> accounts = [SELECT Id FROM Account LIMIT 10];
        Test.startTest();
        AccountService.processAccounts(accounts);
        Test.stopTest();
        // Assert meaningful outcomes — not just no exception
        List<Account> updated = [SELECT Status__c FROM Account WHERE Id IN :accounts];
        Assert.areEqual('Processed', updated[0].Status__c, 'Status should be Processed');
    }
}

Step 7 — Trigger Architecture Checklist

步骤7 — 触发器架构检查清单

  • One trigger per object. If a second trigger exists, consolidate into the handler.
  • Trigger body contains only: context checks, handler invocation, and routing logic.
  • No business logic, SOQL, or DML directly in the trigger body.
  • If a trigger framework (Trigger Actions Framework, ff-apex-common, custom base class) is already in use — extend it. Do not create a parallel pattern.
  • Handler class is
    with sharing
    unless the trigger requires elevated access.
  • 每个对象仅保留一个触发器。如果存在第二个触发器,请合并到处理程序中。
  • 触发器主体仅包含:上下文检查、处理程序调用和路由逻辑。
  • 触发器主体中不直接编写业务逻辑、SOQL或DML代码。
  • 如果项目中已经使用了触发器框架(Trigger Actions Framework、ff-apex-common、自定义基类) — 直接扩展该框架,不要创建并行的实现模式。
  • 处理程序类默认使用
    with sharing
    ,除非触发器需要提升权限。

Quick Reference — Hardcoded Anti-Patterns Summary

快速参考 — 硬编码反模式汇总

PatternAction
SOQL inside
for
loop
Refactor: query before the loop, operate on collections
DML inside
for
loop
Refactor: collect mutations, DML once after the loop
Class missing sharing declarationAdd
with sharing
(or document why
without sharing
)
escape="false"
on user data (VF)
Remove — auto-escaping enforces XSS prevention
Empty
catch
block
Add logging and appropriate re-throw or error handling
String-concatenated SOQL with user inputReplace with bind variable or whitelist validation
Test with no assertionAdd a meaningful
Assert.*
call
System.assert
/
System.assertEquals
style
Upgrade to
Assert.isTrue
/
Assert.areEqual
Hardcoded record ID (
'001...'
)
Replace with queried or inserted test record ID
模式处理方式
for
循环内使用SOQL
重构:在循环前查询数据,基于集合操作
for
循环内使用DML
重构:收集修改项,循环结束后执行一次DML
类缺少共享模式声明添加
with sharing
(或说明使用
without sharing
的原因)
用户数据(VF页面)设置
escape="false"
移除 — 自动转义可防范XSS攻击
catch
代码块
添加日志和合适的重抛或错误处理逻辑
将用户输入字符串拼接进SOQL替换为绑定变量或白名单校验
没有断言的测试用例添加有实际意义的
Assert.*
调用
使用
System.assert
/
System.assertEquals
写法
升级为
Assert.isTrue
/
Assert.areEqual
硬编码记录ID(
'001...'
替换为查询或插入生成的测试记录ID