salesforce-apex-quality
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSalesforce 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 loopRule: if you see or , , , , , inside a loop body — stop and refactor before proceeding.
[SELECTDatabase.queryinsertupdatedeleteupsertmergeforapex
// ❌ 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[SELECTDatabase.queryinsertupdatedeleteupsertmergeStep 2 — Sharing Model Verification
步骤2 — 共享模型验证
Every class must declare its sharing intent explicitly. Undeclared sharing inherits from the caller — unpredictable behaviour.
| Declaration | When to use |
|---|---|
| Default for all service, handler, selector, and controller classes |
| Only when the class must run elevated (e.g. system-level logging, trigger bypass). Requires a code comment explaining why. |
| 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.
每个类都必须显式声明其共享意图。未声明共享模式的类会继承调用方的共享设置,会导致不可预测的行为。
| 声明 | 适用场景 |
|---|---|
| 所有服务类、处理类、选择器类和控制器类的默认选项 |
| 仅当类必须以提升的权限运行时使用(例如系统级日志、触发器绕过),需要添加代码注释说明原因。 |
| 框架入口点,需要遵循调用方的共享上下文时使用 |
如果一个类没有以上三种声明之一,请在编写其他代码前先添加声明。
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 must enforce CRUD/FLS. Internal service methods called only from trusted contexts may use instead.
@InvocableMethodwith sharing代表用户读写记录的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端点或调用的Apex方法必须强制执行CRUD/FLS校验。仅在可信上下文调用的内部服务方法可以使用替代。
@InvocableMethodwith sharingStep 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 pattern | Modern replacement |
|---|---|
| |
| |
| |
| |
| |
优先使用当前版本的语言特性(API 62.0 / Winter '25及以上):
| 旧模式 | 现代替代写法 |
|---|---|
| |
| |
| |
| |
| |
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()to isolate governor limit counters for async work.Test.stopTest()
- 在单个测试事务中插入/更新/删除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 unless the trigger requires elevated access.
with sharing
- 每个对象仅保留一个触发器。如果存在第二个触发器,请合并到处理程序中。
- 触发器主体仅包含:上下文检查、处理程序调用和路由逻辑。
- 触发器主体中不直接编写业务逻辑、SOQL或DML代码。
- 如果项目中已经使用了触发器框架(Trigger Actions Framework、ff-apex-common、自定义基类) — 直接扩展该框架,不要创建并行的实现模式。
- 处理程序类默认使用,除非触发器需要提升权限。
with sharing
Quick Reference — Hardcoded Anti-Patterns Summary
快速参考 — 硬编码反模式汇总
| Pattern | Action |
|---|---|
SOQL inside | Refactor: query before the loop, operate on collections |
DML inside | Refactor: collect mutations, DML once after the loop |
| Class missing sharing declaration | Add |
| Remove — auto-escaping enforces XSS prevention |
Empty | Add logging and appropriate re-throw or error handling |
| String-concatenated SOQL with user input | Replace with bind variable or whitelist validation |
| Test with no assertion | Add a meaningful |
| Upgrade to |
Hardcoded record ID ( | Replace with queried or inserted test record ID |
| 模式 | 处理方式 |
|---|---|
| 重构:在循环前查询数据,基于集合操作 |
| 重构:收集修改项,循环结束后执行一次DML |
| 类缺少共享模式声明 | 添加 |
用户数据(VF页面)设置 | 移除 — 自动转义可防范XSS攻击 |
空 | 添加日志和合适的重抛或错误处理逻辑 |
| 将用户输入字符串拼接进SOQL | 替换为绑定变量或白名单校验 |
| 没有断言的测试用例 | 添加有实际意义的 |
使用 | 升级为 |
硬编码记录ID( | 替换为查询或插入生成的测试记录ID |