Dynamic Apex 完全指南:Describe 信息、动态 SOQL/SOSL 与动态 DML

Dynamic Apex完全指南:涵盖Describe信息(Token轻量引用/getDescribe/describeSObjects)、字段Token使用(静态成员变量/fields/getMap动态映射)、权限考量(用户模式运行/字段隐藏)、Schema方法(describeTabs获取Tab元数据/getGlobalDescribe全局sObject映射/数据类别)、动态SOQL(Database.query/queryWithBinds绑定变量Map/accessLevel用户vs系统模式/不能使用绑定变量字段的限制/escapeSingleQuotes防注入/countQuery/getQueryLocator额外方法)、动态SOSL(search.query/Search.find+WITH SNIPPET搜索片段/SOSL注入防护)以及动态DML(newSObject动态创建/get+put字段操作/getSObject+putSObject外键处理/父子关系动态查询)。...

📅 2026/7/16 ✍️ ponybai 🏷️ apex, salesforce

Dynamic Apex

Dynamic Apex

Dynamic Apex 使开发者能够创建更灵活的应用程序,提供以下能力:访问 sObject 和字段 Describe 信息(元数据)、访问 Salesforce 应用信息、编写动态 SOQL/SOSL 查询和动态 DML。

Apex Describe 信息:Token 与 Describe 结果

Describe信息

Apex 提供两种数据结构和一种方法用于 sObject 和字段的 describe 信息:

  • Token:轻量级、可序列化的 sObject 或字段引用,在编译时验证。用于 token describe。使用 == 运算符比较
  • describeSObjects 方法:Schema 类中执行一个或多个 sObject 类型 describe 的方法
  • Describe 结果对象:Schema.DescribeSObjectResult——包含 sObject 或字段的所有 describe 属性。不可序列化,运行时验证

从 token 到 describe 结果非常容易:getDescribe() 返回 describe 结果;describe 结果上的 getSObjectType()getSObjectField() 返回 token。Token 轻量,使用更快速高效。

sObject s = new Account();
System.assert(s.getsObjectType() == Account.sObjectType);

// 获取 sObject describe 结果
Schema.DescribeSObjectResult dsr = Account.sObjectType.getDescribe();
// 或使用静态成员变量
Schema.DescribeSObjectResult dsr2 = Schema.SObjectType.Account;

// 获取字段 describe 结果
Schema.DescribeFieldResult dfr = Schema.sObjectType.Account.fields.Name;
System.assert(dfr.getSObjectField() == Account.Name);
dfr = dfr.getSObjectField().getDescribe();

访问 sObject Token:通过 sObject 类型上的 sObjectType 成员变量(如 Account.sObjectType),或通过对 sObject 变量/列表/Map 调用 getSObjectType()。注意:某些标准对象(如 RecordType)有 sObjectType 字段——对这些类型始终使用 getSObjectType() 方法。

使用字段 Token

字段Token

访问字段 Token:通过 sObject 静态类型的静态成员变量(如 Account.Description),或通过对字段 describe 结果调用 getSObjectField()

Schema.SObjectField fieldToken = Account.Description;

// 从字段 describe 结果获取 token
Schema.DescribeFieldResult dfr = Schema.sObjectType.Account.fields.Name;
System.assert(dfr.getSObjectField() == Account.Name);
dfr = dfr.getSObjectField().getDescribe();

使用 fields 成员变量:Schema.SObjectType.Account.fields.Name——系统在编译时验证最后的成员变量对指定的 sObject 有效。注意:不要在不使用字段成员变量或 getMap 方法的情况下单独使用 fields 成员变量。

访问所有字段 Describe 结果:使用 getMap() 返回所有字段名(键)和字段 token(值)之间的映射。Map 是动态的(运行时生成)、字段名不区分大小写、键使用命名空间(如有)。注意:值类型是 token 而非 describe 结果(使用 describe 结果将消耗过多系统资源)。

Map<String, Schema.SObjectField> fieldMap = Schema.SObjectType.Account.fields.getMap();
注意:个人账户不支持字段 Token——访问 Schema.Account.fieldname 会抛出异常,应使用字符串指定字段名。从已安装托管包中执行时,即使安装组织中未启用 Chatter,字段 describe 也会返回 Chatter 字段。

Describe 权限与 Schema 方法

权限与Schema

Apex 类默认在用户模式下运行——用户权限和字段级安全受到尊重。无法访问隐藏字段或对象。用户权限在匿名块中执行 describe 调用时也很重要——如果运行用户没有访问权限,并非所有字段都会被返回。

使用 describeSObjects Schema 方法:替代 token 的方式——传递一个或多个 sObject 类型名称的字符串数组:

String[] types = new String[]{'Account', 'Merchandise__c'};
Schema.DescribeSobjectResult[] results = Schema.describeSObjects(types);
for (Schema.DescribeSobjectResult res : results) {
    System.debug('sObject Label: ' + res.getLabel());
    System.debug('Number of fields: ' + res.fields.getMap().size());
    System.debug(res.isCustom() ? 'Custom object.' : 'Standard object.');
    // 获取子关系
    Schema.ChildRelationship[] rels = res.getChildRelationships();
}

全局 Describe:Schema.getGlobalDescribe() 返回所有 sObject 名称到 token 的映射,运行时根据权限动态生成。API 28.0+ 中键始终带有命名空间前缀(如有)。标准 sObject 无命名空间前缀。

描述 Tab 与访问所有 sObject

Tab与sObject

描述 Tab:Schema.describeTabs() 返回每个应用的 TabSet 描述信息。每个 Tab 包含图标 URL、是否为自定义、颜色、sObject 名称等元数据:

List<DescribeTabSetResult> tabSetDesc = Schema.describeTabs();
for (DescribeTabSetResult tsr : tabSetDesc) {
    String appLabel = tsr.getLabel();
    if (appLabel == 'Sales') {
        List<Schema.DescribeTabResult> tabDesc = tsr.getTabs();
        for (Schema.DescribeTabResult tr : tabDesc) {
            System.debug(tr.getLabel() + ' | ' + tr.getSobjectName() + ' | Custom: ' + tr.isCustom());
        }
    }
}

数据类别信息:describeDataCategoryGroups()describeDataCategoryGroupStructures() 返回与特定对象关联的类别组和类别(需要启用 Salesforce Knowledge 和 Answers 功能)。

动态 SOQL

动态SOQL

动态 SOQL 指在运行时使用 Apex 代码创建 SOQL 字符串。使用 Database.query()Database.queryWithBinds() 方法。结果可以指定为具体 sObject 或通用 sObject——系统在运行时验证查询类型与声明类型匹配。受与静态查询相同的 Governor Limit 约束。

// 返回单条记录
Account a = Database.query('SELECT Id FROM Account LIMIT 1');

// 返回列表
List<Account> accts = Database.query('SELECT Id, Name FROM Account');

// 带绑定变量的简单用法
String myTestString = 'TestName';
List<MyCustomObject__c> sobjList = Database.query(
    'SELECT Id FROM MyCustomObject__c WHERE Name = :myTestString', AccessLevel.USER_MODE);

queryWithBinds(API 57.0+):绑定变量从 Map 参数解析,无需变量在作用域内。Map 键比较不区分大小写——重复的键(仅大小写不同)会抛出运行时 QueryException:

Map<String, Object> acctBinds = new Map<String, Object>{'acctName' => 'Acme Corporation'};
List<Account> accts = Database.queryWithBinds(
    'SELECT Id FROM Account WHERE Name = :acctName', acctBinds, AccessLevel.USER_MODE);

访问级别参数(API 55.0+):AccessLevel.SYSTEM_MODE 忽略当前用户的对象和字段级权限,AccessLevel.USER_MODE(默认)强制执行。

动态 SOQL 限制:与内联 SOQL 不同,不能在查询字符串中使用绑定变量字段(如 :myVariable.field1__c)——会产生"Variable does not exist"错误。解决方案:先解析为字符串变量,或使用 queryWithBinds

动态 SOQL:安全与额外方法

SOQL安全

SOQL 注入防护:使用 String.escapeSingleQuotes() 方法——在所有单引号前添加转义字符(\),确保用户输入被当作封闭字符串而非数据库命令处理。

String userInput = ApexPages.currentPage().getParameters().get('search');
String safeInput = String.escapeSingleQuotes(userInput);
List<Account> results = Database.query(
    'SELECT Id FROM Account WHERE Name LIKE \'%' + safeInput + '%\'');

额外动态 SOQL 方法:

  • Database.countQuery() / Database.countQueryWithBinds()——返回动态查询的记录数
  • Database.getQueryLocator() / Database.getQueryLocatorWithBinds()——创建用于 Batch Apex 或 Visualforce 的 QueryLocator 对象

动态 SOSL

动态SOSL

动态 SOSL 指在运行时创建 SOSL 字符串。使用 search.query() 方法。结果求值为 sObject 列表的列表——与静态 SOSL 处理方式相同。

String searchquery = 'FIND \'Edge*\' IN ALL FIELDS RETURNING Account(id,name), Contact, Lead';
List<List<sObject>> searchList = search.query(searchquery);

返回搜索片段(Snippet):使用 Search.find() 方法和 WITH SNIPPET 子句为搜索结果提供更多上下文。片段使识别内容更容易:

Search.SearchResults searchResults = Search.find(
    'FIND \'test\' IN ALL FIELDS RETURNING KnowledgeArticleVersion(id, title
    WHERE PublishStatus = \'Online\' AND Language = \'en_US\') WITH SNIPPET (target_length=120)');
List<Search.SearchResult> articleList = searchResults.get('KnowledgeArticleVersion');
for (Search.SearchResult sr : articleList) {
    KnowledgeArticleVersion article = (KnowledgeArticleVersion) sr.getSObject();
    System.debug(article.Title + ' | Snippet: ' + sr.getSnippet());
}

SOSL 注入防护:同样使用 escapeSingleQuotes() 方法。

动态 DML

动态DML

除了查询 describe 信息和构建运行时 SOQL 查询外,还可以动态创建 sObject 并使用 DML 插入数据库。使用 sObject token 上的 newSObject() 方法——注意 token 必须转换回具体的 sObject 类型

Account a = new Account();
Schema.sObjectType tokenA = a.getSObjectType();
// 错误——token 是通用 sObject,不是 Account
// Account b = tokenA.newSObject();
// 正确——转换回具体类型
Account b = (Account) tokenA.newSObject();

// 通过全局 Describe 动态创建
public static sObject createObject(String typeName) {
    Schema.SObjectType targetType = Schema.getGlobalDescribe().get(typeName);
    if (targetType == null) { throw new IllegalArgumentException('Unknown type: ' + typeName); }
    return targetType.newSObject();
}

// 使用
Account a = (Account) DynamicSObjectCreation.createObject('Account');
a.Name = 'Acme';
insert a;

设置和获取字段值:使用 get()put() 方法,通过字段的 API 名称(String)或字段 token:

SObject s = [SELECT AccountNumber FROM Account LIMIT 1];
Object o = s.get('AccountNumber');  // 通过 API 名称
s.put('AccountNumber', 'abc');

// 通过字段 Token
Schema.DescribeFieldResult dfr = Schema.sObjectType.Account.fields.AccountNumber;
s.put(dfr.getSObjectField(), '12345');

动态 DML:外键

外键

设置和获取外键:使用 get()/put() 设置标量 ID 值;使用 getSObject()/putSObject() 设置关联记录(必须使用 sObject 数据类型,不是 Object)。

SObject c = Database.query('SELECT Id, FirstName, AccountId, Account.Name FROM Contact LIMIT 1');
SObject a = c.getSObject('Account');  // 获取关联的 Account 记录

父子关系动态查询:使用动态 Apex 访问父子关系中的子查询结果:

String queryString = 'SELECT Id, Name, (SELECT FirstName, LastName FROM Contacts LIMIT 1) FROM Account';
SObject[] queryParentObject = Database.query(queryString);
for (SObject parentRecord : queryParentObject) {
    Object parentFieldValue = parentRecord.get('Name');
    SObject[] childRecords = parentRecord.getSObjects('Contacts');
    if (childRecords != null) {
        for (SObject child : childRecords) {
            System.debug('Name: ' + parentFieldValue + '. Contact: '
                + child.get('FirstName') + ' ' + child.get('LastName'));
        }
    }
}

Dynamic Apex 是构建灵活、可扩展的 Salesforce 应用的关键技术——从运行时类型发现到动态查询构建和注入防护,这些能力使你的应用能够适配不同的组织配置和用户权限。