Salesforce 中批量删除自定义标签翻译的高效方法
在 Salesforce 开发或管理过程中,我们常常需要处理大量自定义标签(Custom Labels)的翻译数据。然而,Salesforce 的界面并不支持直接批量删除这些翻译记录,手动操作不仅耗时,还容易出错。本文将介绍一种基于 Tooling API 的解决方案,帮助开发者高效完成这一任务。
问题背景
Salesforce 的自定义标签翻译(Custom Label Translations)存储在 ExternalStringLocalization
对象中,每个翻译记录关联一个标签名称(ExternalString.Name
)和语言(Language
)。如果需要删除数百甚至上千条翻译记录,逐条操作显然不现实。而 Salesforce 的标准 API(如 REST API 或 Bulk API)对这一场景的支持有限,因此我们需要借助 Tooling API 来实现批量删除。
解决方案:利用 Tooling API 分两步操作
第一步:查询目标翻译记录的 ID
通过 Tooling API 的查询接口(/tooling/query
),我们可以编写 SOQL 查询语句,筛选出需要删除的翻译记录。例如,以下请求会获取简体中文(zh_CN
)语言下名为 Test_Lawrence
的标签翻译:
sql
GET Select+Id,+ExternalString.Name,+Language,+Value+from+ExternalStringLocalization+WHERE+Language +='zh_CN'+AND+ExternalString.Name+='Test_Lawrence'+LIMIT+99
-
关键点:
- 对象名称为
ExternalStringLocalization
。 - 支持按标签名称、语言、翻译值等条件筛选。
- 若结果超过一定记录条数,需通过
nextRecordsUrl
分页查询。
- 对象名称为
第二步:逐个删除翻译记录
获取到 ID 后,再次调用 Tooling API 的删除接口(/tooling/sobjects/ExternalStringLocalization/{ID}
),逐条删除记录。例如:
bash
DELETE /services/data/v55.0/tooling/sobjects/ExternalStringLocalization/01j2M00001dFLMsQAO
Apex 代码实现
以下是一个完整的 Apex 类,封装了上述逻辑:
typescript
public with sharing class CustomLabelTranslationDelete {
private static final String INSTANCE_URL = Url.getOrgDomainURL().toExternalForm();
private static final String TOOLING_ENDPOINT = '/services/data/v52.0/tooling/query/?q=';
private static final String TOOLING_DELETE_ENDPOINT = '/services/data/v55.0/tooling/sobjects/ExternalStringLocalization/';
//static List needed to accommodate multiple GET callouts
private static List<Records> customLabelTranslationRecords = new List<Records>();
public static void removeCustomLabelTranslations(Set<String> labelApiNames, String language){
if(labelApiNames == null || labelApiNames.isEmpty() || String.isBlank(language)){
return;
}
//First we find the Custom Label Translations
String externalStringQuery = INSTANCE_URL + TOOLING_ENDPOINT + 'Select+Id+from+ExternalStringLocalization+WHERE+Language+=\'' + language + '\'+AND+ExternalString.Name+IN+' +formatLabelNamesForQuery(labelApiNames) +'+LIMIT+99';
makeCustomLabelCallout(externalStringQuery, null);
//If we didn't find any Translations matching the criteria
if(customLabelTranslationRecords.isEmpty()){
return;
}
//Now that we have the record Ids we can generate all the delete endpoint we need to hit
List<String> labelTranslationEndpoints = new List<String>();
for(Records r : customLabelTranslationRecords){
if(String.isNotBlank(r.Id)) {
labelTranslationEndpoints.add(INSTANCE_URL + TOOLING_DELETE_ENDPOINT + r.Id);
}
}
if(!labelTranslationEndpoints.isEmpty()) {
//Now we fire off all of the REST API calls to delete the custom labels
customLabelTranslationDeleteCallout(labelTranslationEndpoints);
}
}
public static void makeCustomLabelCallout(String query, String nextQueryUrl) {
HttpRequest req = new HttpRequest();
// get Access Token
req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
req.setHeader('Content-Type', 'application/json');
req.setHeader('Accept', '*/*');
if(String.isBlank(nextQueryUrl)) {
req.setEndpoint(query);
}
else{
req.setEndpoint(INSTANCE_URL + nextQueryUrl);
}
req.setMethod('GET');
Http h = new Http();
HttpResponse res = h.send(req);
System.debug(res.getBody());
Response response = (Response) JSON.deserialize(res.getBody(), Response.class);
customLabelTranslationRecords.addAll(response.records);
if(!response.done){
makeCustomLabelCallout(null, response.nextRecordsUrl);
}
}
public static void customLabelTranslationDeleteCallout(List<String> requestUrls) {
HttpRequest req = new HttpRequest();
req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
req.setHeader('Content-Type', 'application/json');
req.setEndpoint(requestUrls[0]);
req.setMethod('DELETE');
Http h = new Http();
HttpResponse res = h.send(req);
requestUrls.remove(0);
if(!requestUrls.isEmpty()){
customLabelTranslationDeleteCallout(requestUrls);
}
}
private static String formatLabelNamesForQuery(Set<String> customLabelNames) {
String value = '(';
for (String s : customLabelNames) {
value += '\'' + s + '\'' + ',';
}
if (String.isNotBlank(value)) {
value = value.removeEnd(',');
value += ')';
}
return value;
}
public class Response {
public Integer size;
public Integer totalSize;
public Boolean done;
public String queryLocator;
public String nextRecordsUrl;
public String entityTypeName;
public List<Records> records;
}
public class Attributes {
public String type;
public String url;
}
public class ExternalString {
public Attributes attributes;
public String Name;
}
public class Records {
public Attributes attributes;
public String Id;
public String Name;
public String Language;
public String Value;
public ExternalString ExternalString;
}
}
使用方法
在匿名 Apex 中调用以下代码即可:
javascript
CustomLabelTranslationDelete.removeCustomLabelTranslations(new Set<String>{'Test_Lawrence'}, 'zh_CN');
注意事项
- 性能限制
每个 Apex 事务最多允许 100 次 HTTP 请求,因此单次调用最多删除 99 条记录。若需处理更多数据,可通过Queueable
或Batch Apex
链式调用。 - 语言与标签名称匹配
确保查询条件(如语言代码zh_CN
和标签名称)与目标数据完全匹配,避免误删。 - 测试环境验证
在生产环境执行前,务必在沙箱环境中测试代码逻辑。
总结
通过 Tooling API 的分页查询与递归删除功能,我们可以高效完成 Salesforce 中自定义标签翻译的批量清理工作。这一方法不仅适用于删除操作,还可扩展至其他复杂的数据管理场景。掌握此类技巧,能显著提升 Salesforce 系统的维护效率。