大对象

一个大对象在 Salesforce 上存储和管理大量数据 平台。您可以存档来自其他对象的数据,也可以从外部系统引入海量数据集 到一个大对象中,以获得客户的全貌。客户端和外部系统使用 用于访问大对象数据的标准 API 集。大对象提供一致的性能, 无论您拥有 100 万条记录、1 亿条记录,还是 10 亿条记录。这个比例给出了一个大对象 它的力量并定义了它的功能。

有两种类型的大物体。

  • 标准 大 objects – 对象 由 Salesforce 定义并包含在 Salesforce 产品中。 是一个标准的大对象,它将数据存储为 Field 的一部分 审计跟踪产品。标准大对象始终可用,无法自定义。FieldHistoryArchive
  • 定制大 对象 – 新建 您创建的用于存储组织特有信息的对象。自定义大对象扩展了 Lightning Platform 提供的功能。例如,如果您正在构建一个要跟踪的应用 产品库存,创建一个自定义大对象,用于跟踪历史库存水平以进行分析和 未来的优化。本实施指南用于配置和部署自定义 big 对象。HistoricalInventoryLevels

自定义大对象用例

  • 360°全景 的 customer – 扩展 您的 Salesforce 数据模型,包括来自忠诚度计划、提要、 点击次数、结算和配置信息等。
  • 审计和 tracking – 追踪 并保持对 Salesforce 或产品使用情况的长期看法,以便进行分析或合规性 目的。
  • 历史的 存档 – 维护 访问历史数据以进行分析或合规,同时优化性能 您的核心 CRM 或 Lightning 平台应用程序。

大对象与其他对象的区别

因为一个大对象可以无限尺度地存储数据,所以它具有不同的特征 而不是其他对象,如 sObjects。大对象也存储在 闪电平台。

大物体s对象
可水平扩展的分布式数据库关系数据库
非事务性数据库事务数据库
数亿甚至数十亿条记录数以百万计的记录

这些大对象行为可确保一致且可扩展的体验。

  • 大对象仅支持对象和字段权限,不支持常规或标准共享 规则。
  • 不支持触发器、流、流程和 Salesforce 移动应用程序等功能 大物体。
  • 当您多次插入具有相同表示的相同大对象记录时, 仅创建单个记录,以便写入可以是幂等的。此行为是不同的 从 sObject,它为每个创建对象的请求创建一条记录。

对大型对象的 API 支持

您可以轻松地将自定义大对象与您的实时 Salesforce 数据集成。你可以处理大 具有 SOQL、Bulk、Chatter 和 SOAP API 的对象。

注意

这些 API 是唯一受支持的 API 适用于大物体。例如,不支持 REST API。

另见

大对象最佳实践

大型对象是独一无二的,因为它能够针对大量对象进行扩展 数据。

适用于:Salesforce Classic 和 Lightning Experience
适用于:EnterprisePerformanceUnlimited 和 Developer Edition,最多可保存 100 万条记录

使用大对象时的注意事项

  • 要定义大对象或向自定义大对象添加字段,请使用元数据 API 或 设置。
  • 大型对象支持自定义 Lightning 和 Visualforce 组件,而不是标准 UI 元素主页、详细信息页面或列表视图。
  • 每个组织最多可以创建 100 个大对象。大对象字段的限制类似 对自定义对象的限制,并取决于组织的许可证类型。
  • 您不能使用 Salesforce Connect 外部对象访问其他组织中的大对象。
  • 大型对象不支持加密。如果从标准或 自定义对象,它以明文形式存储在大对象上。如果您使用 Salesforce Shield Platform 加密、标准或自定义对象字段历史记录已加密。 对于字段历史记录,使用 Shield 字段历史记录存档对数据进行存档。大物体 尊重静态加密。Shield Platform Encryption 则不然 支持自定义大对象。

在设计时考虑弹性

大对象数据库存储数十亿条记录,是一个有利于 一致性高于可用性。该数据库旨在确保行级别的一致性。

在使用 API 或 Apex 处理大数据和写入批量记录时,您可以 在写入某些记录而未写入其他记录时遇到部分批处理失败。因为 数据库在大规模上具有高度响应性和一致性,这种类型的行为是预期的。在 在这些情况下,只需重试,直到写入所有记录。

在处理大型对象时,请牢记这些原则。

  • 写入大对象时,最佳做法是采用重试机制。重试 批处理,直到您从 API 或 Apex 方法获得成功的结果。提示要添加 记录到自定义对象并向用户显示错误,请使用该方法。请参阅异常简介 处理。addError()提示若要验证是否保存了所有记录,请检查类。请参阅 SaveResult 类 参考。Database.SaveResult
  • 不要试图弄清楚哪些记录成功,哪些记录失败。重试整个 批。
  • 大对象不支持事务。如果尝试读取或写入大对象 sObject 上的触发器、进程或流,请使用异步 Apex。异步 Apex 具有以下功能 就像隔离 DML 的接口一样 对不同的 sObject 类型进行操作,以防止混合 DML 错误。Queueable
  • 由于客户端代码必须重试,因此请使用异步 Apex 写入大对象。由 异步写入,可以更好地处理数据库生命周期事件。

另见

定义和部署自定义大对象

您可以使用元数据 API 或在设置中定义自定义大对象。在定义和 部署一个大对象,您可以查看它或在设置中添加字段。在部署了大型 对象,则无法编辑或删除索引。要更改索引,请从新的大 对象。要在设置中定义大对象,请参阅 Salesforce 帮助。

定义自定义大对象

通过元数据 API 定义自定义大对象,方法是创建包含以下内容的 XML 文件 其定义、字段和索引。

  • 对象文件 – 为每个对象创建一个文件 定义自定义大对象、其字段及其索引。
  • 权限集/配置文件 – 创建权限集或配置文件以指定权限 对于每个字段。这些文件不是必需的,但需要授予 访问用户。默认情况下,对自定义大对象的访问是 限制。
  • package file – 为元数据 API 创建一个文件 指定要迁移的元数据的内容。注意包装 文件与 Salesforce 的打包功能无关。此文件 不是已解锁、非托管或托管的包。它只是一个文件 由元数据 API 使用。

注意

虽然自定义大对象使用 CustomObject 元数据类型,但某些 参数对于大型对象是唯一的,其他参数则不适用。具体的 适用于大型对象的元数据参数在此中概述 公文。

自定义大对象的命名约定

对象名称在所有标准对象、自定义对象、外部对象中必须是唯一的 对象,以及组织中的大对象。在 API 中,自定义大对象的名称 后缀为两个下划线,后跟小写字母“b”(__b)。为 例如,一个名为“HistoricalInventoryLevels”的大对象被视为 HistoricalInventoryLevels__b该组织的 WSDL 中。我们建议您制作 对象标签在组织中的所有对象中是唯一的 – 标准、自定义、外部和 大物体。

CustomObject 元数据

字段名称字段类型描述
deploymentStatusDeploymentStatus (枚举类型 字符串)自定义大对象的部署状态(适用于所有大对象 对象)Deployed
fields自定义字段[]大对象中字段的定义
fullName字符串大对象的唯一 API 名称
indexes索引[]指数的定义
label字符串UI 中显示的大对象名称
pluralLabel字符串UI 中显示的字段复数名称

CustomField 元数据

字段名称字段类型描述
fullName字符串字段的唯一 API 名称。
label字符串UI 中显示的字段名称。
lengthint字段的长度(以字符为单位)(Text 和 LongTextArea 字段 仅)。中所有文本字段中的字符总数 索引不能超过 100。要增加此值,请联系 Salesforce 客户支持。注意电子邮件字段为 80 个字符。电话字段 是 40 个字符。设计时请牢记这些长度 您的索引,因为它们计入 100 个字符 限制。
pluralLabel字符串UI 中显示的字段复数名称。
precisionint数字值的位数。例如,数字 256.99 的精度为 5(仅限数字字段)。
referenceTo字符串查阅字段的相关对象类型 (查阅字段 仅)。
relationshipName字符串UI 中显示的关系的名称(查找字段 仅)。
required布尔指定该字段是否为必填字段。属于 的索引必须标记为必需。
scaleint数字小数点右边的位数 价值。例如,数字 256.99 的小数位数为 2(数字 仅限字段)。
type字段类型字段类型。支持日期时间、电子邮件、查找、号码、电话、 Text、LongTextArea 和 URL。注意不能包含 LongTextArea 和 索引中的 URL 字段。

注意

自定义字段不支持唯一性。

索引元数据

表示在自定义 big 中定义的索引 对象。使用此元数据类型定义 自定义大对象。

字段名称字段类型描述
领域索引字段[]索引中字段的定义。
标签字符串必填。此名称用于指代用户界面中的大对象。 在 API 版本 41.0 及更高版本中可用。

IndexField 元数据

定义构成索引的字段、它们的顺序和排序 方向。定义字段的顺序决定了字段的列出顺序 在索引中。

注意

索引中所有文本字段的总字符数不能超过 100. 要增加此值,请联系 Salesforce 客户支持。

字段名称字段类型描述
名字字符串必填。属于索引的字段的 API 名称。此值必须 匹配相应值 字段,并标记为必填。fullName警告什么时候 通过 SOQL 查询一个大对象记录,并将结果作为参数传递给 删除 API,如果任何索引字段名称具有前导或尾随空格,则 无法删除大对象记录。
排序方向字符串必填。索引中字段的排序方向。有效值为升序和降序。ASCDESC

创建用于部署的元数据文件

以下 XML 摘录创建可部署的元数据文件。每个客户 交互对象表示在线视频中单个会话中的客户数据 游戏。、 和 字段定义索引,查找字段将 客户与 Account 对象的交互。Account__cGame_Platform__cPlay_Date__cCustomer_Interaction__b.object

<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
    <deploymentStatus>Deployed</deploymentStatus>

    <fields>
        <fullName>In_Game_Purchase__c</fullName>
        <label>In-Game Purchase</label>
        <length>16</length>
        <required>false</required>
        <type>Text</type>
        <unique>false</unique>
    </fields>
    
    <fields>
        <fullName>Level_Achieved__c</fullName>
        <label>Level Achieved</label>
        <length>16</length>
        <required>false</required>
        <type>Text</type>
        <unique>false</unique>
    </fields>
    
    <fields>
        <fullName>Lives_This_Game__c</fullName>
        <label>Lives Used This Game</label>
        <length>16</length>
        <required>false</required>
        <type>Text</type>
        <unique>false</unique>
    </fields>
    
    <fields>
        <fullName>Game_Platform__c</fullName>
        <label>Platform</label>
        <length>16</length>
        <required>true</required>
        <type>Text</type>
        <unique>false</unique>
    </fields>
    
     <fields>
        <fullName>Score_This_Game__c</fullName>
        <label>Score This Game</label>
        <length>16</length>
        <required>false</required>
        <type>Text</type>
        <unique>false</unique>
    </fields>

    <fields>
        <fullName>Account__c</fullName>
        <label>User Account</label>
        <referenceTo>Account</referenceTo>
        <relationshipName>Game_User_Account</relationshipName>
        <required>true</required>
        <type>Lookup</type>
    </fields>

    <fields>
        <fullName>Play_Date__c</fullName>
        <label>Date of Play</label>
        <required>true</required>
        <type>DateTime</type>
    </fields>

    <fields>
        <fullName>Play_Duration__c</fullName>
        <label>Play Duration</label>
        <required>false</required>
        <type>Number</type>
        <scale>2</scale>
        <precision>18</precision>
    </fields>

    <indexes>
        <fullName>CustomerInteractionsIndex</fullName>
        <label>Customer Interactions Index</label>
        <fields>
            <name>Account__c</name>
            <sortDirection>DESC</sortDirection>
        </fields>
        <fields>
            <name>Game_Platform__c</name>
            <sortDirection>ASC</sortDirection>
        </fields>
        <fields>
        <name>Play_Date__c</name>
        <sortDirection>DESC</sortDirection>
        </fields>        
    </indexes>
    
    <label>Customer Interaction</label>
    <pluralLabel>Customer Interactions</pluralLabel>
</CustomObject>

包.xml

<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <members>*</members>
        <name>CustomObject</name>
    </types>
    <types>
        <members>*</members>
        <name>PermissionSet</name>
    </types>
    <version>41.0</version>
</Package>

Customer_Interaction_BigObject.权限集

<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
     
    <label>Customer Interaction Permission Set</label>
     
    <fieldPermissions>
        <editable>true</editable>
        <field>Customer_Interaction__b.In_Game_Purchase__c</field>
        <readable>true</readable>
    </fieldPermissions>
     
    <fieldPermissions>
        <editable>true</editable>
        <field>Customer_Interaction__b.Level_Achieved__c</field>
        <readable>true</readable>
    </fieldPermissions>
     
    <fieldPermissions>
        <editable>true</editable>
        <field>Customer_Interaction__b.Lives_This_Game__c</field>
        <readable>true</readable>
    </fieldPermissions>
     
     
    <fieldPermissions>
        <editable>true</editable>
        <field>Customer_Interaction__b.Play_Duration__c</field>
        <readable>true</readable>
    </fieldPermissions>
     
    <fieldPermissions>
        <editable>true</editable>
        <field>Customer_Interaction__b.Score_This_Game__c</field>
        <readable>true</readable>
    </fieldPermissions>
     

</PermissionSet>

使用元数据 API 部署自定义大对象

使用 Metadata API 和 Ant Migration Tool 进行部署。在构建文件以部署自定义大对象时,请确保对象文件位于名为 objects 的文件夹中,并且权限集文件位于 在名为 permissionsets 的文件夹中。将 package.xml 文件放在根目录中,而不是放在 子文件夹。

在设置中查看自定义大对象

部署自定义大对象后,您可以通过登录到 组织,然后从“设置”中输入“快速查找”框,然后选择“大” 对象Big Objects

若要查看其字段和关系,请单击大对象的名称。

另见

使用 Zip 文件部署和检索元数据

和调用用于部署和检索 .zip 文件。在 .zip 文件是一个项目清单 (package.xml),其中列出了要执行的操作 检索或部署,以及组织到文件夹中的一个或多个 XML 组件。

deploy()retrieve()

注意

组件是元数据类型的实例。例如,是自定义对象的元数据类型,并且 该组件是 自定义对象。CustomObjectMyCustomObject__c

在 .zip 文件中检索或部署的文件可能是未打包的组件 驻留在组织中(例如标准对象)或打包组件 驻留在命名包中。

注意

您可以部署或 一次最多可检索 10,000 个文件。AppExchange 软件包使用不同的限制: 最多可包含 35,000 个文件。已部署或检索的 .zip 的最大大小 文件大小为 39 MB。如果文件在解压缩的文件夹中解压缩,则大小限制 是 400 MB。

  • 如果使用 Ant 迁移工具执行 部署解压后的文件夹,首先压缩文件夹中的所有文件。这 解压缩文件夹中未压缩组件的最大大小为 400 MB 或更少 取决于压缩比。如果文件具有高压缩比, 您总共可以迁移大约 400 MB,因为压缩的大小 将小于 39 MB。但是,如果组件不能被压缩太多,比如 二进制静态资源,可以迁移小于 400 MB。
  • 元数据 API base-64 对组件进行编码 它们被压缩了。生成的 .zip 文件不能超过 50 MB,即 SOAP 消息的限制。Base-64 编码会增加有效负载的大小,因此 在编码之前,压缩的有效负载不能超过大约 39 MB。
  • 您可以执行大 对象仅在定义其索引时才被定义。如果在安装程序中创建了一个大对象,并且 尚未定义索引,则无法检索它。retrieve()

每个 .zip 文件都包含一个项目清单、一个名为 package.xml 的文件以及一组包含 组件。清单文件定义您尝试执行的组件 在 .zip 文件中检索或部署。清单还定义了 API 版本 用于部署或检索。

注意

您可以编辑项目 清单,但如果您修改它所包含的组件列表,请小心。当你 部署或检索组件时,元数据 API 会引用 清单,而不是 .zip 文件中的目录。

下面是一个示例包 .xml 文件。您可以检索 通过在元素中指定元数据类型的 fullName 字段值来获取元数据类型的单个组件。您还可以 使用 检索元数据类型的所有组件。members<members>*</members>

<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <members>MyCustomObject__c</members>
        <name>CustomObject</name>
    </types>
    <types>
        <members>*</members>
        <name>CustomTab</name>
    </types>
    <types>
        <members>Standard</members>
        <name>Profile</name>
    </types>
    <version>59.0</version>
</Package>

可以在 package.xml 中定义以下元素。

  • <fullName>包含服务器端的名称 包。如果不存在,则 package.xml 定义客户端包。<fullName>unpackaged
  • <types>包含元数据类型的名称 (例如,) 和 成员(例如,) 为 检索或部署。您可以在清单文件中添加多个元素。CustomObjectmyCustomObject__c<types>
  • <members>包含组件的 fullName,例如 。该调用对 确定特定组件的 fullName 元数据类型(如果要检索单个组件)。对于许多元数据 类型,您可以将 中的值替换为通配符(星号) 而不是单独列出每个成员。有关特定 type 以确定该类型是否支持通配符。 每个组件 在元数据 API 部署中必须具有唯一的名称。解析为 重复项,例如一个符号和 UTF-8 编码符号以及一对名为 xyz.typename 和 xyz.typename-meta.xml 仍然是 重复。MyCustomObject__clistMetadata()members*注意在元素中指定 Security,在名称中指定 Settings 元素。<members>
  • <name>包含元数据类型,例如 或 。在 目录。任何扩展元数据的元数据类型都是有效值。输入的名称 必须与元数据 API WSDL 中定义的元数据类型匹配。有关列表,请参阅元数据类型。CustomObjectProfile
  • <version>是使用的 API 版本号 部署或检索 .zip 文件时。当前有效值为 。59.0

有关演示如何工作的更多示例包 .xml 清单文件 使用不同的元数据子集,请参阅示例包 .xml 清单 文件。

要删除组件,请参阅从 组织。

填充自定义大对象

使用 Salesforce API 填充自定义大对象。您可以使用 CSV 文件通过 Bulk API 2.0 将数据加载到自定义大对象中。这 CSV 文件中的第一行必须包含用于将 CSV 数据映射到的字段标签 导入期间自定义大对象中的字段。

注意

批量 API 和批量 API 2.0 支持查询和插入大对象。

插入是幂等的,因此插入已存在的数据不会导致 重复。在上传数百万条记录时,重新插入非常有用。如果错误 发生时,重新插入会重新上传失败的上传,而不会出现重复数据。在 重新插入时,如果提供的索引不存在任何记录,则新记录为 插入。例如,此 CSV 文件包含要导入 Customer Interaction 的数据 对象。

Play Start,In-Game Purchase,Level Achieved,Lives Used,Platform,Play Stop,Score,Account
2015-01-01T23:01:01Z,A12569,57,7,PC,2015-01-02T02:27:01Z,55736,001R000000302D3
2015-01-03T13:22:01Z,B78945,58,7,PC,2015-01-03T15:47:01Z,61209,001R000000302D3
2015-01-04T15:16:01Z,D12156,43,5,iOS,2015-01-04T16:55:01Z,36148,001R000000302D3

使用 Apex 填充自定义大对象

使用 Apex 填充自定义大对象。

您可以使用该方法在 Apex 中创建和更新自定义大对象记录。Database.insertImmediate()

警告

不允许使用混合 DML 调用的 Apex 测试,否则会失败。如果你写 仅对大对象,测试会将错误数据插入到目标大对象中,然后 必须手动删除。若要包含对目标大对象的测试 DML 调用,请使用模拟 框架替换为存根 API。

我们建议您在插入值之前使用 Apex 方法删除前导和尾随空格,尤其是对于 主键字段中的值。此最佳实践可确保 SOQL 查询 对象稍后将按预期工作。String.trim()

在 SOQL 查询 WHERE 子句中指定索引字段时,SOQL 会删除任何前导或 在将空格与实际字段值进行比较之前,先留出空格。即使您的过滤器 字符串与插入的值匹配,不比较前导和尾随空格, 因此,筛选器不会匹配任何行。

因此,前导和尾随空格无法与已 数据处理。即使筛选器字符串与插入的值匹配,也不会匹配任何行 通过过滤器。

如果在更新插入到自定义大对象时将值设置为 NULL,则字段不会 如果它们具有现有值,则更新。若要将这些值设置为 NULL,请删除字段和 重新创建它。

重新插入索引相同但数据不同的记录会导致类似于 更新插入操作。如果存在具有索引的记录,则插入将覆盖索引 值替换为新数据。插入是幂等的,因此插入不存在的数据 导致重复。在上传数百万条记录时,重新插入非常有用。如果错误 发生时,重新插入会重新上传失败的上传,而不会出现重复数据。在 重新插入,如果提供的索引不存在记录,则插入新记录。

如果记录插入失败,该方法不会引发异常。相反,它 返回一个对象,该对象具有返回对象列表的方法。每个对象都包含有关作为 记录插入失败的结果。请参阅 Apex 开发人员指南,了解有关 错误。Database.insertImmediate()SaveResultgetErrors()Database.ErrorDatabase.ErrorSaveResult下面是 Apex 中的插入操作示例,该操作假定一个表,其中索引 由 、 和 组成。

FirstName__cLastName__cAddress__c

// Define the record.
PhoneBook__b pb = new PhoneBook__b();
pb.FirstName__c = 'John';
pb.LastName__c = 'Smith';
pb.Address__c = '1 Market St';
pb.PhoneNumber__c = '555-1212';
database.insertImmediate(pb);
// A single record will be created in the big object.
// Define the record with the same index values but different phone number.
PhoneBook__b pb = new PhoneBook__b();
pb.FirstName__c = 'John';
pb.LastName__c = 'Smith';
pb.Address__c = '1 Market St';
pb.PhoneNumber__c = '415-555-1212';
database.insertImmediate(pb);
// The existing records will be "re-inserted". Only a single record will remain in the big object.
// Define the record with the different index values and different phone number
PhoneBook__b pb = new PhoneBook__b();
pb.FirstName__c = 'John';
pb.LastName__c = 'Smith';
pb.Address__c = 'Salesforce Tower';
pb.PhoneNumber__c = '415-555-1212';
database.insertImmediate(pb);
// A new record will be created leaving two records in the big object.

另见

删除自定义大对象中的数据

使用 Apex 或 SOAP 删除自定义大对象中的数据。

Apex 方法删除数据 在自定义大对象中。声明一个 sObject,其中包含 自定义大对象的索引。sObject 的作用类似于模板。与 sObject 的字段和值将被删除。您只能指定属于部分的字段 大对象的索引。您必须指定索引中的所有字段。你不能 包含部分指定的索引或非索引字段,并且不包含通配符 支持。deleteImmediate()

如果由于容量优化而删除所有记录,请插入一两条记录 删除后空白记录,并等待 24 小时以恢复新容量 认可。

重要

使用大型对象的批处理限制为 50,000 条记录 时间。deleteImmediate()

注意

这些示例假定,当您最初插入大对象值时,您使用了 要删除的 Apex 方法 索引字段中的前导和尾随空格。请参阅使用 Apex 填充自定义大对象。String.trim()

在此示例中,、 和 是自定义大对象索引的一部分。什么时候 在子句后指定特定值,字段必须按照它们在索引中的出现顺序列出,而不显示任何值 差距。Account__cGame_Platform__cPlay_Date__cWHERE

// Declare sObject using the index of the custom big object -->
List<Customer_Interaction__b> cBO = new List<Customer_Interaction__b>();
cBO.addAll([SELECT Account__c, Game_Platform__c, Play_Date__c FROM Customer_Interaction__b WHERE Account__c = '001d000000Ky3xIAB']);

Database.deleteImmediate(cBO);

要使用 SOAP 调用,请声明 一个 sObject,其中包含要删除的字段和值。sObject 的作用类似于 模板。与 sObject 的字段和值匹配的所有行都将被删除。您可以 仅指定属于大对象索引的字段。索引中的所有字段 必须指定。不能包含部分指定的索引或未编制索引的索引 字段和通配符不受支持。此示例删除 001d000000Ky3xIAB、iOS 和 2017-11-28T19:13:36.000z 的所有行。deleteByExample()Account__cGame_Platform__cPlay_Date__c

Java 示例代码:

public static void main(String[] args) {
  try{
       Customer_Interaction__b[] sObjectsToDelete = new Customer_Interaction__b[1];
       //Declare an sObject that has the values to delete
       Customer_Interaction__b customerBO = new Customer_Interaction__b();
       customerBO.setAccount__c (“001d000000Ky3xIAB”);
       customerBO.setGame_Platform__c (“iOS”);
       Calendar dt = new GregorianCalendar(2017, 11, 28, 19, 13, 36);
       customerBO.setPlay_Date__c(dt);
       sObjectsToDelete[0] = customerBO;
       DeleteByExampleResult[] result = connection.deleteByExample(sObjectsToDelete);
  }  catch (ConnectionException ce) {
	      ce.printStackTrace();  
  }
}

注意

重复成功的操作会产生成功的结果,即使这些行已被删除也是如此。deleteByExample()

大对象可排队示例

要使用 sObject 中的触发器、进程或流读取或写入大对象, 使用异步 Apex。此示例使用异步 Apex 接口来隔离 不同的 sObject 类型,以防止混合 DML 错误。

Queueable

插入案例记录时会发生此触发器。它调用一个方法来插入 一批大对象记录并演示了部分故障情况,其中某些 记录成功,有些失败。要在此示例中为对象创建元数据文件, 使用创建用于部署的元数据文件示例中的 XML 摘录。

Customer_Interaction__b

提示

将日志记录添加到自定义对象并将错误显示到 用户,请使用该方法。请参阅简介 异常处理。addError()

// CaseTrigger.apxt

trigger CaseTrigger on Case (before insert) {
    if (Trigger.operationType == TriggerOperation.BEFORE_INSERT){
        // Customer_Interaction__b has three required fields in its row key, in this order:
        // 1) Account__c - lookup to Account
        // 2) Game_Platform__c - Text(18)
        // 3) Play_Date__c - Date/Time
        List<Customer_Interaction__b> interactions = new List<Customer_Interaction__b>();
        
        // Assemble the list of big object records to be inserted
        for (Case c : Trigger.new) {
            Customer_Interaction__b ci = new Customer_Interaction__b(
                Account__c = c.AccountId,
                // In this example, the Case object has a custom field, also named Game_Platform__c
                Game_Platform__c = c.Game_Platform__c,
                Play_Date__c = Date.today()
            );
            interactions.add(ci);
        }
        
        // CustomerInteractionHandler is an asynchronous queuable Apex class
        CustomerInteractionHandler handler = new CustomerInteractionHandler(interactions);
        System.enqueueJob(handler);
    }
}

触发器使用 Apex 接口 异步调用要插入到大对象中的方法。Queueable

// CustomerInteractionHandler.apxc

public class CustomerInteractionHandler implements Queueable {
    
    private List<Customer_Interaction__b> interactions;
    
    public CustomerInteractionHandler(List<Customer_Interaction__b> interactions) {
        this.interactions = interactions;
    }
 
    /*
     * Here we insert the Customer Interaction big object records,
     * or log an error if insertion fails.
     */
    public void execute(QueueableContext context){
        
        List<ExceptionStorage__c> errors = new List<ExceptionStorage__c>();
        
        try {
            // We have to use insertImmediate() to insert big object records.
            List<Database.SaveResult> srList = Database.insertImmediate(interactions);
            
            // Check the save results from the bulk insert
            for (Database.SaveResult sr: srList) {
                if (sr.isSuccess()) {
                       System.debug('Successfully inserted Customer Interaction.');
                } else {
                       for (Database.Error err : sr.getErrors()) {
                        // Display an error message if the insert failed
                        System.debug(err.getStatusCode() + ': ' + err.getMessage() + '; ' +
                                    'Error fields: ' + err.getFields());
                        
                        // Write to a custom object, such as ExceptionStorage__c
                        // for a more durable record of the failure
                        ExceptionStorage__c es = new ExceptionStorage__c(
                               name = 'Error',
                               ExceptionMessage__c = (err.getMessage()).abbreviate(255),
                               ExceptionType__c = String.valueOf(err.getStatusCode()),
                            ExceptionFields__c = (String.valueOf(err.getFields())).abbreviate(255)
                        );
                        errors.add(es);
                    }
                }
            }
        }
        catch (Exception e) {
            // Exception occurred, output the exception message
            System.debug('Exception: ' + e.getTypeName() + ', ' + e.getMessage());
            
            // Write any errors to a custom object as well
            ExceptionStorage__c es = new ExceptionStorage__c(
                   name = 'Exception',
                   ExceptionMessage__c = e.getMessage(),
                   ExceptionType__c = e.getTypeName()
            );
            errors.add(es);
        }
        
        // If any errors occurred, save the ExceptionStorage records
        if (errors.size() > 0) {
               insert errors;
        }
    }
}

另见

大对象查询示例

了解一些常见的大对象查询用例。

客户 360 度和过滤

在此用例中,管理员从 外部源到Salesforce大对象中,然后处理数据以丰富 Salesforce 中的客户资料。目标是存储客户交易和 大范围内的互动,例如销售点数据、订单和订单项 对象,然后处理该数据并将其与核心 CRM 数据相关联。锚固 客户交易和与核心CRM数据的交互提供了更丰富的 360 度视图,可转化为增强的客户体验。

Batch Apex 是对大型对象或 ApiEvent 进行自动处理的最佳选择, ReportEvent 或 ListViewEvent。此示例演示如何添加处理 引用相关数据。对大对象运行批处理 Apex 查询,并关联联系人信息 与那个大 对象。

public class QueryBigObjectAndContact implements Database.Batchable<sObject> {
    private String key;
	public QueryBigObjectAndContact(String keyParam) {
        key = keyParam
    }
    
    public Iterable<SObject> start(Database.BatchableContext BC) {
		return [SELECT Big_Object_Field__c, Account__c FROM Big_Object__b WHERE Big_Object_Primary_Key > key LIMIT 50000]
    }

    public void execute(Database.BatchableContext bc, List<Big_Object__b> bos){
        // process the batch of big objects and associate them to Accounts
        Map<Id, Big_Object__b> accountIdToBigObjectMap = new Map<Id, Big_Object__b>();
        for (Big_Object__b bigObject : bos) {
            accountIdToBigObjectMap.put(bigObject.Account__c, bigObject);
            key = bigObject.Big_Object_Primariy_Key__c
        }
        Map<Id, Account> accountMap = new Map<Id, Account>(
            [SELECT Id, Name, ... FROM Account WHERE Id IN :accountIdToBigObjectMap.keySet()]
        );
        for (Id accountId : accountMap.keySet()) {
            Big_Object__b bigObject = accountIdToBigObjectMap.get(accountId);
            Account account = accountMap.get(accountId);
            // perform any actions that integrate the big object and Account
        }
    }
    public void finish(Database.BatchableContext bc){
        // You can daisy chain additional calls using the primary key of the big object to get around the 50k governor limit
        QueryBigObjectAndContact nextBatch = new QueryBigObjectAndContact(key);
        Database.executeBatch(nextBatch);
    }
}

现场审计跟踪

此示例演示如何在 CSV 中查询和分析大量结果 格式。FieldHistoryArchive

例 URI

/services/data/vXX.X/jobs/query

例 发布请求

{
    "operation": "query",
    "query": "SELECT ParentId, FieldHistoryType, Field, Id, NewValue, OldValueFROM FieldHistoryArchive WHERE FieldHistoryType = ‘Account’ AND CreatedDate > LAST_MONTH"
}

使用“获取查询作业的结果”资源。

示例 CURL 请求

curl --include --request GET \
--header "Authorization: Bearer token" \
--header "Accept: text/csv" \
https://instance.salesforce.com/services/data/vXX.X/jobs/query/750R0000000zxr8IAA/results ?maxRecords=50000

这 请求结果以 CSV 文件的形式提供,可用于检查以进行审核。

实时事件监控

通过实时事件监控,您可以跟踪谁在访问机密和 Salesforce 组织中的敏感数据。您可以查看有关个人的信息 事件或跟踪事件趋势,以快速识别异常行为并采取保护措施 贵公司的数据。这些功能对于遵守法规和 审计要求。

借助实时事件,您可以监控通过 API 调用访问的数据、报告 执行和列表视图。对应的事件对象称为 ApiEvent, ReportEvent 和 ListViewEvent。查询这些事件涵盖了许多常见方案 因为超过 50% 的 SOQL 查询是使用 SOAP、REST 或批量 API 进行的。钥匙 有关每个查询的信息,例如用户名、用户 ID、处理的行、 查询的实体和源 IP 地址存储在事件对象中。你 然后,可以对事件对象运行 SOQL 查询以发现用户活动详细信息。

有关详细信息,请参阅实时事件 监控。此示例演示如何使用字段的 内容。

public class EventMatchesObject implements Database.Batchable<sObject> {
    private String lastEventDate;

    public EventMatchesObject(String lastEventDateParam) {
        lastEventDate = lastEventDateParam;
    }

    public Iterable<SObject> start(Database.BatchableContext bc) {
        return [SELECT EventDate, EventIdentifier, QueriedEntities, SourceIp, Username, UserAgent FROM ApiEvent WHERE EventDate > lastEventDate LIMIT 50000]
    }

    public void execute(Database.BatchableContext bc, List<ApiEvent> events){
        // Process this list of entities if a certain attribute matches
        for (ApiEvent event: events) {
            String objectString = 'Patent__c';
            String eventIdentifier = event.EventIdentifier;
            if (eventIdentifier.contains(objectString) {
                // Perform actions on the event that contains 'Patent__c'
            }
            lastEventDate = format(event.EventDate);
        }
    }

    public void finish(Database.BatchableContext bc){         
        // You can daisy chain additional calls using EventDate or other filter fields to get around the 50k governor limit
        EventMatchesObject nextBatch = new EventMatchesObject(lastEventDate);
        Database.executeBatch(nextBatch);
    }
}

聚合查询

此示例显示了聚合的替代方法 查询与该方法类似。

COUNT()

public class CountBigObjects implements Database.Batchable<sObject> {
    private Integer recordsCounted;
    private String key;

	public CountBigObjects(Integer recordsCountedParam, String keyParam) {
        recordsCounted = recordsCountedParam
        key = keyParam
    } 

    public Iterable<SObject> start(Database.BatchableContext bc) {
        return [SELECT Custom_Field__c FROM Big_Object__b LIMIT 25000]
    }

    public void execute(Database.BatchableContext bc, List<Big_Object__b> bos){
        // process the batch of big objects and associate them to Accounts
        Map<Id, Big_Object__b> accountIdToBigObjectMap = new Map<Id, Big_Object__b>();
        for (Big_Object__b bigObject : bos) {
            accountIdToBigObjectMap.put(bigObject.Account__c, bigObject);
        }
        Map<Id, Account> accountMap = new Map<Id, Account>(
            [SELECT Id, Name, ... FROM Account WHERE Id IN :accountIdToBigObjectMap.keySet()]
        );
        for (Id accountId : accountMap.keySet()) {
            Big_Object__b bigObject = accountIdToBigObjectMap.get(accountId);
            Account account = accountMap.get(accountId);
            // perform any actions that integrate the big object and Account
        }
    }

    public void finish(Database.BatchableContext bc) {         
        // You can daisy chain additional calls using the primary key of the big object to get around the 50k governor limit
        CountBigObjects nextBatch = new CountBigObjects(recordsCounted, key);
        Database.executeBatch(nextBatch);
    }
}

在报告和仪表板中查看大对象数据

在处理大数据和数十亿条记录时,构建报告是不切实际的 或直接从该数据创建仪表板。请改用批量 API 编写一个查询,该查询提取 您感兴趣的较小、具有代表性的数据子集。您可以将其存储为 数据集,并在报告、仪表板或任何其他 Lightning Platform 中使用它 特征。

  1. 确定包含需要报告的数据的大对象。在这个 例如,Ride__b Big 对象包含完整的数据集。
  2. 创建自定义对象。此对象包含大对象数据的工作数据集,该数据集 你想报告。在此示例中,我们使用 Bike_Rental__c 自定义 对象。
    1. 在自定义对象的“可选功能”下,单击“允许” 报告
    2. 将自定义字段添加到对象中,使其与要从 大对象。
  3. 创建一个 SOQL 查询,该查询通过从大 对象添加到自定义对象中。提示确保您的工作数据集始终是最新的,以确保准确 报告时,将此作业设置为每晚运行。
  4. 使用您创建的工作数据集生成报表。
    1. 在“设置”中,在“快速查找”框中输入报告类型”,然后选择“报告类型”。
    2. 创建自定义报告类型。
    3. 对于主对象,从步骤 2 Bike_Rental__c中选择自定义对象。
    4. 将报表设置为“已部署”。
    5. 运行报表。

现在,您不仅可以在报表中使用工作数据集中的信息,还可以在报表中使用工作数据集中的信息。 也存在于仪表板或任何其他 Lightning Platform 功能中。

具有大对象的 SOQL

您可以通过以下方式查询大对象索引中的字段 使用标准 SOQL 命令的子集。

构建索引查询,从索引中定义的第一个字段开始,而不 查询中第一个字段和最后一个字段之间的间隙。您可以使用或在任何 字段,但只能使用一次。可以使用范围运算 、 、 或仅对查询的最后一个字段使用。=ININ<><=>=

提示

当您将子句与 只有一个参数,例如 ,它等价于 使用 ,例如 。 为清楚起见,我们建议您使用 这种情况。INFirstName IN(‘Charlie’)=FirstName=’Charlie’=不支持子查询。不要在你的 查询。例如,不支持此查询

Select CreatedById, CreatedDate, Created_Date__c, Id, Legacy_Record_ID__c, Parent_Case__c, SystemModstamp, Text_Body__c FROM Archived_Email_Message__b WHERE Parent_Case__c IN(select id from case where owner.id in ('00580000008BBVUAA4'))

您可以在查询中包含系统字段 、 和 。CreatedByIdCreatedDateSystemModstamp

若要保证查询结果的顺序,请使用子句。ORDER BY

这些查询假定您有一个表,其中的索引由 、 和 定义。LastName__cFirstName__cPhoneNumber__c

此查询指定索引中的所有三个字段。在这种情况下,筛选器可以使用范围 算子。PhoneNumber__c

SELECT LastName__c, FirstName__c, PhoneNumber__c
FROM Phone_Book__b
WHERE LastName__c='Kelly' AND FirstName__c='Charlie' AND PhoneNumber__c='2155555555'

此查询仅指定索引中的前两个字段。在本例中, 筛选器可以使用范围 算子。FirstName__c

SELECT LastName__c, FirstName__c, PhoneNumber__c
FROM Phone_Book__b
WHERE LastName__c='Kelly' AND FirstName__c='Charlie'

此查询仅指定索引中的第一个字段。筛选器可以使用范围运算符。LastName__c

SELECT LastName__c, FirstName__c, PhoneNumber__c
FROM Phone_Book__b
WHERE LastName__c='Kelly'

此查询使用第一个上的运算符 字段。IN

SELECT LastName__c, FirstName__c, PhoneNumber__c
FROM Phone_Book__b
WHERE LastName__c IN ('Kelly','Jones','Capulet','Montague') AND FirstName__c='Charlie'

此查询不起作用,因为查询中存在缺口。FirstName__c

SELECT LastName__c, FirstName__c, PhoneNumber__c
FROM Phone_Book__b
WHERE LastName__c='Kelly' AND PhoneNumber__c='2155555555'

此查询也不起作用,因为它使用运算符 两次。

IN

SELECT LastName__c, FirstName__c, PhoneNumber__c
FROM Phone_Book__b
WHERE LastName__c IN ('Kelly','Jones') AND FirstName__c IN ('Charlie','Lisa')

此查询有效,即使它在子句中似乎有两个运算符。 但因为第二个只有一个 参数,它等效于等于运算符,所以它是允许的。INWHEREIN

SELECT LastName__c, FirstName__c, PhoneNumber__c
FROM Phone_Book__b
WHERE LastName__c IN ('Kelly','Jones') AND FirstName__c IN ('Charlie')

为清楚起见,我们建议您重写前面的 SOQL 语句,如下所示。

SELECT LastName__c, FirstName__c, PhoneNumber__c
FROM Phone_Book__b
WHERE LastName__c IN ('Kelly','Jones') AND FirstName__c='Charlie'

不允许对大型对象执行 SOQL 操作

  • 生成索引查询时,不要在第一个和最后一个之间留出间隙 字段。
  • 运算符 、 、 和 在任何查询中都无效。!=LIKENOT INEXCLUDESINCLUDES
  • 聚合函数在任何查询中都无效。
  • 若要检索结果列表,请不要在查询中使用该字段。包含在查询中仅返回具有空 ID 的结果 (00000000000000000 或 00000000000000AAA)。IdId注意当您使用开发人员控制台执行以下操作时 从资源生成查询,该字段将自动包含在内。查询大对象 Developer Console,从 生成的查询。IdId

另见

API 生命周期终止政策

查看哪些 REST API 版本受支持、不受支持或不可用。

Salesforce 承诺支持每个 API 版本至少 3 个 自首次发布之日起的年。为了提高质量和性能 API,有时不再支持超过 3 年的版本。

Salesforce 通知使用计划的 API 版本的客户 为 折旧 至少 1 年后,对版本的支持将结束。

Salesforce API 版本版本支持状态版本停用信息
版本 31.0 至 59.0支持。
版本 21.0 至 30.0截至 22 年夏季,这些版本已被弃用,并且没有 Salesforce 支持的时间更长。从 25 年夏季开始,这些 版本将停用且不可用。Salesforce Platform API 版本 21.0 到 30.0 停用
版本 7.0 至 20.0自 22 年夏季起,这些版本已停用,并且 不能利用的。Salesforce Platform API 版本 7.0 到 20.0 停用

如果你 请求任何资源或使用已停用的 API 版本 REST API 中的操作 返回错误代码。410:GONE

识别 从旧的或不受支持的 API 版本发出的请求,请使用 API 总使用量事件类型。

具有大量数据的部署的最佳实践

谁应该阅读此文

本文适用于使用 Salesforce 部署的经验丰富的应用程序架构师,这些部署具有以下特点: 包含大量数据。

“大数据量”是一个不精确的弹性术语。如果您的部署有数十个 数千个用户、数千万条记录或数百 GB 的总记录 存储,您拥有大量数据。即使使用较小的部署,您仍然可以 从这些最佳实践中学习一些东西。

要了解本文中涉及 Salesforce 实施细节的部分, 阅读 https://developer.salesforce.com/page/Multi_Tenant_Architecture

概述

Salesforce 使客户能够轻松地将其应用程序从小批量扩展到大量 的数据。这种缩放通常是自动发生的,但随着数据集变大,时间 某些操作所需的增长。架构师设计和配置数据的方式 结构和操作可以将这些操作时间增加或减少几个数量级 大小。受不同体系结构和配置影响的主要进程包括:

  • 直接或集成加载或更新大量记录
  • 通过报告和查询或视图提取数据

优化这些主要流程的策略包括:

  • 遵循行业标准做法,以适应 启用数据库的应用程序
  • 延迟或绕过业务规则和共享处理
  • 选择最有效的操作来完成任务

本文内容

  • 提高具有大量数据的应用程序性能的技术
  • 影响绩效的 Salesforce 机制和实施不太明显 方式
  • Salesforce 机制,旨在支持具有大数据的系统的性能 卷

Salesforce 大对象

Salesforce 提供称为 Big Objects 的大数据技术。一个大对象存储和管理 Salesforce 平台上的大量数据。您可以存档来自其他对象的数据,或者 将来自外部系统的大量数据集整合到一个大对象中,以全面了解您的 客户。一个大对象提供一致的性能,无论您有 100 万条记录,还是 100 条记录 百万,甚至10亿。这种比例赋予了大物体力量并定义了它 特征。

本文重点介绍如何优化存储在标准和自定义对象中的大量数据,而不是 大物体。实现最佳性能和可持续的长期存储解决方案,甚至 较大的数据集,请使用 Bulk API 或 Batch Apex 将数据移动到大型对象中。

基本概念

本部分概述了两个关键概念:多租户和搜索 架构,以解释 Salesforce 如何:

  • 将其应用程序提供给客户的实例和组织
  • 保持受支持的自定义安全、独立和高 执行
  • 跟踪和存储应用程序数据
  • 为该数据编制索引以优化搜索
  • 多租户和元数据概述
  • 搜索体系结构

多租户和元数据概述

多租户是提供单个应用程序的一种方式 到多个组织,例如不同的公司或部门 在公司内部,来自单个软硬件堆栈。而不是 为每个提供一整套硬件和软件资源 组织,Salesforce 在单个实例和每个实例之间插入一层软件 组织的部署。这一层对组织是不可见的, 它们只能看到自己的数据和架构,而 Salesforce 会在后台重新组织数据以执行高效的操作。

多租户要求应用程序能够可靠地运行,即使在 架构师正在进行 Salesforce 支持的自定义,其中包括创建自定义数据对象、 更改接口,并定义业务规则。为确保 特定于租户的自定义不会破坏其他自定义项的安全性 租户或影响其性能,Salesforce 使用运行时引擎从 这些自定义。通过保持架构之间的边界 Salesforce 保护每个租户的数据和操作的完整性。

当组织创建自定义对象时,平台会跟踪元数据 关于对象及其字段、关系和其他对象 定义特征。Salesforce 将所有虚拟表的应用程序数据存储在几个大型表中 数据库表,按租户分区并用作堆 存储。然后,平台的引擎将虚拟表具体化 运行时的数据,通过考虑相应的元数据。

而不是试图管理一个庞大的、不断变化的实际 每个应用程序和租户、平台的数据库结构 存储模型使用一组元数据管理虚拟数据库结构, 数据和数据透视表。因此,如果应用传统的性能调优 基于组织的数据和架构的技术,您可以 可能无法看到您对实际基础数据的预期影响 结构。

注意

作为客户,您也无法优化 SQL 底层 许多应用程序操作,因为它是由系统生成的, 不是由每个租户编写的。

搜索体系结构

搜索是基于自由格式文本查询记录的功能。Salesforce 搜索架构是 基于其自己的数据存储,该数据存储针对搜索该文本进行了优化。Salesforce 提供搜索 在应用的许多领域具有功能,包括:

  • 侧边栏
  • 高级搜索和全局搜索
  • 查找框和查找字段
  • 建议的解决方案和知识库
  • Web-to-Lead 和 Web-to-Case
  • 重复的潜在客户处理
  • 适用于 Apex 和 API 的 Salesforce 对象搜索语言 (SOSL)

对于要搜索的数据,必须首先对其进行索引。索引是使用搜索创建的 索引服务器,这些服务器还会生成和异步处理新 创建或修改的数据。创建或更新可搜索对象的记录后,它会 更新后的文本可能需要大约 15 分钟或更长时间才能变得可搜索。

Salesforce 执行索引 首先在索引中搜索适当的记录,然后缩小范围 基于访问权限、搜索限制和其他过滤器的结果。此过程将创建 结果集,通常包含最相关的结果。结果出来后 集合达到预定大小,剩余的记录将被丢弃。结果集是 然后用于从数据库中查询记录以检索用户 看到。

提示

也可以使用 SOSL 访问搜索,而 SOSL 又可以 使用 API 或 Apex 调用。

适用于大型系统的基础架构 数据卷

本节概述:

  • 直接支持性能的 Salesforce 组件和功能 具有大量数据的系统
  • Salesforce 使用这些组件和功能的情况
  • 最大限度地利用 Salesforce 基础架构的优势的方法
  • Lightning Platform 查询优化器 Salesforce 多租户架构使用底层数据库的方式使得数据库系统的优化器
    无法有效优化搜索查询。Lightning Platform 查询优化器通过在 Salesforce 中提供高效的数据访问来帮助数据库的优化器生成有效的查询。
  • 数据库统计
  • 瘦表
  • 索引
  • 部门

Lightning Platform 查询优化器

Salesforce 多租户架构使用底层数据库的方式是: 数据库系统的优化器无法有效地优化搜索查询。

Lightning Platform 查询优化器帮助数据库的优化器 通过在 Salesforce 中提供高效的数据访问来生成有效的查询。

重要

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

Lightning Platform 查询优化器适用于自动生成的查询,这些查询可处理 报表、列表视图和 SOQL 查询。优化器还处理依赖于 这些生成的查询。具体来说,优化器:

  • 如果可能,根据 查询
  • 确定从中驱动查询的最佳表(如果没有可用的良好索引)
  • 确定如何对剩余表进行排序以最大程度地降低成本
  • 注入创建高效联接所需的自定义外键值表 路径
  • 影响剩余联接(包括共享联接)的执行计划,以最大程度地减少 数据库输入和输出 (I/O)
  • 更新统计信息

创建高效查询

当您处理大量数据时,构建高效的 SOQL 非常重要 查询、报表和列表视图,它们都依赖于选择性和索引。闪电 平台查询优化器确定 SOQL 查询、报告或 列表视图。通过一些简单的 SOQL 查询,您可以轻松地获得必要的统计信息 确定特定筛选条件是否具有选择性。为选择性字段编制索引后, 具有相关筛选条件的查询可以更高效地执行,并且您的用户可以 更高效。

查看以下测量过滤器选择性的实际注意事项 条件。

确定过滤条件的选择性

为了更好地理解选择性,让我们举个例子。生成 SOQL 查询、报表或 Opportunity 对象的列表视图,该对象是组织中最大的对象之一。你有一个过滤器 条件,例如,仅获取 要从对象获取的行。您的过滤条件是否有足够的选择性,以便优化程序能够 使用可用的索引?WHERE

通过简单的 SOQL 查询,您可以快速获取统计信息,帮助您确定是否 给定字段中的值是选择性的。

使用 SOQL 确定筛选条件的选择性

考虑使用基本一元子句的查询 条件。WHERE

SELECT Id, Name FROM Opportunity
WHERE Stagename = 'Closed Won'

使用您选择的查询工具(例如开发人员控制台查询编辑器),执行 以下查询用于获取与筛选条件的选择性相关的一些统计信息。这 以下是 Stagename 字段的示例查询:

SELECT Stagename, COUNT(id) FROM Opportunity
GROUP BY ROLLUP (Stagename)

结果集显示 Stagename 选择列表字段的每个值的记录分布情况,包括 对象。现在,您拥有了确定过滤器选择性所需的统计数据 涉及 Stagename 字段的条件。

确定更复杂过滤条件的选择性

在类似于上一个查询的查询中使用 示例可以很容易地获得必要的统计数据来评估各种 条件。GROUP BY ROLLUP

下面是具有更复杂筛选条件的查询示例。此查询使用日期 字段 (CloseDate) 以及运算符:AND

SELECT Id, Name FROM Opportunity
WHERE Stagename = 'Closed Won'
AND CloseDate = THIS_WEEK

您已经知道上一个 Stagename 字段的统计信息 查询。获取 CloseDate 字段的相同统计信息(按周分组) 对于每一年,请使用以下查询。

SELECT WEEK_IN_YEAR(CloseDate), CALENDAR_YEAR(CloseDate), COUNT(id)
FROM Opportunity
GROUP BY ROLLUP(WEEK_IN_YEAR(CloseDate),CALENDAR_YEAR(CloseDate))
ORDER BY CALENDAR_YEAR(CloseDate), WEEK_IN_YEAR(CloseDate)

该查询返回有关每周 Opportunity 记录分布的统计信息 每年的 CloseDate。

对于组合了两个或多个条件的过滤条件 (using ),查询优化器在 筛选目标小于:AND

  • 每个过滤器的选择性阈值是两倍
  • 这些字段相交的选择性阈值

对于本主题中的第三个示例,这意味着:

  • Status = ‘Closed Won’是选择性的(49,899 < 150,000)
  • CloseDate = THIS_WEEK是选择性的(~3000 < 150,000)

总体而言,由于这两个原因,过滤条件是选择性的。

如果其中一个筛选条件是非选择性的,例如,对应于 250,000 条记录,则两种可能性可以使 整体过滤条件选择性。Status=’Closed Won’

  • 每个筛选条件对应于少于 300,000 条记录(选择性是 每个过滤器的阈值)
  • 的交集小于 150,000 条记录。Status=’Closed Won’ AND CloseDate = THIS_WEEK

示例中的筛选条件小于 300,000 条记录,因此总体条件为 选择性。

提示

与操作员一起,每个过滤器必须满足 阈值。OR

了解已删除记录对选择性的影响

收集选择性统计信息时,可以使用 布尔字段 IsDeleted。此字段在每个标准和 自定义对象。

使用该函数收集的上一个查询 所有 Opportunity 记录的数据。IsDeleted 是还是 .自 收集 Opportunity.StageName 的选择性统计信息,并显式收集 排除已删除的记录,请尝试以下查询。ROLLUPtruefalse

SELECT Stagename, COUNT(id) FROM Opportunity WHERE IsDeleted=false GROUP BY Stagename

确保字段具有索引

在将查询、报表或列表视图投入生产之前,请确认给定的筛选器 条件是选择性的。但是,使条件具有选择性的字段必须具有索引 要有效。如果没有必要的索引,查询优化器必须执行完全扫描以 获取目标行。该索引有助于提高查询的执行速度,并改进 组织用户的工作效率。

Database Query & Search Optimization 备忘单列出了默认具有索引的标准字段,包括 Id、Name、OwnerId、CreatedDate、SystemModstamp 和 RecordType,以及所有 主从和查找字段。

如果您的筛选条件涉及自定义字段,请与客户支持部门合作,创建一个 筛选器使用的字段的自定义索引。并非所有字段都可以有索引,例如 非确定性公式字段。

数据库统计

现代数据库收集有关存储在其中的数据的数量和类型的统计信息,以及 他们使用此信息来有效地执行查询。由于 Salesforce 的多租户 在软件架构上,平台必须保留自己的一套统计信息 帮助数据库了解访问数据的最佳方式。因此,当大量 使用 API 创建、更新或删除的数据时,数据库必须收集统计信息 在应用程序可以有效地访问数据之前。目前,此统计收集过程 每晚运行。

瘦表

Salesforce 可以创建瘦表来包含常用字段,并 避免联接。这可以提高某些只读操作的性能。瘦桌子 在修改源表时,与源表保持同步。

如果要使用瘦表,请联系 Salesforce 客户支持。启用后,瘦 在适当的情况下自动创建和使用表。您无法创建、访问或 自行修改瘦表。如果要优化报表、列表视图或查询 更改(例如,要添加新字段),您需要联系 Salesforce 以更新您的瘦身 表定义。

瘦表如何提高性能

对于您可见的每个对象表,Salesforce 会在 标准字段和自定义字段的数据库级别。这种分离,是看不见的 当查询包含这两种字段时,客户通常需要联接。一个瘦弱的 表包含这两种类型的字段,并且还省略了软删除的记录。此表显示 Account 视图、相应的数据库表和 可以加快帐户查询速度。

仅引用瘦表中的字段的只读操作不需要额外的 加入,因此可以表现得更好。瘦表对表最有用 包含数百万条记录,以提高只读操作的性能,例如 报告。

重要

瘦桌子不是挥舞性能的魔杖 问题。维护保存实时数据副本的单独表会产生开销。 在不适当的上下文中使用它们可能会导致性能下降,而不是 起色。

可以在自定义对象上创建瘦表,也可以在 Account、Contact、Opportunity、Lead、 和 Case 对象。它们可以增强报表、列表视图和 SOQL 的性能。瘦表可以包含以下类型的字段。

  • 复选框
  • 日期
  • 日期和时间
  • 电子邮件
  • 百分之
  • 电话
  • 选择列表(多选)
  • 发短信
  • 文本区域
  • 文本区域(长)
  • 网址

瘦表和瘦索引也可以包含加密数据。

下面是一个示例,说明瘦表如何加快查询速度。而不是使用日期范围 喜欢 – 这需要 创建年度或年初至今报告的昂贵重复计算 – 您可以 使用瘦表包含 Year 字段并筛选 。01/01/1112/31/11Year = '2011'

考虑

  • 瘦表最多可以包含 100 列。
  • 瘦表不能包含来自其他对象的字段。
  • 对于完整沙盒:瘦表将复制到完整沙盒组织。对于其他 沙盒类型:瘦表不会复制到您的沙盒组织。要有 为完整沙箱以外的沙盒类型激活的生产瘦表,请联系 Salesforce 客户支持。

索引

重要

在可能的情况下,我们更改了非包容性条款,以符合我们的 平等的公司价值观。我们保留了某些条款,以避免对 客户实施。Salesforce 支持自定义索引以加快查询速度,您可以通过以下方式创建自定义索引 联系 Salesforce 客户支持。

注意

Salesforce Customer 的自定义索引 在生产环境中创建的支持将复制到您创建的所有沙盒中 从该生产环境。对于大多数对象,平台在以下字段上维护索引。

  • RecordTypeId
  • 划分
  • 创建日期
  • Systemmodstamp (LastModifiedDate))
  • 名字
  • 电子邮件(用于联系人和潜在客户)
  • 外键关系(查找和大纲-细节)
  • 唯一的 Salesforce 记录 ID,它是每个对象的主键

Salesforce 还支持自定义字段的自定义索引,但多选选择列表、文本除外 区域(长)、文本区域(丰富)、非确定性公式字段和加密文本 领域。

外部 ID 会导致在该字段上创建索引。然后查询优化器 考虑这些字段。您只能在以下字段上创建外部 ID。

  • 自动编号
  • 电子邮件
  • 发短信

要为其他字段类型(包括标准字段)创建自定义索引,请联系 Salesforce 客户支持。

索引表

Salesforce 多租户架构为自定义字段制作基础数据表 不适合索引。为了克服此限制,平台创建了一个索引表 包含数据副本以及有关数据类型的信息。

平台在此索引表上构建标准数据库索引。索引表将上 对索引搜索可以有效返回的记录数的限制。

默认情况下,索引表不包括 null 记录(具有空值的记录)。你 可以与 Salesforce 客户支持合作,创建包含空行的自定义索引。 即使自定义字段上已有自定义索引,也必须显式启用 并重新生成它们以对空值行进行索引。

标准 和自定义索引字段

查询优化器维护一个表,其中包含有关 每个索引中的数据。它使用此表执行预查询,以确定是否使用 index 可以加快查询速度。

为 例如,假设 Account 对象有一个名为 Account_Type 的字段,该字段可以采用值 、 或 ,并且 字段具有自定义索引。LargeMediumSmall

例如,Salesforce 会生成如下查询:

SELECT *
FROM Account
WHERE Account_Type__c = 'Large'

查询优化器对其内部统计信息表执行预查询,以确定数字 在Account_Type字段中的记录。如果此数字超过对象的 10% 总记录数或 333,333 条记录,则查询不使用自定义索引。Large

查询优化器确定索引的用途。标准索引字段如果筛选器匹配的前 100 万条记录少于 30%,并且匹配的 附加记录,最多 100 万条记录。

例如,标准 在以下情况下使用索引:

  • 对包含 200 万条记录的表执行查询,并且筛选器匹配 450,000 条或更少的记录。
  • 对包含 500 万条记录的表执行查询,并且筛选器匹配 900,000 条或更少的记录。

自定义索引字段如果筛选器匹配的记录少于总数的 10%,则使用,最多 333,333 记录。例如,在以下情况下使用自定义索引:

  • 对包含 500,000 条记录的表执行查询,并且筛选器匹配 50,000 条或更少的记录。
  • 对包含 500 万条记录的表执行查询,筛选器 匹配 333,333 条或更少的记录。

如果不满足索引字段的条件,则只会从查询中排除该索引。如果 它们在子句中并满足以下阈值 记录,有时会使用其他索引。WHERE

查询优化器使用类似的注意事项来确定是否使用索引 当子句包含 、 或 .WHEREANDORLIKE

  • 对于 ,查询优化器使用索引 除非其中一个返回对象记录的 20% 以上或总共 666,666 个 记录。AND
  • 对于 ,查询优化器使用索引,除非 它们都返回超过 10% 的对象记录,即总共 333,333 条记录 记录。OR注意子句中的所有字段必须是 为要使用的任何索引编制索引。OR
  • 对于 ,查询优化器不使用其内部 统计表。相反,它会对多达 100,000 条实际数据记录进行采样来决定 是否使用自定义索引。LIKE

可以在确定性公式字段上创建自定义索引。因为有些值会随时间而变化 或者当交易更新相关实体时发生更改,平台无法索引 非确定性公式。

以下是示例 使公式字段具有不确定性的事物。非确定性公式字段可以:

  • 引用其他实体(如可通过查找字段访问的字段)
  • 包括跨其他实体的其他公式字段
  • 使用动态日期和时间函数(例如,和TODAYNOW)

这些公式字段也被视为非确定性字段

  • 所有者、自动编号、部门或审核字段(CreatedDate 和 CreatedByID 字段除外)
    • 对 Lightning Platform 无法索引的字段的引用
    • 多选选择列表
    • 多币种组织中的货币字段
    • 长文本区域字段
    • 二进制字段(blob、文件或加密文本)
  • 具有特殊功能的标准字段
    • 商机:Amount、TotalOpportunityQuantity、ExpectedRevenue、IsClosed、IsWon
    • 案例:ClosedDate、IsClosed
    • 产品:ProductFamily、IsActive、IsArchived
    • 解决方案:状态
    • 线索:状态
    • 活动:Subject、TaskStatus、TaskPriority

注意

如果在创建索引后修改了公式,则会重新生成索引。如果使用跨对象表示法指定,则通常使用跨对象索引,因为它们是 在下文中 例。

SELECT Id
FROM Score__c
WHERE CrossObject1__r.CrossObject2__r.IndexedField__c

您可以使用此方法替换无法自定义索引的公式字段,因为它们 引用其他对象。只要对引用的字段进行索引,跨对象 表示法可以有多个级别。

两列自定义索引

两列自定义索引是 Salesforce 平台的一项特殊功能。它们很有用 对于列表视图以及要使用一个字段选择要显示的记录的情况 和另一个字段对它们进行排序。例如,选择依据和排序依据的“帐户”列表视图可以 在第一列和第二列中使用两列索引。StateCityStateCity当两个字段的组合是查询字符串中的常见筛选器时,两列索引 通常可帮助您对记录进行排序和显示。例如,对于以下 SOQL,其中 出现在伪代码中,两列索引 on 比单索引 on 更有效。

f1__c,f2__cf1__c and f2__c

SELECT Name
FROM Account
WHERE f1__c = 'foo'
     AND f2__c = 'bar'

注意

两列索引与单列索引受到相同的限制,一个 例外。两列索引的第二列中可以有 null,而单列索引可以有 null 值 索引不能,除非 Salesforce 客户支持明确启用该选项以包含 null 值。

区域

划分是对大型数据进行分区的一种手段 部署以减少查询返回的记录数,以及 报告。例如,具有许多客户记录的部署可能 创建名为 、 的部门,并将客户分成更小的组,这些组可能 几乎没有相互关系。USEMEAAPAC

Salesforce 为按部门划分数据提供了特殊支持,这 您可以通过联系 Salesforce 客户来启用 支持。

优化性能的技术

本节概述:

  • 优化 Salesforce 性能的技术
  • 作为基础的安排、特点、机制和选项 这些技术
  • 您应该使用这些技术和定制的情况 他们满足您的需求
  • 使用 Mashup
  • 延迟共享计算
  • 使用 SOQL 和 SOSL
  • 删除数据
  • 搜索

使用 Mashup

减少 Salesforce 中数据量的一种方法是在 其他应用程序,然后根据需要将该应用程序提供给 Salesforce。Salesforce将这样的安排称为 Mashup,因为它提供了两个应用程序的快速、松散耦合的集成。 Mashup 使用 Salesforce 演示文稿来 显示 Salesforce 托管的数据和 外部托管的数据。Salesforce 支持以下功能 Mashup 设计。外部网站Salesforce UI 显示 外部网站,并向其传递信息和请求。通过这种设计,您可以制作 该网站看起来像 Salesforce UI 的一部分。标注Apex 代码允许 Salesforce 使用 用于与外部系统实时交换信息的 Web 服务。

由于其实时性限制,mashup 仅限于短交互和小 数据量。

请参阅 Apex 开发人员指南。

使用混搭的优点

  • 数据永远不会过时。
  • 无需开发专有方法来集成这两个系统。

使用混搭的缺点

  • 访问数据需要更多时间。
  • 功能减少。例如,报告和工作流在外部不起作用 数据。

延迟共享计算

在某些情况下,使用某项功能可能是合适的 称为延迟共享计算,允许用户延迟 在新用户、规则和 其他内容已加载。

组织的管理员可以使用延迟共享 暂停和恢复共享计算的计算权限, 并管理两个流程:组成员资格计算和共享 规则计算。管理员可以暂停这些计算 执行大量配置更改时,可能会 导致非常长的共享规则评估或超时,并恢复 组织维护期间的计算。这 延时可以帮助用户处理大量与共享相关的内容 配置在工作时间内快速更改,然后让 重新计算过程在工作日之间或更长时间内运行一夜之间 一个周末。

使用 SOQL 和 SOSL

SOQL 查询等同于 SQL 语句和 SOSL 查询 是执行基于文本的搜索的编程方式。SELECT

索克尔SOSL公司
执行方式数据库搜索索引
使用query()search()

在以下情况下使用 SOQL:

  • 您知道数据驻留在哪些对象或字段中。
  • 您希望:
    • 从单个对象或多个对象中检索数据 彼此相关
    • 计算符合指定条件的记录数
    • 在查询过程中对结果进行排序
    • 从数字、日期或复选框字段中检索数据

在以下情况下使用 SOSL:

  • 您不知道数据驻留在哪个对象或字段中, 您希望以最有效的方式找到它。
  • 您希望:
    • 高效检索多个对象和字段,以及这些对象 可能彼此相关,也可能不相关
    • 使用以下方法检索组织中特定部门的数据 部门功能,您希望在最有效的位置找到它 可能的方式

使用 SOQL 或 SOSL 时,请考虑以下事项。

  • SOQL 筛选器和 SOSL 搜索查询都可以指定 您应该寻找的文本。当给定的搜索可以使用任何一种语言时,如果搜索表达式 使用术语。WHERECONTAINS
  • SOSL公司 可以标记一个字段中的多个术语(例如,以 spaces),并在此基础上构建搜索索引。如果您正在搜索特定的非 您知道某个字段中存在的术语,您可能会发现 SOSL 在这些搜索中比 SOQL 更快。为 例如,如果出现以下情况,则可以使用 SOSL 您正在针对包含“Paul 和 John”等值的字段搜索“John” 公司”。
  • 在某些情况下,当 SOQL 中使用多个筛选器时,即使可以对子句中的字段进行索引,也无法使用索引。在这种情况下,将单个查询分解为多个查询,每个查询都应有一个筛选器,然后 合并结果。WHEREWHEREWHERE
  • 使用具有选择列表或外键字段的 null 值的筛选器执行查询不会使用索引,应避免使用索引。WHERE为 示例,以下客户查询执行 不好。
SELECT Contact__c, Max_Score__c, CategoryName__c, Category__Team_Name__c
FROM Interest__c
WHERE Contact__c != null
    AND Contact__c IN :contacts
    AND override__c != 0
    AND (
            (override__c != null AND override__c > 0)
            OR
            (score__c != null AND score__c > 0)
        )
    AND Category__c != null
    AND (
            (Category_Team_IsActive__c = true OR CategoryName__c IN :selectvalues)
            AND
            (
                Category_Team_Name__c != null
                AND
                Category_Team_Name__c IN :selectTeamValues
            )
        )

(项目 前面有冒号,例如 ,是 Apex 变量。请参阅使用 Apex 《Apex 开发人员指南》中的 SOQL 和 SOSL 查询中的变量。 在标准中阻止了索引的使用,并且一些 这些条件是多余的,导致执行时间增加。设计数据模型,使其不依赖于有效字段 值。

:contactsNullsnulls

可以重写查询 如:

SELECT Contact__c, Max_Score__c, CategoryName__c, Category_Team_Name__c
FROM Interest__c
WHERE Contact__c IN :contacts
    AND (override__c > 0 OR score__c > 0)
    AND Category__c != 'Default'
    AND (
            (Category_Team_IsActive__c = true OR CategoryName__c IN :selectvalues)
            AND
            Category_Team_Name__c IN :selectTeamValues
        )

为 字段 Category__c,该值被替换为 ,允许索引为 用于该字段。DefaultNULL

再举一个例子,如果动态值用于 WHERE 字段,并且可以传入 null 值,则不要让查询 运行以确定没有记录;相反,如果可能,请检查 null 值并避免查询。按帐户的外键帐号检索帐户的查询可能如下所示 this(在伪代码中)。

SELECT Name
   FROM Account
   WHERE Account_ID___c = :acctid;

if (rows found == 0) return "Not Found"

如果 acctid 为 ,则逐行扫描整个 Account 表,直到检查所有数据。null最好重写 代码 如:

if (acctid != null) {
   SELECT Name
      FROM Account
      WHERE Account_Id___c = :acctid
}
else {
    return "Not Found"
}
  • 在设计自定义查询搜索用户界面时,它是 重要的是:
    • 将要搜索或查询的字段数保持在最低限度。使用许多字段会导致许多排列,这可能很难调整。
    • 确定 SOQL、SOSL 或两者的组合是否适合搜索。

删除数据

Salesforce 数据删除机制可以对大型 数据量。Salesforce 使用回收站隐喻用户删除的数据。相反 删除数据时,Salesforce 会将数据标记为已删除,并通过 回收站。此过程称为软删除。当数据被软删除时,它 仍然会影响数据库性能,因为数据仍处于驻留状态,而已删除的记录具有 从任何查询中排除。

数据在回收站中保留 15 天,或直到回收站增长到特定的 大小。然后在 15 天后从数据库中硬删除数据;当大小限制 达到;或者当使用 UI、API 或 Apex 清空回收站时。

此外,Bulk API 和 Bulk API 2.0 支持硬删除选项,该选项允许 记录以绕过回收站并立即可供删除。我们建议 使用批量 API 2.0 的硬删除功能删除大量数据。

如果要立即删除沙盒组织的自定义对象中的记录, 您可以尝试截断这些自定义对象。您可以联系 Salesforce 客户支持 以获取有关此任务的帮助。

搜索

当添加或更改大量数据时,搜索系统 必须先对该信息编制索引,然后才能进行搜索 由所有用户提供,此过程可能需要很长时间。

请参阅搜索体系结构。

最佳实践

本部分列出了实现良好性能的最佳实践 在具有大量数据的部署中。

在大型 Salesforce 部署中进行性能调优的主要方法依赖于减少系统的记录数 必须处理。如果检索到的记录数足够少, 平台可能使用标准数据库结构,如索引或 非规范化以加快数据检索速度。减少记录数量的方法包括:

  • 通过编写狭窄或选择性的查询来缩小范围例如,如果 Account 对象包含已分发的帐户 均匀地跨所有州,然后按以下方式汇总帐户的报告 单个州的城市范围要广得多,并且需要更长的时间才能 执行 – 而不是按单个帐户汇总帐户的报表 城市在一个州。
  • 减少保持活动状态的数据量例如,如果您的 数据量不断增加,性能会随着时间的推移而下降 由。以相同的速率存档或丢弃数据的策略,其中 它进入系统可以防止这种影响。

这些表格列出了主要目标和要遵循的最佳实践 以实现这些目标。

  • 报告
  • 从 API 加载数据
  • 从 API 中提取数据
  • 搜索
  • SOQL 和 SOSL
  • 删除数据
  • 常规

报告

目标最佳实践
通过以下方式最大限度地提高报告性能:对数据进行分区以匹配其可能的用途最小化每个对象的记录数减少要查询的记录数 – 使用 要对查询进行分段的数据。例如,仅查询单个 状态而不是所有状态。(请参阅分部。
减少联接次数尽量减少以下数量:报告中查询的对象用于生成报表的关系在可行时对数据进行非规范化 — “过度非规范化” 数据会导致更多的开销。使用存储在 报表的父记录。这种做法比 让报表汇总子记录。
减少返回的数据量减少查询的字段数 – 仅添加字段 添加到报表、列表视图或 SOQL 查询 这是必需的。
减少记录数 查询通过存档未使用的记录来减少数据量 — 移动 未使用的记录到自定义对象表中,以减小 Report 对象。使用强调使用标准或自定义的报表筛选器 索引字段。尽可能在报表筛选器中使用索引字段。

从 API 加载数据

目标最佳实践
提高性能任何包含超过 2,000 条记录的数据操作都非常适合 批量 API 2.0,用于成功准备、执行和管理使用批量框架的异步工作流。记录少于 2,000 条的作业 应涉及 REST 中的“批量”同步调用(例如,Composite) 或 SOAP。
使用最高效的 操作使用尽可能快的操作 — 最快、是下一个,以及之后是下一个。如果可能,还分为两个操作:和 .insert()update()upsert()upsert()create()update()使用批量 API 2.0 时,请确保在加载之前数据是干净的。中的错误 批处理会触发该批处理的单行处理,并且该处理量很大 影响性能。
减少要传输和处理的数据更新时,仅发送已更改的字段(仅增量加载)。
缩短传输时间,并 中断对于自定义集成:每次加载一次身份验证,而不是对每条记录进行身份验证。使用 GZIP 压缩和 HTTP 保持活动状态来避免在长时间保存期间丢失 操作。
避免不必要的开销对于自定义集成,每次加载进行一次身份验证,而不是对每条记录进行身份验证。
避免计算在初始加载期间使用公共读/写安全性,以避免共享计算 开销
减少计算如果初始加载可能,请在填充共享规则之前填充角色。将用户加载到角色中。与所有者一起加载记录数据,在角色中触发计算 等级制度。配置公共组和队列,并让这些计算 传播。一次添加一个共享规则,让每个规则的计算完成 在添加下一个之前。如果可能,请在创建和分配组和队列之前添加人员和数据。加载新用户和新记录数据。(可选)加载新的公用组和队列。一次添加一个共享规则,让每个规则的计算完成 在添加下一个之前。
延迟计算并加快负载吞吐量在加载期间禁用 Apex 触发器、工作流规则和验证;探讨 加载完成后使用批处理 Apex 处理记录。
在高效的批量大小与潜在的超时之间取得平衡使用 SOAP API 时,请使用尽可能多的批处理,最多 200 – 在以下情况下仍可避免网络超时:记录很大。保存操作需要大量无法延迟的处理。
优化 Lightning 平台 Web 服务连接器 (WSC) 以使用 Salesforce的使用 WSC 而不是其他 Java API 客户端,如 Axis。
最大程度地减少父记录锁定冲突更改子记录时,按父记录对它们进行分组 – 按 字段 ParentId,以最大程度地减少锁定 冲突。
延迟共享计算使用延迟共享计算权限将共享计算推迟到以下日期 加载所有数据后。(请参阅延迟共享计算。
避免将数据加载到 Salesforce 中使用混搭创建应用程序的耦合集成。(请参阅使用 Mashup。

从 API 中提取数据

目标最佳实践
使用最高效的操作使用 和 SOAP API 每隔一段时间将外部系统与 Salesforce 同步 大于 5 分钟。使用出站消息传递功能进行更频繁的同步。getUpdated()getDeleted()使用可返回超过 100 万个结果的查询时,请考虑使用 批量 API 2.0 的查询功能,这可能更合适。

搜索

目标最佳实践
减少要返回的记录数保持搜索具体,并尽可能避免使用通配符。 例如,使用 而不是 搜索 。MichaelMi*
减少联接次数使用单对象搜索可提高速度和准确性。
提高效率使用设置区域进行搜索以启用语言优化, 并打开增强的查找和自动完成功能以获得更好的性能 在查找字段上。
提高搜索性能在某些情况下,使用除法对数据进行分区。(请参阅分部。
减少索引插入和更新所需的时间 大数据量请参阅搜索体系结构。

SOQL 和 SOSL

目标最佳实践
当具有多个筛选器的 SOQL 查询无法使用索引时,允许索引搜索WHERE分解查询。如果在子句中使用两个由 an 连接的索引字段,则搜索 结果可能超过索引阈值。将查询拆分为两个查询,并联接 结果。ORWHERE
避免对实时计算的公式字段进行查询如果必须查询公式字段,请使用公式。避免使用公式字段进行筛选 包含动态的、不确定的引用。见标准 和自定义索引字段。
对于给定的搜索,使用最合适的语言 SOQL 或 SOSL请参阅使用 SOQL 和 SOSL。
在筛选器中执行具有 null 值的查询 选择列表或外键字段WHERE使用 to 替换选项等值。(请参阅使用 SOQL 和 SOSL。NANULLS
设计高效的自定义查询和搜索用户界面在适当的情况下使用 SOQL 和 SOSL,保持查询的重点,并尽量减少数量 正在查询或搜索的数据。(请参阅使用 SOQL 和 SOSL。
构建高效的 SOQL 和 SOSL 查询在查询中使用筛选器和特定术语。对于 SOQL:使用选择性筛选器,以减少查询优化器的行数 进行扫描。例如,使用引用索引字段和 具有更广泛的可能值。如果筛选器没有选择性,则优化程序 不使用索引列。筛选 和 时,请改用该字段。例如FirstNameLastNameNameSelect id, Email from Lead where Name=’Sam Kennedy’避免使用负滤镜。例如,或status !=’failed’status != NULL使用而不是一大堆语句。例如。INORid in (‘001xxx’,‘001xxy’, ‘001xxz’)避免使用跨对象引用公式字段。不要筛选这些对象。他们 不可索引。对于 SOSL:对于 SOSL,选择性过滤器可减少不相关结果的数量。如果 过滤器不是选择性的,搜索词匹配超过 2,000 个 记录、结果可能会受到搜索拥挤的影响。不要为不想搜索的自定义对象编制索引。它增加了这个数字 可供搜索的记录,这可能导致搜索拥挤。过滤掉您不想搜索的对象。使用特定的搜索词。使用有针对性的搜索组。搜索组包括 NAME、EMAIL 和 PHONE 领域。例如 FIND ‘Avery Smith’ IN NAME FIELDS RETURNING Account(Id,Name), Lead(Id,Name)
避免大型 SOQL 查询超时优化 SOQL 查询,缩小查询范围,并使用选择性筛选器。考虑使用 批量 API 2.0 与批量 API 2.0 查询。如果您已经使用了前面的建议,并且仍然 get timeouts,考虑添加一个 LIMIT 子句(开始 有 100,000 条记录)添加到您的查询中。如果使用批处理 Apex 进行查询,请使用 链接以获取记录集(使用 LIMIT)或考虑将过滤器逻辑移动到 execute 方法。

删除数据

目标最佳实践
删除大量数据删除大量数据时,涉及删除 100 万或 更多记录,请使用 Bulk API 或 Bulk API 2.0 的硬删除选项。删除大 由于删除过程的复杂性,数据量可能需要大量时间。 (请参阅删除数据。
使数据删除过程更加高效删除具有多个子项的记录时,请先删除子项。

常规

目标最佳实践
避免共享计算避免任何用户拥有超过 10,000 条记录。
提高性能使用将数据分散到多个数据分层的策略 对象,并根据需要从另一个对象或外部引入数据 商店。
减少创建完整生产拷贝所需的时间 具有大量数据的沙箱创建生产沙箱的副本时,如果不需要,请排除字段历史记录,并且 在创建沙盒副本之前,不要更改大量数据。
提高部署效率分发子记录,使父级的子记录不超过 10,000 个。例如,在具有许多联系人但不使用 帐户,设置多个虚拟帐户并在它们之间分配联系人。

大数据量案例研究

本节包含:

  • 客户遇到的与大数据量相关的问题
  • 客户使用或可能使用的解决方案来解决这些问题

要识别和解决类似问题,请阅读以下案例研究:

  • 数据聚合
  • 自定义搜索功能
  • 使用 Null 值建立索引
  • 呈现具有大量数据的相关列表
  • API 性能
  • 查询的排序优化
  • 多联接报表性能

数据聚合

情况

客户需要使用标准报告汇总月度和年度指标。这 客户的月度和年度详细信息存储在自定义对象中,其中包含 400 万和 分别为 900 万条记录。这些报告汇总了数百万条记录 在这两个对象中,性能都不是最佳。

解决方案

解决方案是创建一个聚合自定义对象,该对象汇总了每月和每年的 值转换为所需报告所需的格式。然后执行报告 聚合的自定义对象。使用批处理 Apex 填充摘要对象。

自定义搜索功能

情况

客户需要搜索 使用特定值跨多个对象的大量数据量 和通配符。客户创建了一个自定义 Visualforce 页面 这将允许用户输入 1-20 个不同的字段,并且 然后使用 SOQL 进行搜索 这些字段的组合。搜索优化变得困难 因为:

  • 当输入许多值时,子句很大且难以调整。引入通配符时, 查询花费的时间更长。WHERE
  • 有时需要跨多个对象进行查询才能满足 整体搜索查询。这种做法导致了多个查询 发生,从而扩展了搜索范围。
  • SOQL 并不总是适用于所有查询类型。

解决 方案

解决方案是:

  • 仅使用必要的搜索字段来减少字段数量 可以搜索。限制同时字段的数量 可以在对常见用例进行单次搜索期间使用 允许 Salesforce 使用索引进行调整。
  • 将多个对象中的数据非规范化为单个对象 自定义对象,以避免进行多次查询调用。
  • 动态确定 SOQL 的使用 或 SOSL 执行基于 搜索的字段数和输入的值类型。例如,非常 特定值(即无通配符)使用 SOQL 进行查询,这允许索引 以提高性能。

使用 Null 值建立索引

情况

客户需要允许 null 值 并能够对它们进行查询。因为单列 选择列表和外键字段的索引排除了其中 索引列等于 null,则不能使用索引 对于 null 查询。

解决方法

最佳做法是 最初不使用 null 值。如果您发现自己处于类似的状态 情况下,请使用其他字符串,例如 代替 。如果不能这样做,可能是因为记录已经存在于 具有 NULL 值的对象,创建一个公式字段,该字段显示 null 值的文本,然后为该公式字段编制索引。N/ANULL

例如 假设 Status 字段已编制索引并包含 null 值。发出 SOQL 查询 与以下内容类似,可防止使用索引。

SELECT Name
FROM Object
WHERE Status__c = ''

相反,您可以创建公式 叫。

Status_Value

Status_Value__c = IF(ISBLANK(Status__c), "blank", Status__c)

查询时可以索引和使用此公式字段 为 null 值。

SELECT Name
FROM Object
WHERE Status_Value__c = 'blank'

这个概念可以 扩展为包含多个字段。

SELECT Name
FROM Object
WHERE Status_Value__c = '' OR Email = ''

呈现具有大量数据的相关列表

重要

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

情况

客户拥有数十万条帐户记录和 1500 万张发票,这些发票是 在自定义对象中,与客户保持主从关系。每个帐户记录 由于发票相关列表的渲染时间过长,因此需要很长时间才能显示 时间。

溶液

显示发票相关列表的延迟与数据倾斜有关。虽然大多数帐户 记录的发票记录很少,有些记录有数千条发票记录。

为了减少延迟,客户尝试减少这些发票记录的数量 父对象,并将子对象中的数据倾斜度保持在最低限度。使用启用分离 加载相关列表设置允许在 客户正在等待相关列表查询完成。

API 性能

情况

客户设计了一个自定义集成,用于将 Salesforce 数据与外部客户同步 应用。集成过程涉及:

  • 在 Salesforce 中查询 给定对象
  • 将此数据加载到外部系统中
  • 再次查询 Salesforce 以获取 所有数据的 ID,以便集成过程可以确定已删除哪些数据 来自Salesforce

这些对象包含数百万条记录。该集成还使用了特定的 API 用户 这是共享层次结构的一部分,用于限制检索到的记录。查询正在采取 分钟即可完成。

在Salesforce中,共享是一种非常 强大的机制,使某些记录对特定用户可见,并且效果很好 用于 UI 交互。但是,当在 SOQL 查询中用作大量数据筛选器时,性能可能会受到影响 由于使用共享作为筛选器时,数据访问更加复杂且难以处理, 特别是当您尝试在大量数据的情况下过滤掉记录时。

溶液

解决方案是授予查询对所有数据的访问权限,然后使用选择性筛选器 以获取适当的记录。例如,使用管理员作为 API 用户将具有 提供了对所有数据的访问,并阻止了在查询中考虑共享。

另一个解决方案是创建一个增量提取,降低 需要处理的数据。

您可以在共享体系结构指南中找到有关共享如何影响性能的详细信息。

查询的排序优化

情况

客户有以下几点 查询。

SELECT Id,Product_Code__c
FROM Customer_Product__c
WHERE CreatedDate = Last_N_Days:3

查询是 查找过去三天内创建的所有记录,但 对象中的数据量超出了标准索引的阈值: 记录总数的 30%,最多 100 万条记录。执行的查询 不好。

溶液

查询重写为:

SELECT Id,Product_Code__c
FROM Customer_Product__c
WHERE CreatedDate = Last_N_Days:3
ORDER BY CreatedDate LIMIT 99999

在此查询中, 未进行阈值检查,并使用索引来查找记录。这种 的查询最多返回 99,999 条记录,顺序如下 在过去三天内创建,假设 99,999 或更少 记录是在过去三天内创建的。CreatedDate

注意

通常 查询已通过 添加的数据时,如果指定对索引字段的查询 如果限制少于 100,000 条记录,则索引用于执行查询。Last_N_DaysORDER BYORDER BY

多联接报表性能

情况

客户创建了报表 使用四个相关对象:客户 (314,000)、销售订单 (769,000)、 销售详细信息(230 万)和帐户所有权(120 万)。 报表几乎没有过滤,需要优化。

溶液

为了优化报告,客户:

  • 添加了其他筛选器,使查询更具选择性和 确保尽可能多的过滤器可转位
  • 尽可能减少每个对象中的数据量
  • 使回收站保持清空。回收站中的数据会影响查询性能。
  • 确保四个相关的共享规则不存在复杂的共享规则 对象。复杂的共享规则可能会对性能产生明显影响。

总结

Salesforce平台是一个强大的环境,其中本机和自定义 应用程序可以非常快速地扩展到大量数据,而 继续保持良好表现。您可以通过以下方式最大限度地发挥这些功能的优势:

  • 使查询具有选择性 – 确保报表、列表视图、 和 SOQL 正在使用适当的过滤器。
  • 减少活动数据量 — 使用存档、混搭、 以及其他技术来减少存储在 Salesforce 中的数据量。

遵循这两个广泛的原则和最佳实践 支持它们可以减少大数据量对 Salesforce 应用程序性能的影响。

运输发票示例

本附录提供了 Apex 应用程序的示例。这是一个比 Hello World 示例。

  • 运输发票演练
  • 装运发票示例代码
  1. 装运发票示例演练
  2. 装运发票示例代码

装运发票示例演练

本节中的示例应用程序包括混合了的传统 Salesforce 功能 与 Apex。Apex 的许多句法和语义特征,以及常见的习语,是 在此应用程序中进行了说明。

注意

装运发票示例需要自定义对象。您可以 您可以自行创建这些代码,或者将对象和 Apex 代码下载为非托管包。 Salesforce AppExchange。要获取组织中的示例资产,请安装 Apex 教程包。此套餐还 包含 Apex 快速入门的示例代码和对象。

场景

在此示例应用程序中,用户 创建新的装运发票或订单,然后将物料添加到发票中。总金额 对于订单,包括运费,是根据 在发票中添加或删除的项目。

数据和代码模型

此示例应用程序使用两个新的 对象:项目和Shipping_invoice。做出以下假设:

  • 项目 A 不能同时按顺序排列shipping_invoice1和shipping_invoice2。两个客户 无法获得相同的(物理)产品。
  • 税率为9.25%。
  • 运费为每磅 75 美分。
  • 一旦订单超过 100 美元,将应用运费折扣(免运费)。

Item 自定义对象中的字段包括:

名字类型描述
名字字符串项目的名称
价格货币商品价格
数量订单中的项目数
重量物品的重量,用于计算运费
Shipping_invoice大纲-细节 (shipping_invoice)与此项目关联的顺序

Shipping_invoice自定义对象中的字段包括:

名字类型描述
名字字符串装运发票/订单的名称
小计货币小计
总计货币总金额,包括税费和运费
航运货币收取的运费金额(假设每磅 0.75 美元)
运费折扣货币当小计金额达到 100 美元时,只适用一次
货币税额(假设9.25%)
总重量所有物品的总重量

此应用程序的所有 Apex 都包含在触发器中。此应用程序具有 以下触发器:

对象触发器名称运行时描述
项目插入后、更新后、删除后更新装运发票,计算总计和装运
Shipping_invoice运费折扣更新后更新装运发票,计算是否有装运折扣

以下是用户操作的一般流程以及触发器的运行时间:

购物车应用程序的用户操作和触发器流Flow of user action and triggers for shopping cart application

  1. 用户点击订单 |新建,命名装运发票,然后单击保存
  2. 用户单击“新建项目”,填写信息,然后单击“保存”。
  3. 计算触发器运行次数。“计算”触发器的一部分将更新装运发票。
  4. ShippingDiscount 触发器运行。
  5. 然后,用户可以添加、删除或更改发票中的项目。

在装运发票示例代码中,触发器和 列出了测试类。代码中的注释解释了该功能。

测试装运发票应用程序

在将应用程序作为包的一部分包含在内之前,必须将 75% 的代码覆盖 单元测试。因此,装运发票应用程序的一件是用于测试的类 触发器。测试类验证以下操作是否已成功完成:

  • 插入项目
  • 更新项目
  • 删除项目
  • 应用运费折扣
  • 输入不良的阴性测试

装运发票示例代码

以下触发器和测试类组成了装运发票 应用示例:

  • 计算触发器
  • ShippingDiscount 触发器
  • 测试类

计算触发器

trigger calculate on Item__c (after insert, after update, after delete) {

// Use a map because it doesn't allow duplicate values

Map<ID, Shipping_Invoice__C> updateMap = new Map<ID, Shipping_Invoice__C>();

// Set this integer to -1 if we are deleting
Integer subtract ;

// Populate the list of items based on trigger type
List<Item__c> itemList;
    if(trigger.isInsert || trigger.isUpdate){
        itemList = Trigger.new;
        subtract = 1;
    }
    else if(trigger.isDelete)
    {
        // Note -- there is no trigger.new in delete
        itemList = trigger.old;
        subtract = -1;
    }

// Access all the information we need in a single query 
// rather than querying when we need it.
// This is a best practice for bulkifying requests

set<Id> AllItems = new set<id>();

for(item__c i :itemList){
// Assert numbers are not negative.  
// None of the fields would make sense with a negative value

System.assert(i.quantity__c > 0, 'Quantity must be positive');
System.assert(i.weight__c >= 0, 'Weight must be non-negative');
System.assert(i.price__c >= 0, 'Price must be non-negative');

// If there is a duplicate Id, it won't get added to a set
AllItems.add(i.Shipping_Invoice__C);
}

// Accessing all shipping invoices associated with the items in the trigger
List<Shipping_Invoice__C> AllShippingInvoices = [SELECT Id, ShippingDiscount__c, 
                   SubTotal__c, TotalWeight__c, Tax__c, GrandTotal__c 
                   FROM Shipping_Invoice__C WHERE Id IN :AllItems];
                   
// Take the list we just populated and put it into a Map.  
// This will make it easier to look up a shipping invoice
// because you must iterate a list, but you can use lookup for a map, 
Map<ID, Shipping_Invoice__C> SIMap = new Map<ID, Shipping_Invoice__C>();

for(Shipping_Invoice__C sc : AllShippingInvoices)
{
    SIMap.put(sc.id, sc);
}

// Process the list of items
    if(Trigger.isUpdate)
    {
        // Treat updates like a removal of the old item and addition of the         
        // revised item rather than figuring out the differences of each field 
        // and acting accordingly.
        // Note updates have both trigger.new and trigger.old
        for(Integer x = 0; x < Trigger.old.size(); x++)
        {
            Shipping_Invoice__C myOrder;
            myOrder = SIMap.get(trigger.old[x].Shipping_Invoice__C);

            // Decrement the previous value from the subtotal and weight.
            myOrder.SubTotal__c -= (trigger.old[x].price__c * 
                                    trigger.old[x].quantity__c);
            myOrder.TotalWeight__c -= (trigger.old[x].weight__c * 
                                       trigger.old[x].quantity__c);
                
            // Increment the new subtotal and weight.
            myOrder.SubTotal__c += (trigger.new[x].price__c * 
                                    trigger.new[x].quantity__c);
            myOrder.TotalWeight__c += (trigger.new[x].weight__c * 
                                       trigger.new[x].quantity__c);
        }
        
        for(Shipping_Invoice__C myOrder : AllShippingInvoices)
        {
            
            // Set tax rate to 9.25%  Please note, this is a simple example.  
            // Generally, you would never hard code values.
            // Leveraging Custom Settings for tax rates is a best practice.  
            // See Custom Settings in the Apex Developer Guide 
            // for more information.
            myOrder.Tax__c = myOrder.Subtotal__c * .0925;
            
            // Reset the shipping discount
            myOrder.ShippingDiscount__c = 0;
    
            // Set shipping rate to 75 cents per pound.  
            // Generally, you would never hard code values.
            // Leveraging Custom Settings for the shipping rate is a best practice.
            // See Custom Settings in the Apex Developer Guide 
            // for more information.
            myOrder.Shipping__c = (myOrder.totalWeight__c * .75);
            myOrder.GrandTotal__c = myOrder.SubTotal__c + myOrder.tax__c + 
                                    myOrder.Shipping__c;
            updateMap.put(myOrder.id, myOrder);
         }
    }
    else
    { 
        for(Item__c itemToProcess : itemList)
        {
            Shipping_Invoice__C myOrder;
    
            // Look up the correct shipping invoice from the ones we got earlier
            myOrder = SIMap.get(itemToProcess.Shipping_Invoice__C);
            myOrder.SubTotal__c += (itemToProcess.price__c * 
                                    itemToProcess.quantity__c * subtract);
            myOrder.TotalWeight__c += (itemToProcess.weight__c * 
                                       itemToProcess.quantity__c * subtract);
        }
        
        for(Shipping_Invoice__C myOrder : AllShippingInvoices)
        {
            
             // Set tax rate to 9.25%  Please note, this is a simple example.  
             // Generally, you would never hard code values.
             // Leveraging Custom Settings for tax rates is a best practice.  
             // See Custom Settings in the Apex Developer Guide 
             // for more information.
             myOrder.Tax__c = myOrder.Subtotal__c * .0925;
             
             // Reset shipping discount
             myOrder.ShippingDiscount__c = 0;
    
            // Set shipping rate to 75 cents per pound.  
            // Generally, you would never hard code values.
            // Leveraging Custom Settings for the shipping rate is a best practice.
            // See Custom Settings in the Apex Developer Guide 
            // for more information.
            myOrder.Shipping__c = (myOrder.totalWeight__c * .75);
            myOrder.GrandTotal__c = myOrder.SubTotal__c + myOrder.tax__c + 
                                    myOrder.Shipping__c;
                                       
            updateMap.put(myOrder.id, myOrder);
    
         }
     }    
     
     // Only use one DML update at the end.
     // This minimizes the number of DML requests generated from this trigger.
     update updateMap.values();
}

运费折扣 触发

trigger ShippingDiscount on Shipping_Invoice__C (before update) {
    // Free shipping on all orders greater than $100
    
    for(Shipping_Invoice__C myShippingInvoice : Trigger.new)
    {
        if((myShippingInvoice.subtotal__c >= 100.00) && 
           (myShippingInvoice.ShippingDiscount__c == 0))
        {
            myShippingInvoice.ShippingDiscount__c = 
                         myShippingInvoice.Shipping__c * -1;
            myShippingInvoice.GrandTotal__c += myShippingInvoice.ShippingDiscount__c;
        }
    }
}

运输发票 测试

@IsTest
private class TestShippingInvoice{

    // Test for inserting three items at once
    public static testmethod void testBulkItemInsert(){
        // Create the shipping invoice. It's a best practice to either use defaults
        // or to explicitly set all values to zero so as to avoid having
        // extraneous data in your test.
        Shipping_Invoice__C order1 = new Shipping_Invoice__C(subtotal__c = 0, 
                          totalweight__c = 0, grandtotal__c = 0, 
                          ShippingDiscount__c = 0, Shipping__c = 0, tax__c = 0);

        // Insert the order and populate with items
        insert Order1;
        List<Item__c> list1 = new List<Item__c>();
        Item__c item1 = new Item__C(Price__c = 10, weight__c = 1, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c item2 = new Item__C(Price__c = 25, weight__c = 2, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c item3 = new Item__C(Price__c = 40, weight__c = 3, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        list1.add(item1);
        list1.add(item2);
        list1.add(item3);
        insert list1;
        
        // Retrieve the order, then do assertions
        order1 = [SELECT id, subtotal__c, tax__c, shipping__c, totalweight__c, 
                  grandtotal__c, shippingdiscount__c 
                  FROM Shipping_Invoice__C 
                  WHERE id = :order1.id];
        
        System.assert(order1.subtotal__c == 75, 
                'Order subtotal was not $75, but was '+ order1.subtotal__c);
        System.assert(order1.tax__c == 6.9375, 
                'Order tax was not $6.9375, but was ' + order1.tax__c);
        System.assert(order1.shipping__c == 4.50, 
                'Order shipping was not $4.50, but was ' + order1.shipping__c);
        System.assert(order1.totalweight__c == 6.00, 
                'Order weight was not 6 but was ' + order1.totalweight__c);
        System.assert(order1.grandtotal__c == 86.4375, 
                'Order grand total was not $86.4375 but was ' 
                 + order1.grandtotal__c);
        System.assert(order1.shippingdiscount__c == 0, 
                'Order shipping discount was not $0 but was ' 
                + order1.shippingdiscount__c);
    }
    
    // Test for updating three items at once
    public static testmethod void testBulkItemUpdate(){

        // Create the shipping invoice. It's a best practice to either use defaults
        // or to explicitly set all values to zero so as to avoid having
        // extraneous data in your test.
        Shipping_Invoice__C order1 = new Shipping_Invoice__C(subtotal__c = 0, 
                          totalweight__c = 0, grandtotal__c = 0, 
                          ShippingDiscount__c = 0, Shipping__c = 0, tax__c = 0);

        // Insert the order and populate with items.
        insert Order1;
        List<Item__c> list1 = new List<Item__c>();
        Item__c item1 = new Item__C(Price__c = 1, weight__c = 1, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c item2 = new Item__C(Price__c = 2, weight__c = 2, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c item3 = new Item__C(Price__c = 4, weight__c = 3, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        list1.add(item1);
        list1.add(item2);
        list1.add(item3);
        insert list1;
        
        // Update the prices on the 3 items
        list1[0].price__c = 10;
        list1[1].price__c = 25;
        list1[2].price__c = 40;
        update list1;
        
        // Access the order and assert items updated
        order1 = [SELECT id, subtotal__c, tax__c, shipping__c, totalweight__c, 
                  grandtotal__c, shippingdiscount__c 
                  FROM Shipping_Invoice__C 
                  WHERE Id = :order1.Id];

        System.assert(order1.subtotal__c == 75, 
                       'Order subtotal was not $75, but was '+ order1.subtotal__c);
        System.assert(order1.tax__c == 6.9375, 
                       'Order tax was not $6.9375, but was ' + order1.tax__c);
        System.assert(order1.shipping__c == 4.50, 
                       'Order shipping was not $4.50, but was ' 
                       + order1.shipping__c);
        System.assert(order1.totalweight__c == 6.00, 
                       'Order weight was not 6 but was ' + order1.totalweight__c);
        System.assert(order1.grandtotal__c == 86.4375, 
                       'Order grand total was not $86.4375 but was ' 
                       + order1.grandtotal__c);
        System.assert(order1.shippingdiscount__c == 0, 
                       'Order shipping discount was not $0 but was ' 
                       + order1.shippingdiscount__c);
    
    }
    
    // Test for deleting items
    public static testmethod void testBulkItemDelete(){

        // Create the shipping invoice. It's a best practice to either use defaults
        // or to explicitly set all values to zero so as to avoid having
        // extraneous data in your test.
        Shipping_Invoice__C order1 = new Shipping_Invoice__C(subtotal__c = 0, 
                          totalweight__c = 0, grandtotal__c = 0, 
                          ShippingDiscount__c = 0, Shipping__c = 0, tax__c = 0);

        // Insert the order and populate with items
        insert Order1;
        List<Item__c> list1 = new List<Item__c>();
        Item__c item1 = new Item__C(Price__c = 10, weight__c = 1, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c item2 = new Item__C(Price__c = 25, weight__c = 2, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c item3 = new Item__C(Price__c = 40, weight__c = 3, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c itemA = new Item__C(Price__c = 1, weight__c = 3, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c itemB = new Item__C(Price__c = 1, weight__c = 3, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c itemC = new Item__C(Price__c = 1, weight__c = 3, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c itemD = new Item__C(Price__c = 1, weight__c = 3, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        list1.add(item1);
        list1.add(item2);
        list1.add(item3);
        list1.add(itemA);
        list1.add(itemB);
        list1.add(itemC);
        list1.add(itemD);
        insert list1;
        
        // Seven items are now in the shipping invoice. 
       // The following deletes four of them.
        List<Item__c> list2 = new List<Item__c>();
        list2.add(itemA);
        list2.add(itemB);
        list2.add(itemC);
        list2.add(itemD);
        delete list2;
        
        // Retrieve the order and verify the deletion
        order1 = [SELECT id, subtotal__c, tax__c, shipping__c, totalweight__c, 
                  grandtotal__c, shippingdiscount__c 
                  FROM Shipping_Invoice__C 
                  WHERE Id = :order1.Id];
        
        System.assert(order1.subtotal__c == 75, 
                      'Order subtotal was not $75, but was '+ order1.subtotal__c);
        System.assert(order1.tax__c == 6.9375, 
                      'Order tax was not $6.9375, but was ' + order1.tax__c);
        System.assert(order1.shipping__c == 4.50, 
                      'Order shipping was not $4.50, but was ' + order1.shipping__c);
        System.assert(order1.totalweight__c == 6.00, 
                      'Order weight was not 6 but was ' + order1.totalweight__c);
        System.assert(order1.grandtotal__c == 86.4375, 
                      'Order grand total was not $86.4375 but was ' 
                      + order1.grandtotal__c);
        System.assert(order1.shippingdiscount__c == 0, 
                      'Order shipping discount was not $0 but was ' 
                      + order1.shippingdiscount__c);
    }
    // Testing free shipping
    public static testmethod void testFreeShipping(){

        // Create the shipping invoice. It's a best practice to either use defaults
        // or to explicitly set all values to zero so as to avoid having
        // extraneous data in your test.
        Shipping_Invoice__C order1 = new Shipping_Invoice__C(subtotal__c = 0, 
                          totalweight__c = 0, grandtotal__c = 0, 
                          ShippingDiscount__c = 0, Shipping__c = 0, tax__c = 0);

        // Insert the order and populate with items.
        insert Order1;
        List<Item__c> list1 = new List<Item__c>();
        Item__c item1 = new Item__C(Price__c = 10, weight__c = 1, 
                                 quantity__c = 1, Shipping_Invoice__C = order1.id);
        Item__c item2 = new Item__C(Price__c = 25, weight__c = 2, 
                                 quantity__c = 1, Shipping_Invoice__C = order1.id);
        Item__c item3 = new Item__C(Price__c = 40, weight__c = 3, 
                                 quantity__c = 1, Shipping_Invoice__C = order1.id);
        list1.add(item1);
        list1.add(item2);
        list1.add(item3);
        insert list1;
        
        // Retrieve the order and verify free shipping not applicable
        order1 = [SELECT id, subtotal__c, tax__c, shipping__c, totalweight__c, 
                  grandtotal__c, shippingdiscount__c 
                  FROM Shipping_Invoice__C 
                  WHERE Id = :order1.Id];
        
        // Free shipping not available on $75 orders
        System.assert(order1.subtotal__c == 75, 
                      'Order subtotal was not $75, but was '+ order1.subtotal__c);
        System.assert(order1.tax__c == 6.9375, 
                      'Order tax was not $6.9375, but was ' + order1.tax__c);
        System.assert(order1.shipping__c == 4.50, 
                      'Order shipping was not $4.50, but was ' + order1.shipping__c);
        System.assert(order1.totalweight__c == 6.00, 
                      'Order weight was not 6 but was ' + order1.totalweight__c);
        System.assert(order1.grandtotal__c == 86.4375, 
                      'Order grand total was not $86.4375 but was ' 
                      + order1.grandtotal__c);
        System.assert(order1.shippingdiscount__c == 0, 
                      'Order shipping discount was not $0 but was ' 
                      + order1.shippingdiscount__c);
        
        // Add items to increase subtotal
        item1 = new Item__C(Price__c = 25, weight__c = 20, quantity__c = 1, 
                            Shipping_Invoice__C = order1.id);       
        insert item1;

        // Retrieve the order and verify free shipping is applicable
        order1 = [SELECT id, subtotal__c, tax__c, shipping__c, totalweight__c, 
                  grandtotal__c, shippingdiscount__c 
                  FROM Shipping_Invoice__C 
                  WHERE Id = :order1.Id];
        
        // Order total is now at $100, so free shipping should be enabled
        System.assert(order1.subtotal__c == 100, 
                      'Order subtotal was not $100, but was '+ order1.subtotal__c);
        System.assert(order1.tax__c == 9.25, 
                      'Order tax was not $9.25, but was ' + order1.tax__c);
        System.assert(order1.shipping__c == 19.50, 
                      'Order shipping was not $19.50, but was ' 
                      + order1.shipping__c);
        System.assert(order1.totalweight__c == 26.00, 
                      'Order weight was not 26 but was ' + order1.totalweight__c);
        System.assert(order1.grandtotal__c == 109.25, 
                      'Order grand total was not $86.4375 but was ' 
                      + order1.grandtotal__c);
        System.assert(order1.shippingdiscount__c == -19.50, 
                      'Order shipping discount was not -$19.50 but was ' 
                      + order1.shippingdiscount__c);
    }
    
     // Negative testing for inserting bad input
    public static testmethod void testNegativeTests(){

        // Create the shipping invoice. It's a best practice to either use defaults
        // or to explicitly set all values to zero so as to avoid having
        // extraneous data in your test.
        Shipping_Invoice__C order1 = new Shipping_Invoice__C(subtotal__c = 0, 
                          totalweight__c = 0, grandtotal__c = 0, 
                          ShippingDiscount__c = 0, Shipping__c = 0, tax__c = 0);

        // Insert the order and populate with items. 
        insert Order1;
        Item__c item1 = new Item__C(Price__c = -10, weight__c = 1, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c item2 = new Item__C(Price__c = 25, weight__c = -2, quantity__c = 1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c item3 = new Item__C(Price__c = 40, weight__c = 3, quantity__c = -1, 
                                    Shipping_Invoice__C = order1.id);
        Item__c item4 = new Item__C(Price__c = 40, weight__c = 3, quantity__c = 0, 
                                    Shipping_Invoice__C = order1.id);

        try{
            insert item1;
        }
        catch(Exception e)
        {
            system.assert(e.getMessage().contains('Price must be non-negative'), 
                         'Price was negative but was not caught');
        }
        
        try{
            insert item2;
        }
        catch(Exception e)
        {
            system.assert(e.getMessage().contains('Weight must be non-negative'), 
                         'Weight was negative but was not caught');
        }

        try{
            insert item3;
        }
        catch(Exception e)
        {
            system.assert(e.getMessage().contains('Quantity must be positive'), 
                         'Quantity was negative but was not caught');
        }
        
        try{
            insert item4;
        }
        catch(Exception e)
        {
            system.assert(e.getMessage().contains('Quantity must be positive'), 
                         'Quantity was zero but was not caught');
        }
    }
}

部署 Apex

您无法在 Salesforce 生产组织中开发 Apex。您的开发工作是 在沙盒或 Developer Edition 组织中完成。您可以使用以下方法部署 Apex:

  • 更改集
  • Salesforce 扩展 针对 Visual Studio Code
  • 蚂蚁迁徙 工具
  • 肥皂 应用程序接口
  • 使用元数据 API 或工具 API 的第三方工具
  • 带有 Salesforce DX 插件的 VS Code

部署时编译

从 18 年夏季开始,每个组织的 Apex 代码现在都会自动重新编译 完成元数据部署、包安装或包升级。部署时编译 为生产组织自动启用,以确保用户不会 部署后立即体验性能下降,而您不能 禁用它。对于沙盒、开发人员、试用和临时组织,此功能是 默认情况下禁用,但您可以在“设置”中的“Apex 设置”下启用。

此功能会导致部署到组织调用 Apex 编译器并保存 生成的字节码作为部署的一部分。部署时间的增加最小 ,但 Apex 不需要在首次运行时重新编译。所以轻微的 增加部署时间可以防止首次运行时出现性能问题。考虑 在多个用户共享的沙盒或临时组织中启用此功能 功能测试或由持续集成流程使用。

有关使用元数据 API 设置此组织首选项的信息,请参阅《元数据 API 开发人员指南》中的 OrgPreferenceSettings。

  1. 使用更改集部署 Apex
  2. 使用适用于 Visual Studio Code 的 Salesforce 扩展部署 Apex
  3. 使用 Ant 迁移工具部署更改
  4. 使用 SOAP API 部署Apex
    默认情况下,这些 Salesforce 对象和 SOAP API 调用和标头可用于 Apex。有关所有其他 SOAP API 调用的信息,包括可用于扩展或实现任何现有 Apex IDE 的调用,请联系您的 Salesforce 代表。

使用更改集部署 Apex

适用于:Salesforce Classic
提供 EnterprisePerformanceUnlimited 和 Database.com Edition

您可以部署 Apex 连接组织之间的类和触发器,例如,从沙盒组织到 您的生产组织。您可以在 Salesforce 用户界面中创建出站更改集 并添加要上传并部署到目标的 Apex 组件 组织。要了解有关更改集的更多信息,请参阅 Salesforce Online 中的“更改集” 帮助。

使用适用于 Visual Studio Code 的 Salesforce 扩展进行部署 顶点

适用于 Visual Studio Code 的 Salesforce 扩展包括用于在 轻量级、可扩展的 VS Code 编辑器中的 Salesforce 平台。这些工具提供 用于与开发组织(临时组织、沙盒和 DE 组织)、Apex、Aura 合作的功能 组件和 Visualforce。

注意

如果部署到生产组织:

  • 单元测试必须至少覆盖 75% 的 Apex 代码,并且所有这些测试都必须 成功完成。
  • 每个触发器都必须具有一定的测试覆盖率。
  • 所有类和触发器都必须成功编译。

请注意以下事项。

  • 将 Apex 部署到生产组织时,每个单元测试 默认情况下,将执行 Organization 命名空间。
  • 调用不计入 Apex 代码的一部分 覆盖。System.debug
  • 测试方法和测试类不计为 Apex 代码的一部分 覆盖。
  • 虽然只有 75% 的 Apex 代码必须被测试覆盖,但不要专注于 所覆盖代码的百分比。相反,请确保每次使用 涵盖您的申请案例,包括正面和负面案例, 以及批量和单条记录。这种方法可确保 75% 或更多 的代码包含在单元测试中。

有关如何使用 Visual Studio Code 部署到 Salesforce 组织的详细信息,请参阅开发模型。

使用 Ant 迁移工具部署更改

除了适用于 Visual Studio Code 的 Salesforce 扩展之外,您还可以使用脚本来执行以下操作 部署Apex。

下载 Ant 迁移工具以执行基于文件的元数据更改部署,以及 Apex 类从 Developer Edition 或沙盒组织到使用 Apache 的 Ant 构建工具。

注意

Ant 迁移工具是 Salesforce 提供的免费资源 支持其用户和合作伙伴,但出于目的不被视为我们服务的一部分 Salesforce 主要服务协议。要使用 Ant 迁移工具,请执行以下操作:

  1. 访问 http://www.oracle.com/technetwork/java/javase/downloads/index.html 并安装 Java JDK。注意Ant Migration Tool 版本 51.0 及更高版本需要 Java 版本 11 或更高版本。如果使用 Ant 迁移工具版本 36.0 到 50.0,为了增强安全性,我们 建议使用 Java 7 或更高版本以及最新版本的 Ant 迁移工具(版本 36.0 或更高版本)。从版本 36.0 开始,Ant 迁移工具使用 TLS 1.2 来确保安全 当 Salesforce 检测到 Java 版本 7 (1.7) 时与 Salesforce 进行通信。工具 显式启用 Java 7 的 TLS 1.1 和 1.2。如果您使用的是 Java 8 (1.8)、TLS 1.2 被使用。对于 Java V6,使用 TLS 1.0,而 Salesforce的。或者,如果您使用的是 Java 7,而不是升级 Ant 迁移工具 对于版本 36.0 或更高版本,您可以将以下内容添加到环境变量中:ANT_OPTS-Dhttps.protocols=TLSv1.1,TLSv1.2此设置还会对本地的任何其他 Ant 工具强制实施 TLS 1.1 和 1.2 系统。
  2. 访问 http://ant.apache.org/ 并安装 Apache Ant 版本 1.6 或更高版本,在部署计算机上。
  3. 按照《Ant 安装指南》第 http://ant.apache.org/manual/install.html 页中指定的方式设置环境变量(如 、 和 )。ANT_HOMEJAVA_HOMEPATH
  4. 通过打开命令提示符来验证 JDK 和 Ant 是否已正确安装,然后 进入。您的输出 必须看起来像 这:ant –versionApache Ant version 1.7.0 compiled on December 13 2006
  5. 下载21年夏季蚂蚁迁徙的.zip文件 工具。下载链接没有 要求对 Salesforce 进行身份验证。如果您已登录 Salesforce,我们建议您 在访问浏览器中的链接之前注销。
  6. 将下载的文件解压缩到您选择的目录。Zip 文件包含以下内容:
    • 说明如何使用这些工具的自述文件 .html 文件
    • 包含 ant 任务的 Jar 文件:ant-salesforce.jar
    • 包含以下内容的示例文件夹:
      • 一个 codepkg\classes 文件夹,其中包含SampleDeployClass.clsSampleFailingTestClass.cls
      • 包含 SampleAccountTrigger.trigger 的 codepkg\triggers 文件夹
      • 包含自定义对象的 mypkg\objects 文件夹 在示例中使用
      • 一个 removecodepkg 文件夹,其中包含要删除的 XML 文件 来自您组织的示例
      • 您必须编辑的示例 build.properties 文件, 指定您的凭据,以便在 build.xml 中运行示例 Ant 任务
      • 一个示例 build.xml 文件,用于执行 和 API 调用deployretrieve
  7. Ant 迁移工具使用 ant-salesforce.jar 文件 分发.zip文件。如果您安装了该工具的早期版本,并且 将 ant-salesforce.jar 复制到 Ant lib 目录,删除之前的 jar 文件。lib 目录位于 Ant 的根文件夹中 安装。您无需将新的 jar 文件复制到 Ant lib 目录。
  8. 打开解压缩文件中的示例子目录。
  9. 编辑 build.properties 文件:
    1. 分别在 sf.user 和 sf.password 字段中输入您的 Salesforce 生产组织用户名和密码。注意
      • 您指定的用户名必须具有编辑 Apex 的权限。
      • 如果您从不受信任的网络使用 Ant 迁移工具,请附加 安全令牌添加到密码中。了解有关安全性的详细信息 令牌,请参阅“重置” 您的安全令牌“在 Salesforce 帮助中。
    2. 如果要部署到沙盒组织,请将 sf.serverurl 字段更改为 .https://test.salesforce.com
  10. 在示例目录中打开命令窗口。
  11. 进入。这将使用示例类和 Account 运行 API 调用 触发器随 Ant 迁移工具提供。ant deployCodedeploy调用 build.xml 文件中命名的 Ant 目标。ant deployCodedeploy<!-- Shows deploying code & running tests for package 'codepkg' --> <target name="deployCode"> <!-- Upload the contents of the "codepkg" package, running the tests for just 1 class --> <sf:deploy username="${sf.username}" password="${sf.password}" serverurl="${sf.serverurl}" deployroot="codepkg"> <runTest>SampleDeployClass</runTest> </sf:deploy> </target>有关详细信息,请参阅了解部署。
  12. 要删除在执行过程中添加的测试类和触发器,请在命令窗口中输入以下内容: 。ant deployCodeant undeployCodeant undeployCode调用 build.xml 文件中命名的 Ant 目标。undeployCode<target name="undeployCode"> <sf:deploy username="${sf.username}" password="${sf.password}" serverurl= "${sf.serverurl}" deployroot="removecodepkg"/> </target>

查看 Ant 迁移工具 有关 Ant 迁移工具的完整详细信息的指南。

  1. 了解部署
    Ant 迁移工具提供了该任务,该任务可以合并到您的部署脚本中。deploy
  2. 了解检索

了解部署

Ant 迁移工具提供了该任务,该任务可以合并到您的部署脚本中。

deploy

您可以修改 build.xml 示例以包含组织的 类和触发器。有关部署任务的属性的完整列表,请参见《Ant 迁移工具指南》。该任务的一些属性包括:deployusername用于登录 Salesforce 生产组织的用户名。password与登录 Salesforce 生产组织关联的密码。serverURL您正在登录的 Salesforce 服务器的 URL。我们建议您指定 贵组织的 My Domain 登录 URL,列在“My Domain 设置”页面上。如果你不这样做 指定一个值,默认值为 。login.salesforce.comdeployRoot包含 Apex 类和触发器的本地目录,以及任何其他 元数据。创建必要文件结构的最佳方法是 从您的组织或沙盒中检索它。有关详细信息,请参阅了解检索。

  • Apex 类文件必须位于名为 classes 的子目录中。您必须有两个 每个类的文件,命名如下:
    • classname.CLS公司
    • classname.cls-meta.xml
    例如,和 .-meta.xml 文件包含 API 版本和类的状态(活动/非活动)。MyClass.clsMyClass.cls-meta.xml
  • Apex 触发器文件必须位于名为 triggers 的子目录中。你必须有 每个触发器有两个文件,命名如下:
    • triggername.触发
    • triggername.触发器元.xml
    例如,和 .-meta.xml 文件 包含触发器的 API 版本和状态(活动/非活动)。MyTrigger.triggerMyTrigger.trigger-meta.xml
  • 根目录包含一个 XML 文件包 .xml,其中列出了 要部署的所有类、触发器和其他对象。
  • 根目录(可选)包含一个 XML 文件 destructiveChanges.xml,其中列出了所有类、触发器、 以及要从组织中删除的其他对象。

checkOnly指定是将类和触发器部署到目标环境,还是 不。此属性采用布尔值:if 否则,您不希望将类和触发器保存到组织中。如果未指定值,则 缺省值为 。truefalsefalserunTest可选的子元素。包含部署后运行的测试的 Apex 类的列表。 要使用此选项,请将 testLevel 设置为 。RunSpecifiedTeststestLevel自选。指定在部署过程中运行哪些测试。测试级别为 无论部署包中存在的组件类型如何,都强制执行。 有效值为:

  • NoTestRun– 不运行任何测试。此测试 级别仅适用于部署到开发环境,例如沙盒、 Developer Edition 或试用组织。此测试级别是 开发环境。
  • RunSpecifiedTests– 仅运行您在 runTests 选项中指定的测试。代码覆盖率要求不同于 使用此测试级别时的默认覆盖率要求。每个类和触发器 在部署包中,必须至少覆盖 75% 的已执行测试 代码覆盖率。此覆盖率是针对每个类计算的,并单独触发,并且 与总体覆盖率不同。
  • RunLocalTests组织中的所有测试都是 运行,但源自已安装、托管和解锁的那些除外 包。此测试级别是生产部署的默认级别,该级别符合以下条件 包括 Apex 类或触发器。
  • RunAllTestsInOrg运行所有测试。 这些测试包括组织中的所有测试,包括托管测试 包。

如果未指定测试级别,则使用默认测试执行行为。 请参阅元数据 API 开发人员的 指南。

此字段在 API 版本 34.0 中可用,并且 后。runAllTests(已弃用,仅在 API 版本 33.0 及更早版本中可用。此参数是 可选,缺省值为 。设置为在部署后运行所有 Apex 测试,包括测试 源自已安装的托管软件包。falsetrue

了解检索

使用目标检索类和 来自沙盒或生产组织的触发器。在正常的部署周期中,您将在 之前运行,以便为新类和 触发器。但是,对于此示例,首先用于 确保有要检索的内容。retrieveCoderetrieveCodedeploydeploy

若要从现有组织中检索类和触发器, 使用检索 Ant 任务,如示例构建目标所示:ant retrieveCode

<target name="retrieveCode">
   <!-- Retrieve the contents listed in the file codepkg/package.xml into the codepkg directory -->
   <sf:retrieve username="${sf.username}" password="${sf.password}" 
        serverurl="${sf.serverurl}" retrieveTarget="codepkg" unpackaged="codepkg/package.xml"/>
</target>

文件 codepkg/package.xml 列出了元数据 要检索的组件。在此示例中,它检索两个类 和一个触发器。检索到的文件被放入目录 codepkg 中,覆盖目录中已有的所有内容。

检索任务的属性如下:

描述
用户名如果 sessionId 不是,则为必填项 指定。用于登录的 Salesforce 用户名。与此关联的用户名 连接必须具有通过元数据 API 函数修改元数据 许可。
密码如果 sessionId 不是,则为必填项 指定。用于登录与此关联的组织的密码 项目。如果您使用的是安全令牌,请将 25 位令牌值粘贴到 密码末尾。
会话 ID如果未指定用户名和密码,则为必填项。活动 Salesforce 会话或 OAuth 的 ID 访问令牌。用户成功登录 Salesforce 后将创建会话 使用用户名和密码。使用会话 ID 登录到现有会话 而不是创建新会话。或者,将访问令牌用于 OAuth 认证。有关更多信息,请参阅使用 Salesforce 帮助中的 OAuth。
服务器网址自选。Salesforce 服务器 URL(如果为空, 默认值为 )。连接 更改为沙盒实例,请将此值更改为 。login.salesforce.comtest.salesforce.com
检索目标必填。目录结构的根目录 元数据文件被检索到其中。
包名称如果未指定 unpackaged,则为必填项。逗号分隔 要检索的包的名称列表。指定 packageNames 或 unpackaged,但不指定 双。
api版本自选。要用于 检索到的元数据文件。默认值为 59.0。
轮询WaitMillis自选。缺省值为 .数量 轮询检索结果时两次尝试之间等待的毫秒数 请求。客户端继续轮询服务器,直至达到 maxPoll 定义的限制。10000
最大投票自选。缺省值为 .轮询服务器以获取结果的次数 检索请求。200连续轮询尝试之间的等待时间由 pollWaitMillis 定义。
单包自选。缺省值为 .设置此参数 以检索多个包。 如果设置为 ,则检索到的 zip 文件包括 额外的顶级目录,包含每个包的子目录。truefalsefalse
跟踪自选。缺省值为 . 将 SOAP 请求和响应打印到控制台。此选项显示用户的 登录时以纯文本形式输入密码。false
未包装如果未指定 packageNames,则为必需。路径和名称 指定要检索的组件的文件清单。指定 unpackaged 或 packageNames,但不能同时指定两者。
解 压缩自选。缺省值为 .如果设置为 ,则检索到的 组件已解压缩。如果设置为 ,则 检索到的组件将作为 zip 文件保存在 retrieveTarget 目录中。truetruefalse

使用 SOAP API 部署Apex

这些 Salesforce 对象和 SOAP API 调用和标头是 默认情况下可用于 Apex。有关所有其他 SOAP API 调用的信息,包括那些 可用于扩展或实施任何现有的 Apex IDE,请联系您的 Salesforce 代表。

Apex 类方法可以公开为自定义 SOAP Web 服务调用。这允许外部 应用程序调用 Apex Web 服务以在 Salesforce 中执行操作。使用关键字来定义这些方法。为 有关详细信息,请参阅使用 webservice 关键字的注意事项。webservice使用 SOAP API 调用保存的任何 Apex 代码都使用与端点相同的 SOAP API 版本 的请求。例如,如果要使用 SOAP API 版本 59.0 ,请使用端点 59.0:

https://MyDomain.salesforce.com/services/Soap/s/59.0

这些 Salesforce 对象可用于 Apex。

  • ApexTestQueueItem
  • ApexTestResult(阿佩克斯测试结果)
  • ApexTestResultLimits
  • ApexTestRunResult

使用这些 SOAP API 调用来部署 Apex。

  • compileAndTest()
  • compileClasses()
  • compileTriggers()
  • executeanonymous()
  • runTests()

所有这些调用都采用包含类或触发器的 Apex 代码,以及 需要设置的任何字段的值。这些 SOAP 标头在 Apex 的 SOAP API 调用中可用。

  • DebuggingHeader
  • PackageVersionHeader

另请参阅元数据 API 开发人员指南,了解另外两个调用:

  • deploy()
  • retrieve()

使用托管软件包分发 Apex

作为 ISV 或 Salesforce 合作伙伴,您可以将 Apex 代码分发给客户 使用包的组织。在这里,我们将介绍包和包 版本控制。

重要

如果一个类有一个 依赖于 Chatter,代码可以编译并安装在没有 启用 Chatter。但是,如果未启用 Chatter,则代码会在运行时抛出错误 时间。请参阅打包 ConnectApi 类。ConnectApi

  1. 什么是套餐?
  2. 软件包版本
  3. 弃用 Apex
  4. 包版本中的行为

什么是套餐?

是一个容器,用于容纳小到单个组件或 大作为一组相关应用。创建包后,可以将其分发给其他 Salesforce 用户和组织,包括公司外部的用户和组织。组织可以 创建一个可由多个不同用户下载和安装的托管软件包 组织。托管包与非托管包的不同之处在于具有一些锁定的组件, 允许以后升级托管包。非托管包不包括锁定的包 组件,并且无法升级。

软件包版本

包版本是一个数字,用于标识包中上载的组件集。这 版本号的格式为 (对于 例如,2.1.3)。在每个主要数字中,主要数字和次要数字增加到选定的值 释放。仅针对补丁版本生成和更新。majorNumber.minorNumber.patchNumberpatchNumber

非托管包不可升级,因此每个包版本只是一组组件 用于分发。包版本对于托管包具有更重要的意义。包 对于不同的版本,可以表现出不同的行为。发布者可以使用包 版本,通过发布来优雅地发展其托管包中的组件 后续软件包版本,而不会破坏现有客户集成,使用 包。

当现有订阅者安装新的包版本时,仍然只有一个实例 包中的每个组件,但这些组件可以模拟旧版本。为 例如,订阅者可以使用包含 Apex 类的托管包。如果 发布者决定弃用 Apex 类中的方法并发布一个新包 版本,订阅者在安装后仍然只能看到 Apex 类的一个实例 新版本。但是,此 Apex 类仍然可以模拟任何 在旧版本中引用已弃用方法的代码。

在托管软件包中开发 Apex 时,请注意以下事项:

  • 包含在 Apex 类、触发器或 Visualforce 组件中的代码,该组件是 托管软件包被模糊处理,无法在安装组织中查看。唯一的例外是 声明为 global 的方法。您可以在安装组织中查看全局方法签名。在 此外,具有“查看和调试托管 Apex”权限的许可证管理组织用户可以查看 当通过订阅者登录订阅者组织时,他们的软件包会混淆 Apex 类 支持控制台。
  • 托管包接收唯一的命名空间。此命名空间在类名之前, 方法、变量等,这有助于防止安装程序组织中出现重复名称。
  • 在单个事务中,只能引用 10 个唯一命名空间。例如,假设 您有一个对象,该对象在更新对象时执行托管包中的类。 然后,该类更新第二个对象,该对象又在不同的 包。即使第一个包没有直接访问第二个包,访问 发生在同一事务中。因此,它包含在 单笔交易。
  • 包开发人员可以使用已弃用的注释来标识 无法再引用的方法、类、异常、枚举、接口和变量 在它们所在的托管包的后续版本中。这在以下情况下很有用 随着需求的发展重构托管包中的代码。
  • 可以编写测试方法,将包版本上下文更改为其他包 版本,使用系统方法。runAs
  • 在以下情况下,不能将方法添加到全局接口,也不能将抽象方法添加到全局类 接口或类已在托管 – 已发布包版本中上传。如果 Managed – Released 包中的类是虚拟的,您可以添加该方法 它也必须是虚拟的,并且必须有一个实现。如果 托管 – 发布包扩展了另一个类,现有类协定不能 被删除。
  • 非托管包中包含的 Apex 代码 显式引用命名空间无法上传。

弃用 Apex

包开发人员可以使用已弃用的注释来标识 无法再引用的方法、类、异常、枚举、接口和变量 在它们所在的托管包的后续版本中。这在以下情况下很有用 随着需求的发展重构托管包中的代码。 将另一个包版本上传为托管版本后 – 已发布,安装最新包版本的新订阅者看不到已弃用的 元素,而这些元素继续为现有订阅者和 API 运行 集成。弃用的项(如方法或类)仍可引用 由包开发人员在内部进行。

注意

您不能在 Apex 类中使用注释,也不能在非托管包中使用触发器。deprecated

包开发人员可以将托管 – Beta 包版本用于 对不同 Salesforce 组织中的一组试点用户进行评估和反馈。如果 开发人员弃用 Apex 标识符,然后将包的版本上传为托管 – Beta 版,安装包版本的订阅者仍会在其中看到已弃用的标识符 包版本。如果包开发人员随后上传托管 – 已发布的包版本, 订阅者在订阅后将不再在包版本中看到已弃用的标识符 安装它。

包版本中的行为

包组件可以在不同的 包版本。此行为版本控制允许您添加新组件 到您的包装中并优化您现有的组件,同时仍然确保 您的代码继续为现有订阅者无缝工作。 如果包开发人员将新组件添加到包中并上传 新的软件包版本,新组件可供订阅者使用 安装新的软件包版本。

  1. 对 Apex 代码行为进行版本控制
  2. 未进行版本控制的 Apex 代码项
  3. 在包版本中测试行为

对 Apex 代码行为进行版本控制

包开发人员可以在 Apex 类和触发器中使用条件逻辑 以显示不同版本的不同行为。条件逻辑允许包 开发人员支持以前包版本中的类和触发器中的现有行为 同时改进代码。

当订阅者安装包的多个版本并编写引用 Apex 的代码时 包中的类或触发器,它们必须选择 他们引用的版本。在 包中引用的 Apex 代码,您可以有条件地执行 基于调用 Apex 代码的版本设置的不同代码路径 进行引用。调用代码的包版本设置可以是 通过调用该方法在包代码中确定。通过这种方式,包开发人员可以 确定请求上下文并为不同版本指定不同的行为 的包。System.requestVersion

以下示例使用该方法并实例化该类,以在 Apex 触发器中为不同的包版本定义不同的行为。System.requestVersionSystem.Version

trigger oppValidation on Opportunity (before insert, before update) {

    for (Opportunity o : Trigger.new){
    
        // Add a new validation to the package
        // Applies to versions of the managed package greater than 1.0
        if (System.requestVersion().compareTo(new Version(1,0)) > 0) {
            if (o.Probability >= 50 && o.Description == null) {
                o.addError('All deals over 50% require a description');
            }
        }

        // Validation applies to all versions of the managed package.
        if (o.IsWon == true && o.LeadSource == null) {
            o.addError('A lead source must be provided for all Closed Won deals');
        }
    }
}

有关使用包版本的方法的完整列表,请参阅 Version 类和 System Class 中的方法。System.requestVersion

如果已安装包中的某个类调用另一个包中的方法,则会保留请求上下文 类。例如,订阅者安装了一个 GeoReports 包,该包 包含 CountryUtil 和 ContinentUtil Apex 类。订阅者创建一个新的 GeoReportsEx 类,并使用版本设置将其绑定到 2.3 版 GeoReports 包。如果 GeoReportsEx 在 ContinentUtil 中调用内部的方法 调用 CountryUtil 中的方法,请求上下文从 ContinentUtil 传播到 CountryUtil 和方法 在 CountryUtil 中返回 GeoReports 包的 2.3 版本。System.requestVersion

未进行版本控制的 Apex 代码项

您可以跨包版本更改某些 Apex 项目的行为。例如,您可以 弃用某个方法,以便新订阅者无法再在后续中引用该包 版本。

但是,无法对以下修饰符、关键字和批注列表进行版本控制。如果 包开发人员对以下修饰符、关键字或注释之一进行更改, 这些更改将反映在所有包版本中。

当这些项目出现以下情况时,您可以对其中某些项目进行的更改存在限制 在托管包的 Apex 代码中使用。

包开发人员可以添加或删除以下项:

  • @future
  • @isTest
  • with sharing
  • without sharing
  • transient

包开发人员可以对以下项进行有限的更改:

  • private—可以更改为global
  • public—可以更改为global
  • protected—可以更改为global
  • abstract– 可以更改为,但不能删除virtual
  • final– 可以删除,但不能删除 添加

程序包开发人员无法删除或更改以下项:

  • global
  • virtual

包开发人员可以添加关键字, 但是一旦添加,就无法删除。

webservice

注意

不能弃用托管方法或变量 软件包代码。webservice

在包版本中测试行为

当您更改不同包版本的 Apex 类或触发器中的行为时,这很重要 测试代码在不同包版本中是否按预期运行。您可以编写更改包的测试方法 使用系统将上下文版本更改为不同的包版本 方法。runAs您可以 仅在测试中使用 方法。runAs

以下示例显示了具有不同行为的触发器 不同的包版本。

trigger oppValidation on Opportunity (before insert, before update) {

    for (Opportunity o : Trigger.new){
    
        // Add a new validation to the package
        // Applies to versions of the managed package greater than 1.0
        if (System.requestVersion().compareTo(new Version(1,0)) > 0) {
            if (o.Probability >= 50 && o.Description == null) {
                o.addError('All deals over 50% require a description');
            }
        }

        // Validation applies to all versions of the managed package.
        if (o.IsWon == true && o.LeadSource == null) {
            o.addError('A lead source must be provided for all Closed Won deals');
        }
    }
}

以下测试类使用该方法验证触发器的行为(有和没有) 特定版本:runAs

@isTest
private class OppTriggerTests{

   static testMethod void testOppValidation(){
   
      // Set up 50% opportunity with no description
      Opportunity o = new Opportunity();
      o.Name = 'Test Job';
      o.Probability = 50;
      o.StageName = 'Prospect';
      o.CloseDate = System.today();
      
      // Test running as latest package version
      try{
          insert o;
      }
      catch(System.DMLException e){
          System.assert(
              e.getMessage().contains(
                'All deals over 50% require a description'),
                  e.getMessage());
      }
      
      // Run test as managed package version 1.0
      System.runAs(new Version(1,0)){
          try{
              insert o;
          }
          catch(System.DMLException e){
              System.assert(false, e.getMessage());
          }
      }

      // Set up a closed won opportunity with no lead source
      o = new Opportunity();
      o.Name = 'Test Job';
      o.Probability = 50;
      o.StageName = 'Prospect';
      o.CloseDate = System.today();
      o.StageName = 'Closed Won';
      
      // Test running as latest package version
      try{
          insert o;
      }
      catch(System.DMLException e){
          System.assert(
            e.getMessage().contains(
              'A lead source must be provided for all Closed Won deals'),
                e.getMessage());
      }

      // Run test as managed package version 1.0
      System.runAs(new Version(1,0)){
          try{
              insert o;
          }
          catch(System.DMLException e){
              System.assert(
                  e.getMessage().contains(
                    'A lead source must be provided for all Closed Won deals'),
                        e.getMessage());
          }
      }
   }
}

测试 Apex

Apex 提供了一个测试框架,允许您编写单元测试,运行 测试,检查测试结果,并有代码覆盖率结果。

让我们来谈谈单元测试、测试的数据可见性以及可用的工具 在 Lightning 平台上用于测试 Apex。我们还将介绍测试最佳实践 以及一个测试示例。

注意

若要保护数据的隐私,请确保测试错误消息和异常 详细信息不包含任何个人数据。Apex 异常处理程序和测试 框架无法确定敏感数据是否包含在用户定义的消息中,并且 详。要在自定义 Apex 例外中包含个人数据,我们建议您 创建一个 Exception 子类,其中包含保存个人数据的新属性。然后 不要在异常的消息字符串中包含子类属性信息。

  • 了解 Apex 中的测试
  • 在 Apex 中测试什么
  • 什么是 Apex 单元测试?
  • 了解测试数据
    Apex 测试数据是暂时性的,不会提交到数据库。
  • 运行单元测试方法
    若要验证 Apex 代码的功能,请执行单元测试。您可以在开发人员控制台、设置、Visual Studio Code 的 Salesforce 扩展中或使用 API 运行 Apex 测试方法。
  • 测试最佳实践
  • 测试实例
  • 测试和代码覆盖率
    Apex 测试框架为您的 Apex 类生成代码覆盖率数字,并在您每次运行一个或多个测试时触发。代码覆盖率指示测试方法已执行类和触发器中有多少行可执行代码。编写测试方法来测试触发器和类,然后运行这些测试以生成代码覆盖率信息。
  • 代码覆盖率最佳实践 请考虑以下代码覆盖率提示和最佳实践
  • 使用 Stub API 构建模拟框架 Apex 提供了一个用于实现模拟框架的存根 API
    。模拟框架有很多好处。它可以简化和改进测试,并帮助您创建更快、更可靠的测试。您可以使用它来单独测试类,这对于单元测试非常重要。使用存根 API 构建模拟框架也是有益的,因为存根对象是在运行时生成的。由于这些对象是动态生成的,因此不必打包和部署测试类。您可以构建自己的模拟框架,也可以使用其他人构建的模拟框架。

了解 Apex 中的测试

测试是长期成功发展的关键,是一种 开发过程的关键组成部分。强烈建议使用测试驱动开发过程,即同时进行的测试开发 时间即代码开发。

为什么要测试 Apex?

测试是应用程序成功的关键,尤其是当您的应用程序要 部署到客户。如果验证应用程序是否按预期工作,则有 没有意外行为,您的客户会更加信任您。

有两种方法可以测试应用程序。一种是通过Salesforce用户界面, 很重要,但仅仅通过用户界面进行测试并不能捕获所有用例 您的应用程序。另一种方法是测试批量功能:最多可以测试 200 条记录 如果代码是使用 SOAP API 或 Visualforce 标准集调用的,则通过代码进行传递 控制器。

应用程序很少完成。您将拥有它的其他版本,您可以在其中进行更改 并扩展功能。如果你已经编写了全面的测试,你可以确保 回归不会随任何新功能一起引入。在为 Salesforce 部署代码或打包代码之前 AppExchange,则必须满足以下条件。

  • 单元测试必须至少覆盖 75% 的 Apex 代码,并且所有这些测试都必须 成功完成。请注意以下事项。
    • 将 Apex 部署到生产组织时,每个单元测试 默认情况下,将执行 Organization 命名空间。
    • 调用不计入 Apex 代码的一部分 覆盖。System.debug
    • 测试方法和测试类不计为 Apex 代码的一部分 覆盖。
    • 虽然只有 75% 的 Apex 代码必须被测试覆盖,但不要专注于 所覆盖代码的百分比。相反,请确保每次使用 涵盖您的申请案例,包括正面和负面案例, 以及批量和单条记录。这种方法可确保 75% 或更多 的代码包含在单元测试中。
  • 每个触发器都必须具有一定的测试覆盖率。
  • 所有类和触发器都必须成功编译。

Salesforce 在具有 Apex 代码的所有组织中运行所有测试,以验证没有行为 已因任何服务升级而更改。

在 Apex 中测试什么

Salesforce 建议您为以下内容编写测试:单次操作测试以验证单个记录是否生成正确的预期结果。批量操作任何 Apex 代码,无论是触发器、类还是扩展,都可以为 1 到 200 调用 记录。您不仅必须测试单个记录案例,还必须测试批量案例。积极行为测试以验证预期行为是否通过每个预期的排列发生,即 是,用户正确填写了所有内容并且没有超出限制。负面行为您的应用程序可能存在限制,例如无法添加未来日期、 无法指定负数,依此类推。您必须测试阴性情况 并验证错误消息是否正确生成,以及对于正错误消息,在 限制情况。受限用户测试对代码中使用的 sObject 具有受限访问权限的用户是否看到 预期行为。也就是说,它们是否可以运行代码或接收错误消息。

注意

条件运算符和三元运算符不被视为执行,除非两者都为正 并执行负分支。

有关这些类型的测试的示例,请参阅测试 示例

什么是 Apex 单元测试?

为了促进健壮、无错误的代码的开发,Apex 支持创建和 执行单元测试。单元测试是验证 特定代码段工作正常。单元测试方法不带任何参数, 不向数据库提交任何数据,也不发送电子邮件。此类方法在方法定义中使用注释进行标记。 单元测试方法必须在测试类中定义,即用 批注的类。@IsTest@IsTest例如:

@IsTest
private class myClass {
    @IsTest
    static void myTest() {
        // code_block
    }
}

使用注解定义类和 仅包含用于测试应用程序的代码的方法。注释可以采用多个修饰符 括号内,并用空格分隔。@IsTest@IsTest

注意

上的注释 methods 等同于关键字。如 最佳做法,Salesforce 建议您使用 而不是 .关键字可以在 未来版本。@IsTesttestMethod@IsTesttestMethodtestMethod

此测试类示例包含两个测试方法。

@IsTest
private class MyTestClass {

   // Methods for testing
   @IsTest
   static void test1() {
      // Implement test code
   }

   @IsTest
   static void test2() {
      // Implement test code
   }

}

类和方法定义为 can be 或 。测试类方法的访问级别 无所谓。在定义测试类或 测试方法。Apex 中的默认访问级别是私有的。测试框架可以 始终找到测试方法并执行它们,无论其访问级别如何。@IsTestprivatepublic

定义为必须是顶级的类 类,不能是接口或枚举。@IsTest

测试类的方法只能从测试方法或测试调用的代码中调用 方法;非测试请求无法调用它。

此示例显示要测试的类及其对应的测试类。它包含两个 方法和构造函数。

public class TVRemoteControl {
    // Volume to be modified
    Integer volume;
    // Constant for maximum volume value
    static final Integer MAX_VOLUME = 50;    
    
    // Constructor
    public TVRemoteControl(Integer v) {
        // Set initial value for volume
        volume = v;
    }
        
    public Integer increaseVolume(Integer amount) {
        volume += amount;
        if (volume > MAX_VOLUME) {
            volume = MAX_VOLUME;
        } 
        return volume;
    }
    
    public Integer decreaseVolume(Integer amount) {
        volume -= amount;
        if (volume < 0) {
            volume = 0;
        }  
        return volume;
    }    
    
    public static String getMenuOptions() {
        return 'AUDIO SETTINGS - VIDEO SETTINGS';
    }
       
}

此示例包含具有四个测试方法的相应测试类。中的每个方法 调用上一个类。尽管有足够的测试覆盖率,但测试 Test 类中的方法执行额外的测试以验证边界条件。

@IsTest
class TVRemoteControlTest {
    @IsTest 
    static void testVolumeIncrease() {
        TVRemoteControl rc = new TVRemoteControl(10);
        Integer newVolume = rc.increaseVolume(15);
        System.assertEquals(25, newVolume);
    }
    
    @IsTest
    static void testVolumeDecrease() {
        TVRemoteControl rc = new TVRemoteControl(20);
        Integer newVolume = rc.decreaseVolume(15);
        System.assertEquals(5, newVolume);        
    } 
        
    @IsTest
    static void testVolumeIncreaseOverMax() {
        TVRemoteControl rc = new TVRemoteControl(10);
        Integer newVolume = rc.increaseVolume(100);
        System.assertEquals(50, newVolume);        
    }
    
    @IsTest
    static void testVolumeDecreaseUnderMin() {
        TVRemoteControl rc = new TVRemoteControl(10);
        Integer newVolume = rc.decreaseVolume(100);
        System.assertEquals(0, newVolume);        
    }
    
    @IsTest
    static void testGetMenuOptions() {
        // Static method call. No need to create a class instance.
        String menu = TVRemoteControl.getMenuOptions();
        System.assertNotEquals(null, menu);
        System.assertNotEquals('', menu);
    }
}

单元测试注意事项

以下是有关单元测试的一些注意事项。

  • 从 Salesforce API 28.0 开始,测试方法不能再驻留在非测试中 类,并且必须是用 注释的类的一部分。请参阅 TestVisible 注解以了解 如何从测试类访问私有类成员。IsTest
  • 测试方法不能用于测试 Web 服务标注。相反,请使用 模拟标注。请参阅测试 Web 服务标注和测试 HTTP 标注。
  • 不能从测试方法发送电子邮件。
  • 由于测试方法不会提交在测试中创建的数据,因此 不必在完成后删除测试数据。
  • 如果测试类中静态成员变量的值在 testSetup 或 test 方法,则不会保留新值。其他测试方法 此类获取静态成员变量的原始值。此行为 当静态成员变量在另一个类中定义时也适用,并且 在测试方法中访问。
  • 对于某些具有具有唯一约束的字段的 sObject,插入重复项 sObject 记录错误结果。例如,插入 CollaborationGroup 具有相同名称的 sObject 会导致错误,因为 CollaborationGroup 记录必须具有唯一的名称。
  • Chatter 摘要中记录(FeedTrackedChange 记录)的跟踪更改不是 当测试方法修改关联记录时可用。FeedTracked更改 记录要求对与其关联的父记录进行更改 在创建数据库之前提交到数据库。由于测试方法没有 提交数据,它们不会导致创建 FeedTrackedChange 记录。 同样,无法在测试方法中创建字段历史记录跟踪记录 因为它们需要首先提交其他 sObject 记录。例如 无法在测试方法中创建 AccountHistory 记录,因为 Account 记录 必须首先提交。
  • 如果您的测试包括 DML,请确保不超过MAX_DML_ROWS 限制。请参阅执行调控器和限制中的“其他顶点限制”
  1. 访问私有测试类成员

访问私有测试类成员

测试方法是在测试类中定义的,与它们测试的类分开。这可以 当必须从测试中访问私有类成员变量时出现问题 方法,或者在调用私有方法时。因为这些是私有的,所以它们不是 对测试类可见。您可以修改类中的代码以公开公共 将使用这些私有类成员的方法,或者您可以简单地注释 这些带有 . 当您使用此批注对私有成员或受保护成员进行批注时,它们可以 由测试方法访问,并且仅在测试上下文中运行代码。TestVisible

此示例说明如何与 私有成员变量,带有构造函数的私有内部类,私有方法, 和私有自定义例外。所有这些都可以在测试类中访问,因为 它们带有 注释。这 类首先列出,后跟一个包含测试方法的测试类。TestVisibleTestVisible

public class VisibleSampleClass {
    // Private member variables
    @TestVisible private Integer recordNumber = 0;
    @TestVisible private String areaCode = '(415)';
    // Public member variable
    public Integer maxRecords = 1000;
    
    // Private inner class
    @TestVisible class Employee {
        String fullName;
        String phone;
        
        // Constructor
        @TestVisible Employee(String s, String ph) {
            fullName = s;
            phone = ph;
        }
    }
       
    // Private method
    @TestVisible private String privateMethod(Employee e) {
        System.debug('I am private.');
        recordNumber++;
        String phone = areaCode + ' ' + e.phone;
        String s = e.fullName + '\'s phone number is ' + phone;
        System.debug(s);
        return s;
    }
    
    // Public method
    public void publicMethod() {
        maxRecords++;
        System.debug('I am public.');    
    }
    
    // Private custom exception class
    @TestVisible private class MyException extends Exception {}
}
// Test class for VisibleSampleClass
@isTest
private class VisibleSampleClassTest {

    // This test method can access private members of another class 
    // that are annotated with @TestVisible.
    static testmethod void test1() {
        VisibleSampleClass sample = new VisibleSampleClass ();

        // Access private data members and update their values
        sample.recordNumber = 100;
        sample.areaCode = '(510)';
        
        // Access private inner class
        VisibleSampleClass.Employee emp = 
            new VisibleSampleClass.Employee('Joe Smith', '555-1212');
        
        // Call private method
        String s = sample.privateMethod(emp);
        
        // Verify result
        System.assert(
            s.contains('(510)') &&
            s.contains('Joe Smith') &&
            s.contains('555-1212'));
    }
    
    // This test method can throw private exception defined in another class
    static testmethod void test2() {
        // Throw private exception.
        try {
            throw new VisibleSampleClass.MyException('Thrown from a test.');
        } catch(VisibleSampleClass.MyException e) {
            // Handle exception 
        }
    }
    
    static testmethod void test3() {
        // Access public method.
        // No @TestVisible is used.
        VisibleSampleClass sample = new VisibleSampleClass ();
        sample.publicMethod();
    }   

}

当您 升级 Salesforce API 包含混合测试代码和非测试代码的现有类的版本。因为测试 从 API 版本 28.0 开始,非测试类中不允许使用方法,您必须 升级 API 版本 时,将测试方法从旧类移动到新的测试类(用 批注的类) 你的班级。在访问私有方法或 原始类的成员变量。在这种情况下,只需注释这些私有 具有 .TestVisibleisTestTestVisible

了解测试数据

Apex 测试数据是暂时性的,不会提交到数据库。

这意味着在测试方法完成执行后,测试插入的数据 不会保留在数据库中。因此,无需删除任何测试数据 测试的结论。同样,对现有记录的所有更改,例如更新或 删除,不要持久。测试数据的这种瞬态行为使得管理 数据更容易,因为您不必执行任何测试数据清理。同时,如果你 测试访问组织数据,这可以防止意外删除或修改现有数据 记录。

默认情况下,现有组织数据对测试方法不可见,但例外 某些设置对象。应尽可能为测试方法创建测试数据。 但是,针对 Salesforce API 版本 23.0 或更早版本保存的测试代码可以访问组织中的所有数据。数据可见性 下一节将更详细地介绍测试。

  • 在单元测试中将测试数据与组织数据隔离
  • 使用 isTest(SeeAllData=True) 批注 批注
    测试类或测试方法,以打开对组织中记录的数据访问。IsTest(SeeAllData=true) 批注适用于数据查询,但不适用于记录创建或更改,包括删除。即使在使用注释时,新记录和更改的记录仍会在 Apex 测试中回滚。IsTest(SeeAllData=true)
  • 加载测试数据 使用该方法,可以在测试方法中填充数据
    ,而无需编写许多代码行。Test.loadData
  • 用于创建
    测试数据的公共测试实用程序类 公共测试实用程序类是公共测试类,其中包含用于创建测试数据的可重用代码。
  • 使用测试设置方法 使用测试设置方法(用 批注的方法)创建一次测试记录,然后在测试类中的每个测试方法
    中访问它们。当您需要为所有测试方法创建参考或先决条件数据,或者需要为所有测试方法操作的一组通用记录创建时,测试设置方法可以节省时间。@testSetup

将测试数据与单元中的组织数据隔离 测试

默认情况下,Apex 测试方法(API 版本 24.0 及更高版本)不能 访问预先存在的组织数据,例如标准对象、自定义对象和自定义设置 数据。他们只能访问他们创建的数据。但是,用于管理 仍然可以在测试中访问您的组织或元数据对象。这些是一些 此类对象的示例。

  • 用户
  • 轮廓
  • 组织
  • Cron触发器
  • 记录类型
  • 顶点类
  • Apex触发器
  • Apex组件
  • ApexPage(顶点页面)

尽可能为每个测试创建测试数据。您可以通过以下方式禁用此限制 使用注释注释测试类或测试方法。IsTest(SeeAllData=true)

使用 Salesforce API 版本 23.0 或更早版本保存的测试代码 继续有权访问组织中的所有数据,其数据访问权限为 变。数据访问注意事项

  • 使用数据孤岛 Apex 测试时,不支持使用该关系的跨对象字段引用。由于 此限制在数据孤岛 Apex 测试中运行时返回 null。OwnerSELECT Owner.IsActive FROM Account
  • 如果使用 Salesforce API 版本 24.0 或更高版本保存的新测试方法调用方法 在使用版本 23.0 或更低版本保存的另一个类中,数据访问限制 调用方在被调用的方法中强制执行。调用的方法无法访问 组织数据,因为调用方无法访问它,即使它保存在 早期版本。
  • 注解具有 添加到使用 Salesforce API 版本 23.0 保存的 Apex 代码时不起作用,并且 早些时候。IsTest(SeeAllData=true)
  • 此对测试数据的访问限制适用于所有代码 在测试上下文中运行。例如,如果测试方法导致触发器执行 并且测试无法访问组织数据,触发器将无法 也。
  • 如果测试发出 Visualforce 请求,则执行的测试将停留在测试上下文中,但 在不同的线程中运行。因此,不再强制执行测试数据隔离。在 在这种情况下,测试将能够访问组织中的所有数据 发起 Visualforce 请求。但是,如果 Visualforce 请求执行 callback,例如 JavaScript remoting 调用,则回调插入的任何数据都不是 对测试可见。
  • 在 API 版本 27.0 及更早版本中,VLOOKUP 验证规则函数始终查找 当由正在运行的 Apex 测试触发时,除了测试数据外,还会向上组织数据。开头为 版本 28.0,VLOOKUP 验证规则函数不再访问组织 来自正在运行的 Apex 测试的数据。该函数仅查找测试创建的数据, 除非测试类或方法用 注释。IsTest(SeeAllData=true)
  • 在某些情况下,您可能无法从测试中创建某些类型的数据 由于特定的限制,方法。以下是此类限制的一些示例。
    • 某些标准对象不可创建。有关这些对象的详细信息, 请参阅对象参考 Salesforce的。
    • 对于某些具有具有唯一约束的字段的 sObject,插入重复项 sObject 记录错误结果。例如,插入 CollaborationGroup 具有相同名称的 sObject 会导致错误,因为 CollaborationGroup 记录必须具有唯一的名称。无论您的测试是否被批注,都会发生此错误 有 ,或没有。IsTest(SeeAllData=true)
    • 仅在将相关记录提交到 数据库,例如 Chatter 中跟踪的更改。记录的跟踪更改 (FeedTrackedChange 记录)在测试方法时不可用 修改关联的记录。FeedTrackedChange 记录需要更改为 之前要提交到数据库的与它们关联的父记录 它们被创造出来。由于测试方法不提交数据,因此它们不会导致 创建 FeedTrackedChange 记录。同样,字段历史记录跟踪记录 无法在测试方法中创建,因为它们需要其他 sObject 记录 首先承诺。例如,无法在测试中创建 AccountHistory 记录 方法,因为必须首先提交帐户记录。

使用 isTest(SeeAllData=True) 注解

对测试类或测试方法进行批注,以打开对组织中记录的数据访问。这 IsTest(SeeAllData=true) 批注适用于数据查询,但不适用于记录创建 或更改,包括删除。新记录和更改的记录仍会在 Apex 测试中回滚 即使在使用注释时。

IsTest(SeeAllData=true)

警告

通过用 注释类,可以允许测试方法访问所有组织记录。这 但是,最佳做法是使用 .取决于 API 版本 您正在使用时,默认注释可能会有所不同。@isTest(SeeAllData=true)@isTest(SeeAllData=false)此示例演示如何使用注释定义测试类。所有 此类中的测试方法可以访问 组织。

@IsTest(SeeAllData=true)

// All test methods in this class can access all data.
@IsTest(SeeAllData=true)
public class TestDataAccessClass {

    // This test accesses an existing account. 
    // It also creates and accesses a new test account.
    @IsTest
    static void myTestMethod1() {
        // Query an existing account in the organization. 
        Account a = [SELECT Id, Name FROM Account WHERE Name='Acme' LIMIT 1];
        System.assert(a != null);
        
        // Create a test account based on the queried account.
        Account testAccount = a.clone();
        testAccount.Name = 'Acme Test';
        insert testAccount;
        
        // Query the test account that was inserted.
        Account testAccount2 = [SELECT Id, Name FROM Account 
                                WHERE Name='Acme Test' LIMIT 1];
        System.assert(testAccount2 != null);
    }
       
    
    // Like the previous method, this test method can also access all data
    // because the containing class is annotated with @IsTest(SeeAllData=true).
    @IsTest
    static void myTestMethod2() {
        // Can access all data in the organization.
   }
  
}

第二个示例演示如何在测试上应用注释 方法。由于测试方法的类没有注释,因此必须注释 方法,以便能够访问该方法的所有数据。第二种测试方法没有 具有此注释,因此它只能访问它创建的数据。此外,它还可以 访问用于管理组织的对象,例如 用户。

@IsTest(SeeAllData=true)

// This class contains test methods with different data access levels.
@IsTest
private class ClassWithDifferentDataAccess {

    // Test method that has access to all data.
    @IsTest(SeeAllData=true)
    static void testWithAllDataAccess() {
        // Can query all data in the organization.      
    }
    
    // Test method that has access to only the data it creates
    // and organization setup and metadata objects.
    @IsTest
    static void testWithOwnDataAccess() {
        // This method can still access the User object.
        // This query returns the first user object.
        User u = [SELECT UserName,Email FROM User LIMIT 1]; 
        System.debug('UserName: ' + u.UserName);
        System.debug('Email: ' + u.Email);
        
        // Can access the test account that is created here.
        Account a = new Account(Name='Test Account');
        insert a;      
        // Access the account that was just created.
        Account insertedAcct = [SELECT Id,Name FROM Account 
                                WHERE Name='Test Account'];
        System.assert(insertedAcct != null);
    }
}

注释的注意事项@IsTest(SeeAllData=true)

  • 如果测试类是用注解定义的,则 未显式设置关键字的测试方法。@IsTest(SeeAllData=true)SeeAllData=trueSeeAllData
  • 注解用于打开 在类或方法级别应用时的数据访问。但是,如果 包含类已用 、 对于方法,将忽略对方法进行批注。 在这种情况下,该方法仍然可以访问 组织。使用重写对方法进行注释,对于该方法,对 类。@IsTest(SeeAllData=true)@IsTest(SeeAllData=true)@IsTest(SeeAllData=false)@IsTest(SeeAllData=true)@IsTest(SeeAllData=false)
  • @IsTest(SeeAllData=true)并且不能使用注释 一起使用相同的 Apex 方法。@IsTest(IsParallel=true)

加载测试数据

使用该方法,可以在测试方法中填充数据,而无需 编写多行代码。

Test.loadData请按照下列步骤操作:

  1. 在 .csv 文件中添加数据。
  2. 为此文件创建静态资源。
  3. 在测试中调用 方法,并向其传递 sObject 类型令牌和静态资源名称。Test.loadData

例如,对于帐户记录和静态资源名称 ,进行以下调用:

myResource

List<sObject> ls = Test.loadData(Account.sObjectType, 'myResource');

方法 返回与插入的每条记录相对应的 sObject 列表。Test.loadData

在调用此方法之前,必须创建静态资源。 静态资源是以逗号分隔的文件,以 .csv 扩展名结尾。 该文件包含测试记录的字段名称和值。这 文件的第一行必须包含字段名称,随后的行 行是字段值。若要了解有关静态资源的详细信息,请执行以下操作: 请参阅 Salesforce 联机帮助中的“定义静态资源”。创建后 .csv 文件的静态资源,静态资源将是 分配了 MIME 类型。支持的 MIME 类型包括:

  • 文本/CSV
  • 应用程序/vnd.ms-excel
  • application/octet-stream
  • 文本/纯文本

Test.loadData 示例

以下是步骤 用于创建示例 .csv 文件和静态资源,并调用以插入测试记录。

Test.loadData

  1. 创建一个包含测试记录数据的 .csv 文件。此示例 .csv 文件有三个 帐户记录。您可以使用此示例内容创建 .csv 文件。Name,Website,Phone,BillingStreet,BillingCity,BillingState,BillingPostalCode,BillingCountry sForceTest1,http://www.sforcetest1.com,(415) 901-7000,The Landmark @ One Market,San Francisco,CA,94105,US sForceTest2,http://www.sforcetest2.com,(415) 901-7000,The Landmark @ One Market Suite 300,San Francisco,CA,94105,US sForceTest3,http://www.sforcetest3.com,(415) 901-7000,1 Market St,San Francisco,CA,94105,US
  2. 为 .csv 文件创建静态资源:
    1. 在“设置”中,输入“快速查找”框,然后选择“静态资源”。Static Resources
    2. 单击“新建”。
    3. 将静态资源命名为 。testAccounts
    4. 选择您创建的文件。
    5. 点击保存
  3. 呼入 用于填充测试帐户的测试方法。Test.loadData@isTest private class DataUtil { static testmethod void testLoadData() { // Load the test accounts from the static resource List<sObject> ls = Test.loadData(Account.sObjectType, 'testAccounts'); // Verify that all 3 test accounts were created System.assert(ls.size() == 3); // Get first test account Account a1 = (Account)ls[0]; String acctName = a1.Name; System.debug(acctName); // Perform some testing using the test records } }

测试数据的通用测试实用程序类 创造

常见的测试实用程序类是公共测试类,它们 包含用于创建测试数据的可重用代码。

公共测试实用程序类使用注释进行定义,因此被排除在组织代码大小限制之外并执行 在测试上下文中。它们可以由测试方法调用,但不能由非测试代码调用。IsTest

公共测试实用程序类中的方法的定义方式与非测试中的方法相同 类。它们可以接受参数并返回值。必须声明这些方法 作为公共或全局,以便对其他测试类可见。这些常用方法可以是 由 Apex 类中的任何测试方法调用,以在运行 测试。虽然您可以在常规 Apex 中创建用于创建测试数据的公共方法 类,如果没有注解,你 不要从组织代码大小中排除此代码的好处 限制。IsTest

这是测试实用程序类的一个示例。它包含一个方法,该方法接受 要创建的帐户数和每个帐户的联系人数。 下一个示例演示一个测试方法,该方法调用此方法以创建 一些数据。createTestRecords

@IsTest
public class TestDataFactory {
    public static void createTestRecords(Integer numAccts, Integer numContactsPerAcct) {
        List<Account> accts = new List<Account>();
        
        for(Integer i=0;i<numAccts;i++) {
            Account a = new Account(Name='TestAccount' + i);
            accts.add(a);
        }
        insert accts;
        
        List<Contact> cons = new List<Contact>();
        for (Integer j=0;j<numAccts;j++) {
            Account acct = accts[j];            
            // For each account just inserted, add contacts
            for (Integer k=numContactsPerAcct*j;k<numContactsPerAcct*(j+1);k++) {
                cons.add(new Contact(firstname='Test'+k,
                                     lastname='Test'+k,
                                     AccountId=acct.Id));
            }
        }
        // Insert all contacts for all accounts
        insert cons;
    }
}

此类中的测试方法调用测试实用程序方法 ,以创建 5 个 测试帐户,每个帐户有三个联系人。createTestRecords

@IsTest
private class MyTestClass {
    static testmethod void test1() {
        TestDataFactory.createTestRecords(5,3);
        // Run some tests
    }
}

使用测试设置方法

使用测试设置方法(带有 注释的方法)创建一次测试记录,然后再创建一次 在测试类的每个测试方法中访问它们。测试设置方法可以是 当您需要为所有测试方法创建参考或先决条件数据时,可以节省时间, 或所有测试方法都操作的一组通用记录。@testSetup

测试设置方法可以减少测试执行时间,尤其是在 您正在处理许多记录。测试设置方法使您能够创建通用测试 轻松高效地获取数据。通过为班级设置一次记录,您不需要 为每个测试方法重新创建记录。此外,因为回滚的记录 在测试设置期间创建,发生在整个类执行结束时, 减少回滚的记录数。因此,系统资源是 与创建这些记录并回滚这些记录相比,使用效率更高 每种测试方法。

如果测试类包含测试设置方法,则测试 Framework 首先执行测试设置方法,然后再执行类中的任何测试方法。 在测试设置方法中创建的记录可用于 测试类,并在测试类执行结束时回滚。如果测试方法 更改这些记录,例如记录字段更新或记录删除,这些更改 在每个测试方法完成执行后回滚。下一个执行测试 方法获取对这些记录的原始未修改状态的访问。

语法

测试设置方法是在测试类中定义的,不带任何参数,也不返回 价值。以下是测试设置方法的语法。

@testSetup static void methodName() {

}

以下示例演示如何创建一次测试记录,然后在 多种测试方法。此外,该示例还显示了如何在 第一个测试方法将回滚,并且不适用于第二个测试 方法。

@isTest
private class CommonTestSetup {

    @testSetup static void setup() {
        // Create common test accounts
        List<Account> testAccts = new List<Account>();
        for(Integer i=0;i<2;i++) {
            testAccts.add(new Account(Name = 'TestAcct'+i));
        }
        insert testAccts;        
    }
    
    @isTest static void testMethod1() {
        // Get the first test account by using a SOQL query
        Account acct = [SELECT Id FROM Account WHERE Name='TestAcct0' LIMIT 1];
        // Modify first account
        acct.Phone = '555-1212';
        // This update is local to this test method only.
        update acct;
        
        // Delete second account
        Account acct2 = [SELECT Id FROM Account WHERE Name='TestAcct1' LIMIT 1];
        // This deletion is local to this test method only.
        delete acct2;
        
        // Perform some testing
    }

    @isTest static void testMethod2() {
        // The changes made by testMethod1() are rolled back and 
        // are not visible to this test method.        
        // Get the first account by using a SOQL query
        Account acct = [SELECT Phone FROM Account WHERE Name='TestAcct0' LIMIT 1];
        // Verify that test account created by test setup method is unaltered.
        System.assertEquals(null, acct.Phone);
        
        // Get the second account by using a SOQL query
        Account acct2 = [SELECT Id FROM Account WHERE Name='TestAcct1' LIMIT 1];
        // Verify test account created by test setup method is unaltered.
        System.assertNotEquals(null, acct2);
        
        // Perform some testing
    }

}

测试设置方法注意事项

  • 测试设置方法仅支持 测试类的默认数据隔离模式。如果测试类或测试 方法可以使用注释访问组织数据, 此类不支持测试设置方法。因为数据隔离 for tests 可用于 API 版本 24.0 及更高版本的测试设置方法 也仅适用于这些版本。@isTest(SeeAllData=true)
  • 每个测试类只能有一种测试设置方法。
  • 如果在执行测试设置方法期间发生致命错误,例如 由 DML 操作或断言失败导致的异常,整个 测试类失败,并且不会执行该类中的其他测试。
  • 如果测试设置方法调用另一个类的非测试方法,则无代码 覆盖率是针对非测试方法计算的。

运行单元测试方法

若要验证 Apex 代码的功能,请执行单元测试。您可以运行 Apex 在开发人员控制台、设置和 Salesforce Visual 扩展中测试方法 Studio Code,或使用 API。可以运行这些单元测试分组。

  • 特定类中的部分或全部方法
  • 一组类中的部分或全部方法
  • 预定义的类套件,称为测试套件
  • 组织中的所有单元测试

若要运行测试,请使用以下任一方法:

  • Salesforce 用户界面
  • 适用于 Visual Studio 的 Salesforce 扩展 法典
  • 闪电平台开发人员 安慰
  • The API

从 Salesforce 用户界面启动的所有 Apex 测试(包括 Developer Console)异步并行运行。Apex 测试类放置在 用于执行的 Apex 作业队列。可以运行的最大测试类数 每 24 小时是 500 或 10 乘以测试类数的较大值 在组织中。对于沙盒和 Developer Edition 组织,此限制更高,并且是 500 或 20 中的较大值乘以组织中的测试类数。

注意

作为部署的一部分运行的 Apex 测试始终同步运行,并且 串行。

通过 Salesforce 用户运行测试 接口

您可以在 Apex 测试执行页面上运行单元测试。在此页面上开始测试 异步运行,也就是说,您不必等待测试类执行 完成。“Apex Test Execution”(Apex 测试执行)页面刷新测试状态,并显示 测试完成后的结果。

Apex 测试执行页面
  1. 在“设置”中,输入“快速查找”框,然后选择“Apex 测试” 执行Apex Test Execution
  2. 单击“选择测试…”。注意如果你 具有从托管包安装的 Apex 类,您必须 首先通过单击“全部编译”来编译这些类 Apex 类页面上的类,以便它们显示在 列表。
  3. 选择要运行的测试。测试列表仅包括 包含测试方法。
    • 若要从已安装的托管包中选择测试,请选择 下拉列表中托管包的相应命名空间。 仅具有所选命名空间的托管包的类 出现在列表中。
    • 若要选择组织中本地存在的测试,请从下拉列表中选择“[我的命名空间]”。 只有不是来自托管包的本地类才会出现在 列表。
    • 要选择任何测试,请从下拉列表中选择[所有命名空间]。将显示组织中的所有类, 无论它们是否来自托管包。
    注意当前正在运行测试的类不会出现在 列表。
  4. 若要选择不在测试运行期间收集代码覆盖率信息,请选择“跳过代码覆盖率”。
  5. 单击运行

使用 Apex Test Execution 页面运行测试后,您可以查看代码覆盖率 开发者控制台中的详细信息。

在“设置”中,输入“快速查找”框,选择“Apex 测试执行”,然后单击“查看” 测试历史记录,用于查看组织的所有测试结果,而不是 只是您运行的测试。测试结果在测试后保留 30 天 完成运行,除非清除。Apex

使用 Salesforce 扩展运行测试 针对 Visual Studio Code

可以使用 Visual Studio Code 执行测试。请参阅适用于 Visual Studio 的 Salesforce 扩展 代码。

使用 Lightning 运行测试 平台开发人员控制台

在开发者控制台中,您可以在特定的测试类中执行部分或全部测试, 设置和运行测试套件,或运行所有测试。开发者控制台运行测试 在后台异步运行,除非测试运行仅包含一个类和 你尚未在“测试”菜单中选择“始终异步运行”。 通过异步运行测试,您可以在开发者控制台的其他区域工作 在测试运行时。测试完成执行后,可以检查测试 结果在开发者控制台中。此外,还可以检查整体代码覆盖率 对于测试涵盖的课程。

有关更多信息,请参阅 Salesforce 中的开发人员控制台文档 帮助。

使用 API 运行测试

您可以使用来自 SOAP 的调用 用于运行测试的 API 同步。

runTests()

RunTestsResult[] runTests(RunTestsRequest ri)

此调用允许您运行所有类中的所有测试,以及特定类中的所有测试 命名空间,或指定特定命名空间中类子集中的所有测试 在 RunTestsRequest 对象中。它返回以下内容。

  • 运行的测试总数
  • 代码覆盖率统计
  • 每个失败测试的错误信息
  • 每个成功的测试的信息
  • 运行测试所花费的时间

有关 的更多信息,请参见 SOAP API 中的 runTests() 开发人员指南。runTests()

还可以使用工具 REST API 运行测试。使用 和 终结点运行测试 异步或同步。有关用法的详细信息,请参阅工具 API:REST 资源。/runTestsAsynchronous//runTestsSynchronous/

使用 ApexTestQueueItem 运行测试

可以使用 ApexTestQueueItem 和 ApexTestResult 异步运行测试。这些对象允许您 将测试添加到 Apex 作业队列,并检查已完成的测试运行的结果。 此过程不仅使你能够异步启动测试,还使你能够安排 使用 Apex 调度程序在特定时间执行的测试。有关详细信息,请参阅 Apex Scheduler。

插入一个对象以放置其 Apex 作业队列中相应的 Apex 类以供执行。Apex 作业执行 类中的测试方法。作业执行后,包含每个测试方法的结果 作为测试的一部分执行。ApexTestQueueItemApexTestResult

要中止 Apex 作业队列中的类,请在 ApexTestQueueItem 对象,并将其 Status 字段设置为 。Aborted

如果在单个批量操作中插入多个 Apex 测试队列项,则队列 项目共享相同的父作业。这意味着测试运行可以包括 如果插入了所有测试队列项,则执行多个类的测试 在相同的批量操作中。

可以在 Apex 作业队列是 500 或 10 乘以测试类数中的较大值 在组织中。对于沙盒和 Developer Edition 组织,此限制更高 并且是 500 或 20 中的较大值乘以 组织。此示例使用 DML 操作插入和查询 和 对象。该方法为以 测试。然后,它返回一个队列项的父作业 ID,该 ID 对于所有队列项都是相同的 对项目进行排队,因为它们是批量插入的。该方法检索对应于 指定的作业 ID。然后,它查询并输出名称、作业状态和传递 每个班级的费率。该方法获取作为 工作。

ApexTestQueueItemApexTestResultenqueueTestscheckClassStatuscheckMethodStatus

public class TestUtil {

    // Enqueue all classes ending in "Test". 
    public static ID enqueueTests() {
        ApexClass[] testClasses = 
           [SELECT Id FROM ApexClass 
            WHERE Name LIKE '%Test'];
        if (testClasses.size() > 0) {
            ApexTestQueueItem[] queueItems = new List<ApexTestQueueItem>();
            for (ApexClass cls : testClasses) {
                queueItems.add(new ApexTestQueueItem(ApexClassId=cls.Id));
            }

            insert queueItems;

            // Get the job ID of the first queue item returned.
            ApexTestQueueItem item = 
               [SELECT ParentJobId FROM ApexTestQueueItem 
                WHERE Id=:queueItems[0].Id LIMIT 1];
            return item.parentjobid;
        }
        return null;
    }

    // Get the status and pass rate for each class
    // whose tests were run by the job.
    // that correspond to the specified job ID.
    public static void checkClassStatus(ID jobId) {
        ApexTestQueueItem[] items = 
           [SELECT ApexClass.Name, Status, ExtendedStatus 
            FROM ApexTestQueueItem 
            WHERE ParentJobId=:jobId];
        for (ApexTestQueueItem item : items) {
            String extStatus = item.extendedstatus == null ? '' : item.extendedStatus;
            System.debug(item.ApexClass.Name + ': ' + item.Status + extStatus);
        }
    }

    // Get the result for each test method that was executed.
    public static void checkMethodStatus(ID jobId) {
        ApexTestResult[] results = 
           [SELECT Outcome, ApexClass.Name, MethodName, Message, StackTrace 
            FROM ApexTestResult 
            WHERE AsyncApexJobId=:jobId];
        for (ApexTestResult atr : results) {
            System.debug(atr.ApexClass.Name + '.' + atr.MethodName + ': ' + atr.Outcome);
            if (atr.message != null) {
                System.debug(atr.Message + '\n at ' + atr.StackTrace);
            }
        }
    }
}
  1. 使用 runAs 方法
  2. 使用 Limits、startTest 和 stopTest
  3. 将 SOSL 查询添加到单元测试

使用 runAs 方法

通常,所有 Apex 代码都运行在系统模式下,其中权限和记录共享 的当前用户不被考虑在内。使用系统方法,可以编写更改的测试方法 现有用户或新用户的用户上下文,以便用户的记录 强制执行共享。该方法强制执行 记录 共享。 用户权限和字段级权限将应用于新的上下文用户,因为 在强制执行对象和字段权限中进行了描述。runAsrunAs

注意

无论测试类的共享模式如何,都会在块内强制执行用户的共享权限。如果 在块中调用用户定义的方法, 强制执行的共享模式是定义方法的类的共享模式。runAsrunAs

您只能在测试方法中使用。这 完成所有测试方法后,将再次启动原始系统上下文。runAsrunAs

该方法忽略用户许可证限制。你 能 创造 即使你的组织没有 其他用户许可证。runAsrunAs

注意

每个调用都很重要 相对于流程中发出的 DML 语句总数。runAs

在以下示例中,将创建一个新的测试用户,然后以该用户身份运行代码,其中 该用户的记录共享访问权限:

@isTest
private class TestRunAs {
   public static testMethod void testRunAs() {
        // Setup test data
        // Create a unique UserName
        String uniqueUserName = 'standarduser' + DateTime.now().getTime() + '@testorg.com';
        // This code runs as the system user
        Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
        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=uniqueUserName);

        System.runAs(u) {
              // The following code runs as user 'u'
              System.debug('Current User: ' + UserInfo.getUserName());
              System.debug('Current Profile: ' + UserInfo.getProfileId());
          }
    }
}

可以嵌套多个方法。为 例:runAs

@isTest
private class TestRunAs2 {

   public static testMethod void test2() {

      Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
      User u2 = new User(Alias = 'newUser', Email='newuser@testorg.com',
         EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US',
         LocaleSidKey='en_US', ProfileId = p.Id,
         TimeZoneSidKey='America/Los_Angeles', UserName='newuser@testorg.com');

      System.runAs(u2) {
         // The following code runs as user u2.
         System.debug('Current User: ' + UserInfo.getUserName());
         System.debug('Current Profile: ' + UserInfo.getProfileId());

         // The following code runs as user u3.
         User u3 = [SELECT Id FROM User WHERE UserName='newuser@testorg.com'];
         System.runAs(u3) {
            System.debug('Current User: ' + UserInfo.getUserName());
            System.debug('Current Profile: ' + UserInfo.getProfileId());
         }

         // Any additional code here would run as user u2.
      }
   }
}

runAs 的其他用途

您还可以使用该方法来执行 通过将 DML 操作封闭在块中,在测试中混合 DML 操作。这样,您就可以绕过混合 DML 在插入或更新安装对象时返回的错误 其他 sObjects。请参见不能一起使用的 sObjects 在 DML 操作中。runAsrunAs

runAs 方法 (runAs(System.Version)) 的另一个重载将包版本作为参数。这 方法导致使用特定版本的托管包的代码。为 有关使用 runAs 方法和 指定包版本上下文,请参阅测试包中的行为 版本。

使用 Limits、startTest 和 stopTest

Limits 方法返回特定调控器的特定限制,例如数字 方法的调用次数或剩余的堆大小量。

每种方法都有两个版本。第一个版本返回具有 在当前上下文中使用。第二个版本包含“限制”一词 并返回可用于该上下文的资源总量。例如,将标注数返回到 已在当前上下文中处理的外部服务,而返回标注的总数 在给定的上下文中可用。getCalloutsgetLimitCallouts

除了 Limits 方法之外,还可以使用 和 方法来验证代码的接近程度 是达到调速器限制。startTeststopTest

该方法标记测试中的点 在测试实际开始时编写代码。每个测试方法只允许调用此方法 一次。此方法之前的所有代码都应用于初始化变量、填充数据 结构等,允许您设置运行测试所需的一切。任何代码 在调用之后执行,并在调用之前执行,分配了一组新的调控器 限制。startTeststartTeststopTest

该方法不会刷新 测试:它为测试添加上下文。例如,如果您的类进行 98 次 SOQL 查询 在它调用之前,第一个重要的 后面的语句是 DML 语句, 程序现在可以进行额外的 100 次查询。但是,一旦被调用,程序就会返回到原始上下文,并且 在达到 100 个限制之前,只能进行 2 个额外的 SOQL 查询。startTeststartTeststartTeststopTest

当测试结束时,该方法在测试代码中标记该点。stopTest在 与方法结合使用。每次测试 方法只允许调用此方法一次。在为方法分配原始限制后执行的任何代码 实际上之前被调用了。都 在该方法之后进行的异步调用是 由系统收集。startTeststopTeststartTeststartTest执行时,将运行所有异步进程 同步。stopTest

将 SOSL 查询添加到单元测试

为确保测试方法始终以可预测的方式运行,任何 Salesforce 对象 添加到 Apex 测试方法的搜索语言 (SOSL) 查询将返回一组空的 测试方法执行时的搜索结果。如果您不希望查询返回 结果列表为空,您可以使用系统方法定义记录 ID 列表,该 由搜索返回。稍后在测试方法中发生的所有 SOSL 查询 返回方法指定的记录 ID 列表。此外,测试方法可以多次调用 为不同的 SOSL 查询定义不同的结果集。如果未在测试方法中调用该方法,或者 如果在未指定记录 ID 列表的情况下调用此方法,则任何 SOSL 查询 place later in the test 方法返回一个空的结果列表。Test.setFixedSearchResultsTest.setFixedSearchResultsTest.setFixedSearchResultsTest.setFixedSearchResults

该方法指定的记录 ID 列表将替换通常 如果 SOSL 查询不受任何 OR 子句的约束,则由 SOSL 查询返回。如果这些 子句存在于 SOSL 查询中,它们应用于固定搜索结果列表。为 例:Test.setFixedSearchResultsWHERELIMIT

@isTest
private class SoslFixedResultsTest1 {

    public static testMethod void testSoslFixedResults() {
       Id [] fixedSearchResults= new Id[1];
       fixedSearchResults[0] = '001x0000003G89h';
       Test.setFixedSearchResults(fixedSearchResults);
       List<List<SObject>> searchList = [FIND 'test' 
                                         IN ALL FIELDS RETURNING 
                                            Account(id, name WHERE name = 'test' LIMIT 1)];
    }
}

注意

ContentDocument (File) 或 ContentNote (Note) 实体的 SOSL 查询需要与 ContentVersion ID 一起使用,以便与 Salesforce 索引的方式保持一致 并搜索文件和笔记。setFixedSearchResults

尽管 ID 为 的客户记录可能与 FIND 子句 () 中的查询字符串不匹配,但该记录将传递到 SOSL 语句的子句中。如果记录 如果 ID 与子句筛选器匹配,则返回记录。如果是这样 不匹配子句,无记录 返回。001x0000003G89h‘test’RETURNING001x0000003G89hWHEREWHERE

测试最佳实践

好的测试可以做到以下几点:

  • 覆盖尽可能多的代码行。在部署 Apex 或 将其打包为 AppExchange,则必须满足以下条件。重要
    • 单元测试必须覆盖至少 75% 的 Apex 代码,以及所有 这些测试必须成功完成。
    • 每个触发器都必须具有一定的测试覆盖率。
    • 所有类和触发器都必须成功编译。
    请注意以下事项。
    • 将 Apex 部署到生产组织时,每个 组织命名空间中的单元测试由 违约。
    • 调用不计入 Apex 代码的一部分 覆盖。System.debug
    • 测试方法和测试类不计入 Apex 代码覆盖率。
    • 虽然只有 75% 的 Apex 代码必须被 测试,不要关注代码的百分比 覆盖。相反,请确保你的每个用例 涵盖应用,包括正面和负面 案例,以及批量和单条记录。这种方法 确保 75% 或更多的代码被单元覆盖 测试。
  • 如果代码使用条件逻辑(包括三元运算符),请执行每个分支。
  • 使用有效和无效输入调用方法。
  • 成功完成而不会引发任何异常,除非这些错误是预期的 并被困在一个街区。try…catch
  • 始终处理捕获的所有异常,而不仅仅是捕获 异常。
  • 使用方法证明代码 行为正常。System.assert
  • 使用该方法测试应用程序 在不同的用户上下文中。runAs
  • 练习批量触发器功能 – 在测试中使用至少 20 条记录。
  • 使用关键字确保 记录按预期顺序返回。ORDER BY
  • 不假定记录 ID 按顺序排列。记录 ID 不是在 升序,除非您使用同一请求插入多条记录。例如 如果您创建了一个帐户 A,并接收了 ID,则创建帐户 B,则帐户 B 的 ID 可能会,也可能不会 依次走高。001D000000IEEmT
  • 设置测试数据:
    • 在测试类中创建必要的数据,以便测试不必依赖数据 在特定组织中。
    • 在调用方法之前创建所有测试数据。Test.startTest
    • 由于测试不提交,因此无需删除任何数据。
  • 写评论,不仅要说明应该测试的内容,还要说明假设 测试人员对数据、预期结果等进行了测试。
  • 单独测试应用程序中的类。永远不要测试你的整个应用程序 一次测试。

注意

若要保护数据的隐私,请确保测试错误消息和异常 详细信息不包含任何个人数据。Apex 异常处理程序和测试 框架无法确定敏感数据是否包含在用户定义的消息中,并且 详。要在自定义 Apex 例外中包含个人数据,我们建议您 创建一个具有新属性的 Exception 子类,用于保存个人数据。然后 不要在异常的消息字符串中包含子类属性信息。

如果要运行许多测试,请测试组织中的类 在 Salesforce 用户界面中单独使用,而不是使用“全部运行” “测试”按钮将它们一起运行。

并行测试执行的最佳实践

从 Salesforce 用户界面(包括开发人员控制台)启动的测试 并行运行。并行测试执行可以加快测试运行时间。有时,并行 测试执行会导致数据争用问题,您可以在 那些情况。具体而言,在以下情况下可能会发生数据争用问题和错误:

UNABLE_TO_LOCK_ROW

  • 当测试同时更新相同的记录时。更新相同的记录 通常在测试不创建自己的数据并关闭数据时发生 隔离以访问组织的数据。
  • 当并行运行并尝试创建 具有重复索引字段值的记录。当两个正在运行的测试发生时,会发生死锁 等待彼此回滚数据。如果插入两个测试,则可能会发生这种等待 以不同顺序具有相同唯一索引字段值的记录。

您可以通过在 Salesforce 用户界面:

  1. 在“设置”中,输入 。Apex Test
  2. 单击“选项…”。
  3. 在“Apex 测试执行选项”对话框中,选择“禁用并行 Apex” “测试”,然后单击“确定”。

带批注的测试类指示测试类可以同时运行,并且超过默认数量 并发测试类。此批注将覆盖默认设置。IsTest(IsParallel=true)

测试实例

以下示例包括以下类型的案例 测试:

  • 具有单条和多条记录的阳性病例
  • 单条和多条记录的负数大小写
  • 与其他用户一起测试

该测试与简单的里程跟踪应用程序一起使用。现有代码 应用程序验证一天内输入的里程不超过 500 英里。这 主要对象是名为 Mileage__c 的自定义对象。该测试创建一条包含 300 的记录 英里并验证仅记录了 300 英里。然后循环创建 200 条记录 每人一英里。最后,它验证总共记录了 500 英里( 原来的 300 加上新的)。这是整个测试类。以下各节 单步执行代码的特定部分。

@isTest
private class MileageTrackerTestSuite {

    static testMethod void runPositiveTestCases() {
        
        Double totalMiles = 0;
        final Double maxtotalMiles = 500;
        final Double singletotalMiles = 300;
        final Double u2Miles = 100;
  
        
        //Set up user
        User u1 = [SELECT Id FROM User WHERE Alias='auser'];
        
        //Run As U1
        System.RunAs(u1){

            
        System.debug('Inserting 300  miles... (single record validation)');
        
        Mileage__c testMiles1 = new Mileage__c(Miles__c = 300, Date__c = System.today());
        insert testMiles1;
        
        //Validate single insert
        for(Mileage__c m:[SELECT miles__c FROM Mileage__c 
            WHERE CreatedDate = TODAY
            and CreatedById = :u1.id
            and miles__c != null]) {
                totalMiles += m.miles__c;
            }
        
        Assert.areEqual(singletotalMiles, totalMiles);
    
    
        //Bulk validation   
        totalMiles = 0; 
        System.debug('Inserting 200 mileage records... (bulk validation)');
        
        List<Mileage__c> testMiles2 = new List<Mileage__c>();
        for(integer i=0; i<200; i++) {
            testMiles2.add( new Mileage__c(Miles__c = 1, Date__c = System.today()) );
        }
        insert testMiles2;
       
        for(Mileage__c m:[SELECT miles__c FROM Mileage__c
            WHERE CreatedDate = TODAY
            and CreatedById = :u1.Id
            and miles__c != null]) {
                totalMiles += m.miles__c;
            }
        
        Assert.areEqual(maxtotalMiles, totalMiles);

        }//end RunAs(u1)


       //Validate additional user:
       totalMiles = 0;
       //Setup RunAs
       User u2 = [SELECT Id FROM User WHERE Alias='tuser'];
       System.RunAs(u2){
        
        Mileage__c testMiles3 = new Mileage__c(Miles__c = 100, Date__c = System.today());
        insert testMiles3;
        
                for(Mileage__c m:[SELECT miles__c FROM Mileage__c
            WHERE CreatedDate = TODAY
            and CreatedById = :u2.Id
            and miles__c != null]) {
                totalMiles += m.miles__c;
            }
        //Validate 
        Assert.areEqual(u2Miles, totalMiles);
        
       } //System.RunAs(u2)

      
    } // runPositiveTestCases()
   
    static testMethod void runNegativeTestCases() {

        User u3 = [SELECT Id FROM User WHERE Alias='tuser'];
        System.RunAs(u3) {

           System.debug('Inserting a record with 501 miles... (negative test case)'); 

           Mileage__c testMiles3 = new Mileage__c( Miles__c = 501, Date__c = System.today() );

           try {
               insert testMiles3;
               Assert.fail('DmlException expected');
           } catch (DmlException e) {
               //Assert Status Code
               Assert.areEqual('FIELD_CUSTOM_VALIDATION_EXCEPTION', e.getDmlStatusCode(0));
            
               //Assert field
               Assert.areEqual(Mileage__c.Miles__c, e.getDmlFields(0)[0]);
                        
               //Assert Error Message
               Assert.isTrue(e.getMessage().contains(
                'Mileage request exceeds daily limit(500): [Miles__c]'),
                'DMLException did not contain expected validation message:' + e.getMessage() );
            
             } //catch
           } //RunAs(u3) 
    } // runNegativeTestCases() 
  
    
} // class MileageTrackerTestSuite

阳性测试用例

以下步骤通过上面的代码,特别是, 单条记录和多条记录的阳性测试用例。

  1. 将文本添加到调试日志中,指示代码的下一步:System.debug('Inserting 300 more miles...single record validation');
  2. 创建一个Mileage__c对象并将其插入到数据库中。Mileage__c testMiles1 = new Mileage__c(Miles__c = 300, Date__c = System.today() ); insert testMiles1;
  3. 通过返回插入的记录来验证代码:for(Mileage__c m:[SELECT miles__c FROM Mileage__c WHERE CreatedDate = TODAY and CreatedById = :createdbyId and miles__c != null]) { totalMiles += m.miles__c; }
  4. 使用该方法验证 预期结果是 返回:Assert.areEqualAssert.areEqual(singletotalMiles, totalMiles);
  5. 在进入下一个测试之前,请设置总里程数 返回 0:totalMiles = 0;
  6. 通过创建 200 条记录的批量插入来验证代码。首先,在调试日志中添加文本,指示下一步 法典:System.debug('Inserting 200 Mileage records...bulk validation');
  7. 然后插入 200 条Mileage__c记录:List<Mileage__c> testMiles2 = new List<Mileage__c>(); for(Integer i=0; i<200; i++){ testMiles2.add( new Mileage__c(Miles__c = 1, Date__c = System.today()) ); } insert testMiles2;
  8. 用于验证预期结果 是 返回:Assert.areEqualfor(Mileage__c m:[SELECT miles__c FROM Mileage__c WHERE CreatedDate = TODAY and CreatedById = :CreatedbyId and miles__c != null]) { totalMiles += m.miles__c; } Assert.areEqual(maxtotalMiles, totalMiles);

阴性测试用例

以下步骤通过上面的代码,特别是, 阴性测试用例。

  1. 创建一个名为 :runNegativeTestCasesstatic testMethod void runNegativeTestCases(){
  2. 将文本添加到调试日志中,指示代码的下一步:System.debug('Inserting 501 miles... negative test case');
  3. 创建 501 英里的Mileage__c记录。Mileage__c testMiles3 = new Mileage__c(Miles__c = 501, Date__c = System.today());
  4. 将语句放在 / 块中。这允许您捕获验证异常和 断言生成的错误消息。使用该方法明确断言您期望 验证异常。inserttrycatchAssert.failtry { insert testMiles3; Assert.fail('DmlException expected'); } catch (DmlException e) {
  5. 现在使用 和 方法进行测试。 将以下代码添加到之前的块中 创建:Assert.areEqualAssert.isTruecatch//Assert Status Code Assert.areEqual('FIELD_CUSTOM_VALIDATION_EXCEPTION', e.getDmlStatusCode(0)); //Assert field Assert.areEqual(Mileage__c.Miles__c, e.getDmlFields(0)[0]); //Assert Error Message Assert.isTrue(e.getMessage().contains( 'Mileage request exceeds daily limit(500): [Miles__c]'), 'DMLException did not contain expected validation message:' + e.getMessage() );

以第二用户身份进行测试

以下步骤通过上面的代码,特别是, 以第二个用户身份运行。

  1. 在进入下一个测试之前,请设置总里程数 返回 0:totalMiles = 0;
  2. 设置下一个用户。User u2 = [SELECT Id FROM User WHERE Alias='tuser']; System.RunAs(u2){
  3. 将文本添加到调试日志中,指示代码的下一步:System.debug('Setting up testing - deleting any mileage records for ' + UserInfo.getUserName() + ' from today');
  4. 然后插入一条Mileage__c记录:Mileage__c testMiles3 = new Mileage__c(Miles__c = 100, Date__c = System.today()); insert testMiles3;
  5. 通过返回插入的记录来验证代码:for(Mileage__c m:[SELECT miles__c FROM Mileage__c WHERE CreatedDate = TODAY and CreatedById = :u2.Id and miles__c != null]) { totalMiles += m.miles__c; }
  6. 使用该方法验证 预期结果是 返回:Assert.areEqualAssert.areEqual(u2Miles, totalMiles);

测试和代码覆盖率

Apex 测试框架为您的 Apex 类生成代码覆盖率数字,并且 每次运行一个或多个测试时触发。代码覆盖率指示有多少可执行行 类和触发器中的代码已由测试方法执行。将测试方法写入 测试触发器和类,然后运行这些测试以生成代码覆盖率 信息。

测试方法涵盖的顶点触发器和类

测试方法涵盖的顶点触发器和类

除了确保代码的质量外,单元测试还使您能够满足代码要求 部署或打包 Apex 的覆盖率要求。要部署 Apex 或将其打包为 Salesforce AppExchange,单元测试必须至少覆盖 75% 的 Apex 代码,并且这些测试 必须通过。

代码覆盖率是测试有效性的一个指标,但不能保证测试 有效性。测试的质量也很重要,但您可以使用代码覆盖率作为工具来 评估是否需要添加更多测试。虽然您需要满足最低代码覆盖率 部署或打包 Apex 代码的要求,代码覆盖率不应是唯一的目标 您的测试。测试应断言应用的行为,并确保代码的质量。

代码覆盖率是如何计算的?

代码覆盖率百分比是计算 覆盖线除以覆盖线数和未覆盖线数之和。只 包括可执行代码行。(注释和空行不计算在内。 语句和大括号在以下情况下被排除在外 它们单独出现在一行上。一行上的多个语句计为一行 代码覆盖率的目的。如果语句由多个表达式组成,这些表达式写在 多行,每行都计入代码覆盖率。System.debug()

下面是一个具有一个方法的类的示例。这门课的测试是 run,并在开发人员控制台中为此类选择了显示代码覆盖率的选项。 蓝线表示测试覆盖的线。未突出显示的行 被排除在代码覆盖率计算之外。红线表示未覆盖的线 通过测试。为了实现全面覆盖,需要更多的测试。测试必须使用不同的输入进行调用,并验证返回的 价值。getTaskPriority()

这是测试方法部分涵盖的类。对应的测试类 未显示。

开发者控制台中具有代码覆盖率视图的示例类

测试类(带有 注释的类) 从代码覆盖率计算中排除。此排除项适用于所有测试类 无论它们包含什么 – 用于测试的测试方法或实用方法。@isTest

注意

Apex 编译器有时会优化语句中的表达式。例如,如果多个 字符串常量与运算符 编译器在内部将这些表达式替换为一个字符串常量。如果字符串 串联表达式位于单独的行上,附加行不计为 优化后的代码覆盖率计算。为了说明这一点,字符串变量是 分配给两个连接的字符串常量。第二个字符串常量位于 分开 线。+

String s = 'Hello'
    + ' World!';
String s = 'Hello World!';

检查代码覆盖率

运行测试后,可以在“开发人员”的“测试”选项卡中查看代码覆盖率信息 安慰。代码覆盖率窗格包括每个 Apex 类的覆盖率信息,以及 全面覆盖组织中的所有 Apex 代码。

此外,代码覆盖率存储在两个 Lightning Platform Tooling API 对象中: ApexCodeCoverageAggregate 和 ApexCodeCoverage。ApexCodeCoverageAggregate 存储 在检查测试类的所有测试方法后,为该类覆盖了行。ApexCodeCoverage 存储 每个单独的测试方法覆盖和未覆盖的线。出于这个原因,一个 类在 ApexCodeCoverage 中可以有多个覆盖率结果,每个测试方法一个覆盖率结果 这已经测试了它。可以使用 SOQL 和工具 API 查询这些对象以检索 覆盖范围信息。将 SOQL 查询与工具 API 结合使用是检查代码的另一种方法 覆盖范围和获取更多详细信息的快速方法。

例如,此 SOQL 查询获取类的代码覆盖率。覆盖率是从执行 此类中的方法。TaskUtil

SELECT ApexClassOrTrigger.Name, NumLinesCovered, NumLinesUncovered 
FROM ApexCodeCoverageAggregate 
WHERE ApexClassOrTrigger.Name = 'TaskUtil'

注意

此 SOQL 查询需要工具 API。您可以使用查询编辑器运行此查询 ,然后选中 Use Tooling API

下面是测试部分覆盖的类的示例查询结果:

ApexClassOrTrigger.NameNumLines覆盖NumLines未覆盖
任务实用性82

下一个示例演示如何确定哪些测试方法涵盖了该类。查询 从存储覆盖范围的其他对象 ApexCodeCoverage 获取覆盖率信息 按测试类和方法列出的信息。

SELECT ApexTestClass.Name,TestMethodName,NumLinesCovered,NumLinesUncovered 
FROM ApexCodeCoverage 
WHERE ApexClassOrTrigger.Name = 'TaskUtil'

下面是一个示例查询结果。

ApexTestClass.NameTestMethodName (测试方法名称)NumLines覆盖NumLines未覆盖
TaskUtilTesttestTaskPriority(测试任务优先级)73
TaskUtilTesttestTaskHighPriority(测试任务高优先级)64

ApexCodeCoverage 中的 NumLinesUncovered 值与 聚合结果为 ApexCodeCoverageAggregate,因为它们表示与 测试方法各。例如,测试方法覆盖了整个班级的 7 行,总共 10 行 行,因此未覆盖的行数为 3 行 (10–7)。因为聚合覆盖率存储在 ApexCodeCoverageAggregate 包括所有测试方法的覆盖率,其中 和 的覆盖率被包括在内,只剩下 2 行未被覆盖 任何测试方法。testTaskPriority()testTaskPriority()testTaskPriority()testTaskHighPriority()

代码覆盖率最佳实践

请考虑以下代码覆盖率提示和最佳做法。

代码覆盖率一般提示

  • 运行测试以刷新代码覆盖率。在以下情况下不会刷新代码覆盖率数字 除非重新运行测试,否则将对组织中的 Apex 代码进行更新。
  • 如果组织自上次测试运行以来已更新,则代码覆盖率估计可以 不正确。重新运行 Apex 测试以获得正确的估计值。
  • 组织中的总体代码覆盖率百分比不包括 托管包测试。唯一的例外是托管包测试导致触发器 火灾。有关更多信息,请参见托管包测试。
  • 覆盖率基于组织中的代码行总数。添加或删除 代码行更改覆盖率百分比。例如,假设一个组织有 50 个 测试方法涵盖的代码行。如果添加的触发器包含 50 行代码,则不 通过测试,代码覆盖率从 100% 下降到 50%。触发器增加 组织中的总代码行从 50 行到 100 行,其中只有 50 行由 测试。

为什么沙盒和生产环境之间的代码覆盖率数字不同

当 Apex 部署到生产环境或作为软件包的一部分上传到 Salesforce 时 AppExchange、Salesforce 在目标组织中运行本地测试。沙盒和生产 环境通常不包含相同的数据和元数据,因此代码覆盖率结果不会 始终匹配。如果代码在生产环境中的覆盖率低于 75%,请增加覆盖率以便能够 以部署或上传代码。以下是代码差异的常见原因 开发或沙盒环境与生产环境之间的覆盖率。这 信息可以帮助您排查和协调这些差异。测试失败如果一个环境中的测试结果不同,则总体代码覆盖率百分比 不匹配。在比较沙盒和生产环境之间的代码覆盖率之前,请使 确保要部署或打包的代码的所有测试都在组织中通过 第一。参与代码覆盖率计算的测试必须全部通过 部署或包上传。数据依赖关系如果测试使用批注访问组织数据,则测试结果可以 根据组织中可用的数据而有所不同。如果 测试中引用的记录不存在或已更改,测试失败或 在 Apex 方法中执行不同的代码路径。修改测试,以便 他们创建测试数据,而不是访问组织数据。@IsTest(SeeAllData=true)元数据依赖关系元数据中的更改(例如用户配置文件设置中的更改)可能会导致测试 失败或执行不同的代码路径。确保沙盒和生产环境中的元数据 匹配,或确保元数据更改不是导致不同测试执行的原因 行为。托管包测试在用户界面中运行所有 Apex 测试后计算的代码覆盖率,例如 开发人员控制台,可能与在 部署。如果运行所有测试,包括托管包测试,请在 用户界面,组织中的整体代码覆盖率没有 包括托管包代码的覆盖率。虽然托管包测试 覆盖托管包中的代码行,此覆盖率不是 组织的代码覆盖率计算为总行数和覆盖行数。 相比之下,在运行所有 测试通过测试 级别包括托管包代码的覆盖率。如果您运行的是托管 通过测试级别在部署中打包测试,建议运行此 首先在沙盒中部署或执行验证部署以验证 代码覆盖率。RunAllTestsInOrgRunAllTestsInOrg部署导致整体覆盖率低于 75%将具有 100% 覆盖率的新组件部署到生产环境时,部署失败 如果新代码和现有代码之间的平均覆盖率未达到 75% 的阈值。如果 在目标组织中测试运行返回的覆盖率结果小于 75%,修改 现有的测试方法或编写额外的测试方法以提高代码覆盖率 75%.单独部署修改后的或新的测试方法,或使用具有 100% 的新代码部署 覆盖。生产环境中的代码覆盖率降至 75% 以下有时,生产中的整体覆盖率会下降到75%以下,即使至少是 当组件从沙盒部署时,为 75%。依赖于 组织的数据和元数据可能会导致代码覆盖率下降。如果数据和元数据 已经进行了充分的更改以改变相关测试方法的结果,某些方法可能会失败 或行为不同。在这种情况下,某些行将不再被覆盖。

为生产环境匹配代码覆盖率数字的推荐过程

  • 使用完整沙盒作为生产部署的暂存沙盒环境。一个完整的 沙盒模拟生产中的元数据和数据,并有助于减少代码差异 两个环境之间的覆盖数。
  • 若要减少对沙盒和生产组织中数据的依赖性,请在 您的 Apex 测试。
  • 如果由于代码覆盖率不足而导致部署到生产环境失败,请编写更多测试 将整体代码覆盖率提高到尽可能高的覆盖率或 100%。重试 部署。
  • 如果在沙盒中提高代码覆盖率后部署到生产环境失败, 从生产组织运行本地测试。确定低于 75% 的类 覆盖。在沙盒中为这些类编写其他测试以提高代码覆盖率。

使用 Stub API 构建模拟框架

Apex 提供了一个用于实现模拟框架的存根 API。模拟框架 有很多好处。它可以简化和改进测试,并帮助您更快、更快速地创建更多 可靠的测试。您可以使用它来单独测试类,这对单元很重要 测试。使用存根 API 构建模拟框架也是有益的,因为 存根对象在运行时生成。由于这些对象是动态生成的,因此 不必打包和部署测试类。您可以构建自己的模拟框架,或者 您可以使用其他人构建的。

您可以定义存根对象的行为,这些对象在运行时创建为匿名对象 Apex 类的子类。存根 API 由接口和方法组成。System.StubProviderSystem.Test.createStub()

注意

此功能适用于高级 Apex 开发人员。使用它需要彻底 了解单元测试和模拟框架。如果你认为嘲笑 框架是取笑你的东西,你可能想多做一点 在进一步阅读之前进行研究。

让我们看一个示例来说明存根 API 的工作原理。此示例不是故意的 来演示模拟框架的广泛可能用途。它 有意简单专注于使用 Apex 存根 API 的机制。

假设我们想在下面的类中测试格式化方法。

public class DateFormatter {    
    // Method to test    
    public String getFormattedDate(DateHelper helper) {
        return 'Today\'s date is ' + helper.getTodaysDate();
    }
}

通常,当我们调用此方法时,我们会传入一个帮助程序类,该类具有一个方法 返回今天的日期。

public class DateHelper {   
    // Method to stub    
    public String getTodaysDate() {
        return Date.today().format();
    }
}

下面的代码调用该方法。

DateFormatter df = new DateFormatter();
DateHelper dh = new DateHelper();
String dateStr = df.getFormattedDate(dh);

为了进行测试,我们希望隔离该方法以确保格式有效 适当地。该方法的返回值通常因日期而异。然而,在这个 情况下,我们希望返回一个恒定的、可预测的值,以将我们的测试隔离到 格式。而不是编写类的“假”版本,该方法返回 一个常量值,我们创建类的存根版本。创建存根对象 在运行时动态,我们可以指定其方法的“存根”行为。getFormattedDate()getTodaysDate()要使用 Apex 类的存根版本,请执行以下操作:

  1. 通过实现接口来定义存根类的行为。System.StubProvider
  2. 使用该方法实例化存根对象。System.Test.createStub()
  3. 从测试类中调用存根对象的相关方法。

实现 StubProvider 接口

下面是接口的实现。StubProvider

@isTest
public class MockProvider implements System.StubProvider {
    
    public Object handleMethodCall(Object stubbedObject, String stubbedMethodName, 
        Type returnType, List<Type> listOfParamTypes, List<String> listOfParamNames, 
        List<Object> listOfArgs) {
        
        // The following debug statements show an example of logging 
        // the invocation of a mocked method.
       
        // You can use the method name and return type to determine which method was called.
        System.debug('Name of stubbed method: ' + stubbedMethodName);
        System.debug('Return type of stubbed method: ' + returnType.getName());
        
        // You can also use the parameter names and types to determine which method 
        // was called.
        for (integer i =0; i < listOfParamNames.size(); i++) {
            System.debug('parameter name: ' + listOfParamNames.get(i));
            System.debug('  parameter type: ' + listOfParamTypes.get(i).getName());
        }
        
        // This shows the actual parameter values passed into the stubbed method at runtime.
        System.debug('number of parameters passed into the mocked call: ' + 
            listOfArgs.size());
        System.debug('parameter(s) sent into the mocked call: ' + listOfArgs);
        
        // This is a very simple mock provider that returns a hard-coded value 
        // based on the return type of the invoked.
        if (returnType.getName() == 'String')
            return '8/8/2016';
        else 
            return null;
    }
}

StubProvider是一个回调接口。它 指定需要实现的单个方法:。当调用存根方法时,将调用。您可以定义 此方法中存根类的行为。该方法具有以下特点 参数。handleMethodCall()handleMethodCall()

  • stubbedObject:存根对象
  • stubbedMethodName:调用方法的名称
  • returnType:被调用方法的返回类型
  • listOfParamTypes:参数类型的列表 invoked 方法
  • listOfParamNames:参数名称的列表 invoked 方法
  • listOfArgs:传递到此参数的实际参数值 方法在运行时

您可以使用这些参数来确定调用了类的哪个方法,以及 然后,您可以定义每个方法的行为。在这种情况下,我们检查退货 用于标识它并返回硬编码值的方法的类型。

实例化类的存根版本

下一步是实例化该类的存根版本。以下实用程序 类返回一个可用作模拟的存根对象。

public class MockUtil {
    private MockUtil(){}

    public static MockProvider getInstance() {
        return new MockProvider();
    }
    
     public static Object createMock(Type typeToMock) {
        // Invoke the stub API and pass it our mock provider to create a 
        // mock class of typeToMock.
        return Test.createStub(typeToMock, MockUtil.getInstance());
    }
}

该类包含方法, 调用该方法。 该方法采用 Apex 类 类型和我们之前创建的接口的实例。它返回一个存根对象,我们可以在 测试。createMock()Test.createStub()createStub()StubProvider

调用 Stub 方法

最后,我们从测试中调用存根类的相关方法 类。

@isTest 
public class DateFormatterTest {   
    @isTest 
    public static void testGetFormattedDate() {
        // Create a mock version of the DateHelper class.
        DateHelper mockDH = (DateHelper)MockUtil.createMock(DateHelper.class);
        DateFormatter df = new DateFormatter();
        
        // Use the mocked object in the test.
        System.assertEquals('Today\'s date is 8/8/2016', df.getFormattedDate(mockDH));
    }
}

在这个测试中,我们调用 创建类的存根版本。然后,我们可以在存根对象上调用该方法,该方法返回我们的 硬编码日期。使用硬编码日期允许我们在 隔离。createMock()DateHelpergetTodaysDate()getFormattedDate()

Apex 存根 API 限制

使用 Apex 存根 API 时,请记住以下限制。

  • 被模拟的对象必须与对方法的调用位于同一命名空间中。但是, 接口的实现可以在另一个命名空间中。Test.createStub()StubProvider
  • 您无法模拟以下 Apex 元素。
    • 静态方法(包括将来的方法)
    • 私有方法
    • 属性(getter 和 setter)
    • 触发器
    • 内部类
    • 系统类型
    • 实现接口的类Batchable
    • 只有私有构造函数的类
  • 迭代器不能用作返回类型或参数类型。

Apex 中的异常

异常会记录中断正常代码流的错误和其他事件 执行。 语句用于 生成异常,而 、 和 语句用于从异常中正常恢复。throwtrycatchfinally

有许多方法可以处理代码中的错误,包括使用断言(如调用)或返回错误 代码或布尔值,那么为什么要使用异常呢?使用异常的优点是 它们简化了错误处理。异常从被调用的方法冒泡到 caller,根据需要设置多个级别,直到找到处理错误的语句。这种冒泡缓解了 您不必在每个方法中编写错误处理代码。此外,通过使用语句,您可以在一个位置进行恢复 从异常,例如重置变量和删除数据。System.assertcatchfinally

发生异常时会发生什么情况?

发生异常时,代码执行将停止。任何 在异常之前处理的 DML 操作将回滚,并且 未提交到数据库。异常记录在调试日志中。为 Salesforce 发送的未处理异常(代码未捕获的异常) 包含异常信息的电子邮件。最终用户看到错误 Salesforce 用户界面中的消息。

未经处理的异常电子邮件

当发生未经处理的 Apex 异常时,将发送电子邮件 包括 Apex 堆栈跟踪、异常消息以及客户的组织和用户 ID。报表不会返回其他数据。默认情况下,未处理的异常电子邮件会发送给指定的开发人员 在失败类的 LastModifiedBy 字段中,或者 触发。此外,您还可以将电子邮件发送给 Salesforce 的用户 组织和任意电子邮件地址。这些电子邮件收件人还可以 接收流程或流程错误电子邮件。要设置这些电子邮件通知, 从“设置”中,输入“快速查找”框,然后选择“Apex” 例外电子邮件。然后,输入的电子邮件地址将应用于 客户组织中的所有托管软件包。您还可以配置 Apex 使用 Tooling API 对象的异常电子邮件 Apex电子邮件通知。Apex Exception Email

注意

  • 如果同步运行的 Apex 代码中出现重复异常,或者 异步地,后续异常电子邮件将被禁止,并且只有 发送第一封电子邮件。此电子邮件抑制可防止泛滥 开发人员的收件箱中包含有关相同错误的电子邮件。
  • 对于匿名 Apex 遇到的异常,不会发送电子邮件 执行。
  • 每个应用程序每小时限制为 10 封电子邮件 Apex 例外电子邮件 服务器。由于此限制不是基于每个组织,因此电子邮件传递到 特定的组织可能不可靠。

用户界面中未处理的异常

如果最终用户在使用 标准用户界面,则显示错误消息。错误消息包含文本 与此处显示的通知类似。

“新商品”页面中未处理的异常
  • 异常语句
  • 异常处理示例
  • 内置异常和常用方法
  • 捕获不同的异常类型
  • 创建自定义例外

异常语句

Apex 使用异常来记录错误和其他事件 扰乱代码执行的正常流程。 语句可用于生成异常,而 、 和 可以 用于从异常中正常恢复。throwtrycatchfinally

抛出语句

语句允许您发出信号 发生错误。若要引发异常,请使用该语句并为其提供 一个 Exception 对象,用于提供有关特定错误的信息。 例如:

throwthrow

throw exceptionObject;

尝试-捕捉-最后 语句

、 和 语句可用于从引发的异常中正常恢复:

trycatchfinally

  • 该语句标识一个 可能发生异常的代码块。try
  • 该语句标识一个 可以处理特定类型异常的代码块。单个语句可以有零或 更多相关语句。 每个语句必须具有 唯一的异常类型。此外,一旦捕获到特定的异常类型 一个区块,其余区块(如果有的话), 不执行。catchtrycatchcatchcatchcatch
  • 该语句标识一个 保证执行的代码块,并允许您清理您的 法典。单个语句可以 最多有一个关联的语句。块中的代码 无论是否引发异常或类型如何,始终执行 引发的异常。由于该块始终执行,因此请将其用于清理代码,例如 至于释放资源。finallytryfinallyfinallyfinally

语法

、 和 语句的语法 如下所示。trycatchfinally

try {
  // Try block
 code_block
} catch (exceptionType variableName) {
  // Initial catch block.
  // At least the catch block or the finally block must be present. 
 code_block
} catch (Exception e) {
  // Optional additional catch statement for other exception types.
  // Note that the general exception type, 'Exception',
  // must be the last catch block when it is used.
 code_block
} finally {
  // Finally block.
  // At least the catch block or the finally block must be present.
 code_block
}

至少一个块或一个块必须与块一起存在。以下是 try-catch 块。catchfinallytry

try {
 code_block
} catch (exceptionType variableName) {
 code_block
}
// Optional additional catch blocks

以下是 try-finally 块的语法。

try {
 code_block
} finally {
 code_block
}

这是 try-catch-finally 的骨架示例 块。

try {
    // Perform some operation that 
    //   might cause an exception.
} catch(Exception e) {
    // Generic exception handling code here.
} finally {
    // Perform some clean up.
}

无法捕获的异常

某些特殊类型的内置异常无法捕获。这些例外与 闪电平台中的危急情况。这些情况需要堕胎 的代码执行,并且不允许通过异常恢复执行 处理。其中一个异常是限制异常 (),如果调控器限制具有 已超过,例如,当发出的最大 SOQL 查询数 超过。其他示例是断言语句失败时引发的异常 (通过方法)或许可证 异常。System.LimitExceptionSystem.assert

当异常无法捕获时,块以及块(如果有)不会捕获 执行。catchfinally

版本化行为更改

在 API 版本 41.0 及更高版本中,代码中的 unreachable 语句将导致 编译错误。例如,以下代码块生成编译时 API 版本 41.0 及更高版本中的错误。永远无法达到第三种说法 因为前面的语句抛出了一个无条件的 例外。

Boolean x = true; 
throw new NullPointerException();
x = false;

异常处理示例

若要查看操作中的异常,请执行一些导致引发 DML 异常的代码。 在 Developer 中执行以下命令 安慰:

Merchandise__c m = new Merchandise__c();
insert m;

DML 语句 在示例中导致 DmlException,因为我们插入 未设置任何必填字段的商品项。这 是您在调试日志中看到的异常错误。insert

System.DmlException: Insert failed. First exception on row 0; first error: REQUIRED_FIELD_MISSING, Required fields are missing: [Description, Price, Total Inventory]: [Description, Price, Total Inventory]接下来,在开发者控制台中执行此代码段。它基于前面的示例,但 包括一个 try-catch 块。

try {
    Merchandise__c m = new Merchandise__c();
    insert m;
} catch(DmlException e) {
    System.debug('The following exception has occurred: ' + e.getMessage());
}

请注意,开发者控制台中的请求状态现在报告成功。这是因为 代码处理异常。

在异常之后发生的 try 块中的任何语句都是 跳过,不执行。例如,如果添加语句 之后,此语句 不会被执行。执行以下命令:insert m;

try {
    Merchandise__c m = new Merchandise__c();
    insert m;
    // This doesn't execute since insert causes an exception
    System.debug('Statement after insert.');
} catch(DmlException e) {
    System.debug('The following exception has occurred: ' + e.getMessage());
}

在新的调试日志条目中,请注意,您没有看到 的调试消息。这是因为此调试语句发生在异常之后 由插入引起,并且永远不会被执行。继续执行 的代码语句,将语句放在 在 try-catch 块之后。执行这个修改后的代码片段,然后 请注意,调试日志现在具有 的调试消息。Statement after insertStatement after insert

try {
    Merchandise__c m = new Merchandise__c();
    insert m;
} catch(DmlException e) {
    System.debug('The following exception has occurred: ' + e.getMessage());
}
// This will get executed
System.debug('Statement after insert.');

或者,您可以包含其他 try-catch 块。这 代码片段在第二个 try-catch 块中具有该语句。执行它以查看 你得到和以前一样的结果。System.debug

try {
    Merchandise__c m = new Merchandise__c();
    insert m;
} catch(DmlException e) {
    System.debug('The following exception has occurred: ' + e.getMessage());
}

try {
    System.debug('Statement after insert.');
    // Insert other records
}
catch (Exception e) {
    // Handle this exception here
}

无论出现什么异常,finally 块始终执行 被抛出,即使没有抛出异常。让我们来看看吧 在行动中使用。执行以下命令:

// Declare the variable outside the try-catch block
// so that it will be in scope for all blocks.
XmlStreamWriter w = null;
try {
    w = new XmlStreamWriter();
    w.writeStartDocument(null, '1.0');
    w.writeStartElement(null, 'book', null);
    w.writeCharacters('This is my book');
    w.writeEndElement(); 
    w.writeEndDocument();

    // Perform some other operations
    String s;
    // This causes an exception because
    // the string hasn't been assigned a value.
    Integer i = s.length();
} catch(Exception e) {
    System.debug('An exception occurred: ' + e.getMessage());
} finally {
    // This gets executed after the exception is handled
    System.debug('Closing the stream writer in the finally block.');
    // Close the stream writer
    w.close();
}

前面的代码片段创建一个 XML 流编写器,并将 一些 XML 元素。接下来,由于访问 null 字符串变量。这 catch 块处理此异常。然后执行 finally 块。 它写入调试消息并关闭流编写器,从而释放 任何关联的资源。检查调试日志中的调试输出。 异常后,你将看到调试消息 错误。这告诉您,在异常之后执行的 finally 块 被抓住了。sClosing the stream writer in the finally block.

内置异常和常用方法

Apex 提供了许多内置的异常类型,运行时引擎会在以下情况下抛出这些异常类型 在执行过程中遇到错误。你已在前面的 例。下面是一些其他内置异常的示例。有关完整列表 内置异常类型,请参阅 Exception 类和内置异常。DmlExceptionDML 语句的任何问题,例如语句在记录中缺少必填字段。insert此示例使用 DmlException。此示例中的 DML 语句会导致 DmlException,因为 它插入商品项目而不设置任何所需的内容 领域。此异常在块中捕获,异常消息将写入调试日志 使用语句。insertcatchSystem.debug

try {
    Merchandise__c m = new Merchandise__c();
    insert m;
} catch(DmlException e) {
    System.debug('The following exception has occurred: ' + e.getMessage());
}

列表异常列表的任何问题,例如尝试访问超出 边界。此示例创建一个列表并向其添加一个元素。然后,进行尝试 访问两个元素,一个位于索引 0(存在),另一个位于索引 1(其中 导致引发 ListException,因为此索引处不存在任何元素。 此异常在 catch 块中被捕获。catch 块中的语句将以下内容写入 调试日志:。System.debugThe following exception has occurred: List index out of bounds: 1

try {
    List<Integer> li = new List<Integer>();
    li.add(15);
    // This list contains only one element,
    // but we're attempting to access the second element
    // from this zero-based list.
    Integer i1 = li[0]; 
    Integer i2 = li[1]; // Causes a ListException
} catch(ListException le) {
    System.debug('The following exception has occurred: ' + le.getMessage());
}

NullPointerException取消引用变量的任何问题。null此示例创建一个名为 String 的变量,但我们不将其初始化为值,因此它是 null。 在我们的 null 上调用该方法 变量导致 NullPointerException。例外情况在我们的捕获中被发现 块,这是写入调试日志的内容:。scontainsThe following exception has occurred: Attempt to de-reference a null object

try {
    String s;
    Boolean b = s.contains('abc'); // Causes a NullPointerException
} catch(NullPointerException npe) {
    System.debug('The following exception has occurred: ' + npe.getMessage());
}

查询异常SOQL 查询的任何问题,例如分配不返回任何记录的查询 或多条记录到一个单一实例 sObject 变量。此示例中的第二个 SOQL 查询会导致 QueryException。示例 将 Merchandise 对象分配给从查询返回的内容。请注意在查询中使用。这确保了 从数据库中最多返回一个对象,因此我们可以将其分配给 单个对象,而不是列表。但是,在这种情况下,我们没有 商品名为 XYZ,因此不会返回任何内容,并且尝试将 单个对象的返回值会导致 QueryException。例外情况是 捕获到我们的 catch 块中,这就是您将在调试日志中看到的内容:.LIMIT 1The following exception has occurred: List has no rows for assignment to SObject

try {
    // This statement doesn't cause an exception, even though 
    // we don't have a merchandise with name='XYZ'.
    // The list will just be empty.
    List<Merchandise__c> lm = [SELECT Name FROM Merchandise__c WHERE Name = 'XYZ'];
    // lm.size() is 0 
    System.debug(lm.size());
    
    // However, this statement causes a QueryException because 
    // we're assiging the return value to a Merchandise__c object
    // but no Merchandise is returned.
    Merchandise__c m = [SELECT Name FROM Merchandise__c WHERE Name = 'XYZ' LIMIT 1];
} catch(QueryException qe) {
    System.debug('The following exception has occurred: ' + qe.getMessage());    
}

SObjectExceptionsObject 记录的任何问题,例如尝试更改语句中的字段,该字段只能是 期间更改。updateinsert此示例在 try 块中生成 SObjectException,该块在 catch 块。该示例查询发票对帐单,并仅选择其 名称字段。然后,它尝试获取查询的 Description__c 字段 sObject,它不可用,因为它不在 在 SELECT 语句中查询的字段。这会导致 SObjectException。 这个异常在我们的 catch 块中被捕获,这就是你将看到的 在调试日志中:。The following exception has occurred: SObject row was retrieved via SOQL without querying the requested field: Invoice_Statement__c.Description__c

try {
    Invoice_Statement__c inv = new Invoice_Statement__c(
        Description__c='New Invoice');
    insert inv;

    // Query the invoice we just inserted
    Invoice_Statement__c v = [SELECT Name FROM Invoice_Statement__c WHERE Id = :inv.Id];
    // Causes an SObjectException because we didn't retrieve
    // the Description__c field.
    String s = v.Description__c;
} catch(SObjectException se) {
    System.debug('The following exception has occurred: ' + se.getMessage());
}

常见异常方法

您可以使用常见的异常方法来获取有关异常的详细信息,例如 作为异常错误消息或堆栈跟踪。前面的示例调用该方法,该方法返回 与异常关联的错误消息。还有其他异常方法 也可用。以下是一些有用方法的说明:

getMessage

  • getCause:返回 exception 作为异常对象。
  • getLineNumber:返回该行 引发异常的编号。
  • getMessage:返回错误 为用户显示的消息。
  • getStackTraceString:返回 以字符串形式引发异常的堆栈跟踪。
  • getTypeName:返回 异常,例如 DmlException、ListException、MathException 等 上。

若要了解一些常见方法返回的内容,请尝试运行此示例。

try {
    Merchandise__c m = [SELECT Name FROM Merchandise__c LIMIT 1];
    // Causes an SObjectException because we didn't retrieve
    // the Total_Inventory__c field.
    Double inventory = m.Total_Inventory__c;
} catch(Exception e) {
    System.debug('Exception type caught: ' + e.getTypeName());    
    System.debug('Message: ' + e.getMessage());    
    System.debug('Cause: ' + e.getCause());    // returns null
    System.debug('Line number: ' + e.getLineNumber());    
    System.debug('Stack trace: ' + e.getStackTraceString());    
}

所有语句的输出 如下所示:System.debug

17:38:04:149 USER_DEBUG [7]|DEBUG|Exception type caught: System.SObjectException

17:38:04:149 USER_DEBUG [8]|DEBUG|Message: SObject row was retrieved via SOQL without querying the requested field: Merchandise__c.Total_Inventory__c

17:38:04:150 USER_DEBUG [9]|DEBUG|Cause: null

17:38:04:150 USER_DEBUG [10]|DEBUG|Line number: 5

17:38:04:150 USER_DEBUG [11]|DEBUG|Stack trace: AnonymousBlock: line 5, column 1

catch 语句参数类型是泛型 Exception 类型。它抓住了更多 特定的 SObjectException。您可以通过检查退货来验证是否如此 的值 输出。输出还包含 SObjectException 的其他属性,如 错误消息、发生异常的行号和堆栈跟踪。 您可能想知道为什么返回 null。这是因为在我们的样本中,之前没有异常(内部 exception),导致此异常。在 Create Custom Exceptions 中,您将看到一个示例,其中 的返回值是实际值 例外。e.getTypeName()getCausegetCause

更多异常方法

某些异常类型(如 DmlException)具有特定的异常方法,这些方法 仅适用于它们,不适用于其他异常类型:

  • getDmlFieldNames(Index of the failed record):返回导致错误的字段的名称 指定的失败记录。
  • getDmlId(Index of the failed record): 返回导致指定错误的失败记录的 ID 失败的记录。
  • getDmlMessage(Index of the failed record):返回指定失败的错误消息 记录。
  • getNumDml:返回失败的次数 记录。

此代码片段利用 DmlException 方法获取有关 插入 Merchandise 对象列表时返回的异常。项目清单 要插入包含三个项目,其中最后两个不是必需的 字段和原因 异常。

Merchandise__c m1 = new Merchandise__c(
    Name='Coffeemaker',
    Description__c='Kitchenware',
    Price__c=25,
    Total_Inventory__c=1000);
// Missing the Price and Total_Inventory fields
Merchandise__c m2 = new Merchandise__c(
    Name='Coffeemaker B',
    Description__c='Kitchenware');
// Missing all required fields
Merchandise__c m3 = new Merchandise__c();
Merchandise__c[] mList = new List<Merchandise__c>();
mList.add(m1);
mList.add(m2);
mList.add(m3);

try {
    insert mList;
} catch (DmlException de) {
    Integer numErrors = de.getNumDml();
    System.debug('getNumDml=' + numErrors);
    for(Integer i=0;i<numErrors;i++) {
        System.debug('getDmlFieldNames=' + de.getDmlFieldNames(i));
        System.debug('getDmlMessage=' + de.getDmlMessage(i));  
    }
}

请注意,上面的示例在尝试中没有包含所有初始代码 块。只有可能生成异常的代码部分才会被包装 在块内,在这种情况下,该语句可能会在 如果输入数据无效。操作导致的异常由其后面的块捕获。执行后 此示例中,你将看到类似于以下内容的语句输出:tryinsertinsertcatchSystem.debug

14:01:24:939 USER_DEBUG [20]|DEBUG|getNumDml=2

14:01:24:941 USER_DEBUG [23]|DEBUG|getDmlFieldNames=(Price, Total Inventory)

14:01:24:941 USER_DEBUG [24]|DEBUG|getDmlMessage=Required fields are missing: [Price, Total Inventory]

14:01:24:942 USER_DEBUG [23]|DEBUG|getDmlFieldNames=(Description, Price, Total Inventory)

14:01:24:942 USER_DEBUG [24]|DEBUG|getDmlMessage=Required fields are missing: [Description, Price, Total Inventory]

DML 失败数正确报告为 2,因为我们列表中有两个项目 插入失败。此外,导致失败的字段名称和错误消息 对于每个失败的记录,将写入输出。

捕获不同的异常类型

在前面的示例中,我们使用了 catch 块。我们也可以捕获通用异常 键入所有示例,这将捕获所有异常类型。例如 尝试运行此示例,该示例会引发 SObjectException 并具有 catch 语句,参数类型为 Exception。The SObjectException 被 catch 块卡住。

try {
    Merchandise__c m = [SELECT Name FROM Merchandise__c LIMIT 1];
    // Causes an SObjectException because we didn't retrieve
    // the Total_Inventory__c field.
    Double inventory = m.Total_Inventory__c;
} catch(Exception e) {
    System.debug('The following exception has occurred: ' + e.getMessage());    
}

或者,您可以有多个 catch 块 – 一个 catch 块,以及捕获的最终 catch 块 泛型 Exception 类型。请看这个例子。请注意,它有 三个捕获块。

try {
    Merchandise__c m = [SELECT Name FROM Merchandise__c LIMIT 1];
    // Causes an SObjectException because we didn't retrieve
    // the Total_Inventory__c field.
    Double inventory = m.Total_Inventory__c;
} catch(DmlException e) {
    System.debug('DmlException caught: ' + e.getMessage());    
} catch(SObjectException e) {
    System.debug('SObjectException caught: ' + e.getMessage());    
} catch(Exception e) {
    System.debug('Exception caught: ' + e.getMessage());    
}

请记住,只执行一个 catch 块,其余的 catch 块 那些被绕过。此示例与上一个示例类似,但 它有更多的捕获块。运行此代码段时,一个 SObjectException 在以下行上抛出:。每个捕获块 按指定的顺序进行检查,以查找抛出的匹配项 exception 和 catch 块参数中指定的异常类型:

Double inventory = m.Total_Inventory__c;

  1. 第一个 catch 块参数的类型为 DmlException,其类型为 与引发的异常 (SObjectException) 不匹配。
  2. 第二个 catch 块参数的类型为 SObjectException,其 与我们的异常匹配,因此此块被执行并执行以下 消息将写入调试日志:。SObjectException caught: SObject row was retrieved via SOQL without querying the requested field: Merchandise__c.Total_Inventory__c
  3. 最后一个 catch 块被忽略,因为一个 catch 块已经 执行。

最后一个 catch 块很方便,因为它可以捕获任何异常 类型,从而捕获上一个未捕获的任何异常 catch 块。假设我们修改了上面的代码以导致 NullPointerException 要抛出,此异常将在最后一个 catch 块中捕获。 执行此修改后的示例。你将看到以下调试You’ll see the following debug 消息:。Exception caught: Attempt to de-reference a null object

try {
    String s;
    Boolean b = s.contains('abc'); // Causes a NullPointerException
} catch(DmlException e) {
    System.debug('DmlException caught: ' + e.getMessage());    
} catch(SObjectException e) {
    System.debug('SObjectException caught: ' + e.getMessage());    
} catch(Exception e) {
    System.debug('Exception caught: ' + e.getMessage());    
}

创建自定义例外

您不能抛出内置的 Apex 异常。你只能抓住他们。但是有定制 异常,您可以在方法中抛出和捕获它们。自定义异常使您能够 指定详细的错误消息,并在捕获中具有更多自定义错误处理 块。

异常可以是顶级类,也就是说,它们可以具有成员变量、方法和 构造函数,它们可以实现接口,等等。

若要创建自定义异常类,请扩展内置类,并确保类名以单词结尾,例如“MyException”或 “PurchaseException”。所有异常类都扩展了系统定义的基类,因此继承了所有公共类 异常方法。ExceptionExceptionException此示例定义了一个名为 的自定义异常。

MyException

public class MyException extends Exception {}

与 Java 类一样,用户定义的异常类型可以形成继承树,并捕获 块可以捕获此继承树中的任何对象。例如:

public class ExceptionExample {
    public virtual class BaseException extends Exception {}
    public class OtherException extends BaseException {}

    public static void testExtendedException() {
        try {
            Integer i=0;
            // Your code here
            if (i < 5) throw new OtherException('This is bad');
        } catch (BaseException e) {  
            // This catches the OtherException
            System.debug(e.getMessage());
        }  
    }
}

您可以通过以下几种方式创建异常对象,然后可以抛出这些对象。您可以构造异常:

  • 没有 参数:new MyException();
  • 使用指定错误的单个 String 参数 消息:new MyException('This is bad');
  • 使用单个 Exception 参数,该参数指定原因并显示在 任何堆栈 跟踪:new MyException(e);
  • 同时显示 String 错误消息和链式异常原因 任何堆栈 跟踪:new MyException('This is bad', e);

重新引发异常和内部异常

在 catch 块中捕获异常后,您可以选择重新抛出 捕获异常变量。如果方法被另一个方法调用,这将非常有用 并且您希望将异常的处理委托给调用方方法。您可以 将捕获的异常作为内部异常重新抛出,并具有 main 方法捕获自定义异常类型。

下面的示例演示如何将异常重新引发为内部异常。这 example 定义了两个自定义异常和 ,并生成一个堆栈跟踪,其中包含有关两者的信息。My1ExceptionMy2Exception

// Define two custom exceptions
public class My1Exception extends Exception {} 
public class My2Exception extends Exception {} 

try { 
    // Throw first exception
    throw new My1Exception('First exception'); 
} catch (My1Exception e) { 
    // Throw second exception with the first 
    // exception variable as the inner exception
    throw new My2Exception('Thrown with inner exception', e);
}

这是运行上述代码后生成的堆栈跟踪的样子:

15:52:21:073 EXCEPTION_THROWN [7]|My1Exception: First exception

15:52:21:077 EXCEPTION_THROWN [11]|My2Exception: Throw with inner exception

15:52:21:000 FATAL_ERROR AnonymousBlock: line 11, column 1

15:52:21:000 FATAL_ERROR Caused by

15:52:21:000 FATAL_ERROR AnonymousBlock: line 7, column 1

下一节中的示例演示如何使用 异常。getCause

内部异常示例

现在,您已经了解了如何创建自定义异常类以及如何构造 您的异常对象,让我们创建并运行一个示例来演示 自定义异常的有用性。

  1. 在开发者控制台中,创建一个名为 内容。MerchandiseExceptionpublic class MerchandiseException extends Exception { }你会 在创建的第二个类中使用此异常类。大括号 最后将异常类的主体括起来,我们将其留空,因为 我们得到了一些免费代码——我们的类继承了所有的构造函数和公共 异常方法,例如 来自内置类。getMessageException
  2. 接下来,创建名为 .MerchandiseUtilitypublic class MerchandiseUtility { public static void mainProcessing() { try { insertMerchandise(); } catch(MerchandiseException me) { System.debug('Message: ' + me.getMessage()); System.debug('Cause: ' + me.getCause()); System.debug('Line number: ' + me.getLineNumber()); System.debug('Stack trace: ' + me.getStackTraceString()); } } public static void insertMerchandise() { try { // Insert merchandise without required fields Merchandise__c m = new Merchandise__c(); insert m; } catch(DmlException e) { // Something happened that prevents the insertion // of Employee custom objects, so throw a more // specific exception. throw new MerchandiseException( 'Merchandise item could not be inserted.', e); } } }这 class 包含调用 . 后者通过插入不需要的商品来导致异常 领域。catch 块捕获此异常并抛出一个新异常,即 您之前创建的自定义 MerchandiseException。请注意,我们调用了 接受两个参数的异常的构造函数:错误消息和 原始异常对象。您可能想知道为什么我们要传递原始版本 例外?因为它是有用的信息 – 当 MerchandiseException 在第一种方法中捕获,原始异常(称为内部 exception) 确实是导致此异常的原因,因为它发生在 MerchandiseException。mainProcessinginsertMerchandisemainProcessing
  3. 现在让我们看看所有这些的实际效果,以便更好地理解。执行 以后:MerchandiseUtility.mainProcessing();
  4. 检查调试日志输出。您应该看到类似于 以后:18:12:34:928 USER_DEBUG [6]|DEBUG|Message: Merchandise item could not be inserted.18:12:34:929 USER_DEBUG [7]|DEBUG|Cause: System.DmlException: Insert failed. First exception on row 0; first error: REQUIRED_FIELD_MISSING, Required fields are missing: [Description, Price, Total Inventory]: [Description, Price, Total Inventory]18:12:34:929 USER_DEBUG [8]|DEBUG|Line number: 2218:12:34:930 USER_DEBUG [9]|DEBUG|Stack trace: Class.EmployeeUtilityClass.insertMerchandise: line 22, column 1一些感兴趣的项目:
    • MerchandiseException 的原因是 DmlException。您可以看到 DmlException 消息还指出必填字段是 失踪。
    • 堆栈跟踪是第 22 行,这是第二次出现异常 扔。它对应于 throw 语句 MerchandiseException。throw new MerchandiseException('Merchandise item could not be inserted.', e);

调试、测试和部署 Apex

在沙盒中开发 Apex 代码,并使用开发人员控制台对其进行调试,然后进行调试 原木。对代码进行单元测试,然后使用包将其分发给客户。

  • 调试Apex
    Apex 提供调试支持。您可以使用开发人员控制台和调试日志来调试 Apex 代码。
  • 测试 Apex Apex
    提供了一个测试框架,允许您编写单元测试、运行测试、检查测试结果以及获得代码覆盖率结果。
  • 部署 Apex
    您无法在 Salesforce 生产组织中开发 Apex。您的开发工作是在沙盒或 Developer Edition 组织中完成的。
  • 使用托管软件包
    分发 Apex 作为 ISV 或 Salesforce 合作伙伴,您可以使用软件包将 Apex 代码分发给客户组织。在这里,我们将介绍包和包版本控制。

调试Apex

Apex 提供调试支持。您可以使用开发人员调试 Apex 代码 控制台和调试日志。

为了帮助在代码中进行调试,Apex 支持异常语句和自定义异常。 此外,Apex 还会向开发人员发送电子邮件,以解决未处理的异常。

  1. 调试日志
  2. Apex 中的异常

调试日志

调试日志可以记录数据库操作、系统进程和以下情况下发生的错误 执行事务或运行单元测试。调试日志可以包含以下信息:

  • 数据库更改
  • HTTP 标注
  • 顶点错误
  • Apex 使用的资源
  • 自动化工作流程,例如:
    • 工作流规则
    • 分配规则
    • 审批流程
    • 验证规则
    注意调试日志不包括来自基于时间的触发的操作的信息 工作流。

您可以保留和管理特定用户(包括您自己)和类的调试日志 和触发器。设置类和触发器跟踪标志不会导致生成日志或 保存。类和触发器跟踪标志覆盖其他日志记录级别,包括日志记录级别 由用户跟踪标志设置,但它们不会导致日志记录发生。如果在以下情况下启用日志记录 类或触发器执行,日志在执行时生成。

若要查看调试日志,请从“设置”中输入“快速查找”框,然后选择“调试日志”。然后 单击要检查的调试日志旁边的“查看”。单击下载”将日志下载为 XML 文件。Debug Logs

调试日志具有以下限制。

  • 每个调试日志必须小于或等于 20 MB。减少大于 20 MB 的调试日志 通过删除较旧的日志行(例如早期语句的日志行)来调整大小。日志行可以从任何 位置,而不仅仅是调试日志的开头。System.debug
  • 系统调试日志将保留 24 小时。监视调试日志保留 7 个 日。
  • 如果在 15 分钟的时段内生成超过 1,000 MB 的调试日志,则跟踪标志为 禁用。我们向上次修改跟踪标志的用户发送一封电子邮件,通知他们 他们可以在 15 分钟内重新启用跟踪标志。警告如果调试日志跟踪 标志在经常访问的 Apex 类上启用,或者为经常执行请求的用户启用, 无论时间窗口和调试大小如何,请求都可能导致失败 原木。
  • 当您的组织累积的调试日志超过 1,000 MB 时,我们会阻止组织中的用户 添加或编辑跟踪标志。添加或编辑跟踪标志以便生成更多日志 达到限制后,请删除一些调试日志。

检查调试日志部分

生成调试日志后,列出的信息类型和数量取决于为用户设置的筛选器值。 但是,调试日志的格式始终相同。

注意

会话 ID 在 Apex 调试日志中替换为“SESSION_ID_REMOVED”调试日志包含以下部分。页眉标头包含以下信息。

  • 事务期间使用的 API 版本。
  • 用于生成 日志。例如:

下面是一个示例 页眉。

59.0 APEX_CODE,DEBUG;APEX_PROFILING,INFO;CALLOUT,INFO;DB,INFO;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,INFO;
WORKFLOW,INFO

本示例中,API版本为59.0,并设置了以下调试日志类别和级别。

Apex 代码调试
Apex 分析信息
标注信息
数据库信息
系统调试
验证信息
视觉力信息
工作流程信息

警告

如果 Apex Code 日志级别设置为 FINEST,则调试 日志包含所有 Apex 变量分配的详细信息。确保 Apex 代码 被跟踪不会处理敏感数据。在启用 FINEST 日志级别之前,请 确保了解组织的 Apex 处理的敏感数据级别。是 特别注意社区用户自助注册等流程,其中 可以将用户密码分配给 Apex 字符串变量。执行单元执行单元等同于事务。它包含以下一切 在事务中发生。 并分隔执行单元。EXECUTION_STARTEDEXECUTION_FINISHED代码单元代码单元是事务中的离散工作单元。例如,触发器 是一个代码单元,方法也是一个 或验证规则。webservice

注意

不是离散的代码单元。CODE_UNIT_STARTED并分隔代码单位。工作单元 可以嵌入其他工作单元。例如:CODE_UNIT_FINISHED

EXECUTION_STARTED
CODE_UNIT_STARTED|[EXTERNAL]execute_anonymous_apex
CODE_UNIT_STARTED|[EXTERNAL]MyTrigger on Account trigger event BeforeInsert for [new]|__sfdc_trigger/MyTrigger
CODE_UNIT_FINISHED <-- The trigger ends
CODE_UNIT_FINISHED <-- The executeAnonymous ends
EXECUTION_FINISHED

代码单元包括但不限于以下内容:

  • 触发器
  • 工作流调用和基于时间的工作流
  • 验证规则
  • 审批流程
  • Apex 潜在客户转换
  • @future方法调用
  • Web 服务调用
  • executeAnonymous调用
  • Apex 控制器上的 Visualforce 属性访问
  • Apex 控制器上的 Visualforce 操作
  • 批量执行 Apex 和方法,以及每次执行 方法startfinishexecute
  • Apex 方法的执行System.Schedule execute
  • 传入电子邮件处理

日志行日志行包含在代码单元中,并指示哪些代码或规则是 正在执行。日志行也可以是写入调试日志的消息。例如:

Debug Log Line Example

日志行由一组字段组成,由竖线 () 分隔。格式为:|

  • timestamp:由事件发生的时间和一个值组成 括号之间。时间采用用户的时区,格式为 。括号中的值表示时间 自请求开始以来经过的纳秒。运行时间值为 从使用执行时在开发者控制台中查看的日志中排除 日志视图。但是,当您使用“原始日志”视图时,您可以看到经过的时间。自 打开 Raw Log 视图,在 Developer Console 的 Logs 选项卡中,右键单击名称 ,然后选择打开原始日志HH:mm:ss.SSS
  • event identifier:指定触发调试日志的事件 条目(例如 或 )。SAVEPOINT_RESETVALIDATION_RULE还包括其他 与该事件一起记录的信息,例如方法名称或行,以及 执行代码的字符号。如果无法找到行号,则改为记录。为 示例,为内置记录 托管包中的 Apex 类或代码。[EXTERNAL][EXTERNAL]对于某些事件(、、、、和),事件结束 标识符包括一个管道 (),后跟 Apex 类或触发器的 typeRef。CODE_UNIT_STARTEDCODE_UNIT_FINISHEDVF_APEX_CALL_STARTVF_APEX_CALL_ENDCONSTRUCTOR_ENTRYCONSTRUCTOR_EXIT|对于触发器,typeRef 开始 替换为 SFDC 触发器前缀。例如,或 .__sfdc_trigger/__sfdc_trigger/YourTriggerName__sfdc_trigger/YourNamespace/YourTriggerName为 一个类,typeRef 使用格式 或 。YourClassYourClass$YourInnerClass,YourNamespace/YourClass$YourInnerClass

更多日志数据此外,日志还包含以下信息。

  • 累积资源使用情况记录在许多代码单元的末尾。其中 代码单元是触发器、 批处理 Apex 消息处理、方法、Apex 测试方法、Apex Web 服务方法和 Apex 潜在客户转换。executeAnonymous@future
  • 累积分析信息在事务结束时记录一次 并包含有关 DML 调用、成本高昂的查询等的信息。 “昂贵”的查询会大量使用资源。

下面是一个调试示例 日志。

37.0 APEX_CODE,FINEST;APEX_PROFILING,INFO;CALLOUT,INFO;DB,INFO;SYSTEM,DEBUG;
    VALIDATION,INFO;VISUALFORCE,INFO;WORKFLOW,INFO
Execute Anonymous: System.debug('Hello World!');
16:06:58.18 (18043585)|USER_INFO|[EXTERNAL]|005D0000001bYPN|devuser@example.org|
    Pacific Standard Time|GMT-08:00
16:06:58.18 (18348659)|EXECUTION_STARTED
16:06:58.18 (18383790)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
16:06:58.18 (23822880)|HEAP_ALLOCATE|[72]|Bytes:3
16:06:58.18 (24271272)|HEAP_ALLOCATE|[77]|Bytes:152
16:06:58.18 (24691098)|HEAP_ALLOCATE|[342]|Bytes:408
16:06:58.18 (25306695)|HEAP_ALLOCATE|[355]|Bytes:408
16:06:58.18 (25787912)|HEAP_ALLOCATE|[467]|Bytes:48
16:06:58.18 (26415871)|HEAP_ALLOCATE|[139]|Bytes:6
16:06:58.18 (26979574)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:1
16:06:58.18 (27384663)|STATEMENT_EXECUTE|[1]
16:06:58.18 (27414067)|STATEMENT_EXECUTE|[1]
16:06:58.18 (27458836)|HEAP_ALLOCATE|[1]|Bytes:12
16:06:58.18 (27612700)|HEAP_ALLOCATE|[50]|Bytes:5
16:06:58.18 (27768171)|HEAP_ALLOCATE|[56]|Bytes:5
16:06:58.18 (27877126)|HEAP_ALLOCATE|[64]|Bytes:7
16:06:58.18 (49244886)|USER_DEBUG|[1]|DEBUG|Hello World!
16:06:58.49 (49590539)|CUMULATIVE_LIMIT_USAGE
16:06:58.49 (49590539)|LIMIT_USAGE_FOR_NS|(default)|
  Number of SOQL queries: 0 out of 100
  Number of query rows: 0 out of 50000
  Number of SOSL queries: 0 out of 20
  Number of DML statements: 0 out of 150
  Number of DML rows: 0 out of 10000
  Maximum CPU time: 0 out of 10000
  Maximum heap size: 0 out of 6000000
  Number of callouts: 0 out of 100
  Number of Email Invocations: 0 out of 10
  Number of future calls: 0 out of 50
  Number of queueable jobs added to the queue: 0 out of 50
  Number of Mobile Apex push calls: 0 out of 10

16:06:58.49 (49590539)|CUMULATIVE_LIMIT_USAGE_END

16:06:58.18 (52417923)|CODE_UNIT_FINISHED|execute_anonymous_apex
16:06:58.18 (54114689)|EXECUTION_FINISHED

为 Apex 类设置调试日志过滤器和 触发器

调试日志筛选提供了一种机制,用于在触发器时微调日志详细程度 和班级水平。这在调试 Apex 时特别有用 逻辑。例如,若要评估复杂进程的输出,可以引发日志 给定类的详细程度,同时关闭其他类或触发器的日志记录 单个请求。

当您重写类或触发器的调试日志级别时, 这些调试级别也适用于您的类或触发器调用的类方法,以及 因此被执行的触发器。执行路径中的所有类方法和触发器 从其调用方继承调试日志设置,除非他们具有这些设置 重写。

下图演示了在类和触发器中重写调试日志级别 水平。对于这种情况,假设导致 您想仔细研究的一些问题。为此,调试日志级别 的被提升到最细的粒度。 不会覆盖这些日志级别,并且 因此继承了 的粒度日志过滤器。但是,有 已经过测试并且已知可以正常工作,因此它已关闭其日志过滤器。 同样,不在导致 一个问题,因此它的日志记录最小化,只记录 Apex 代码的错误 类别。 从 继承这些日志设置。Class1Class1Class3Class1UtilityClassClass2Trigger2Class2

微调类和触发器的调试日志记录Debug log filters for classes and triggers下面是该关系图所基于的伪代码示例。

  1. Trigger1调用 的方法和 的另一个方法。为 例:Class1Class2trigger Trigger1 on Account (before insert) { Class1.someMethod(); Class2.anotherMethod(); }
  2. Class1调用 的方法,而 的方法又调用实用程序的方法 类。为 例:Class3public class Class1 { public static void someMethod() { Class3.thirdMethod(); } } public class Class3 { public static void thirdMethod() { UtilityClass.doSomething(); } }
  3. Class2导致触发器 执行。为 例:Trigger2public class Class2 { public static void anotherMethod() { // Some code that causes Trigger2 to be fired. } }
  • 在开发者控制台中使用日志
  • 调试Apex API 调用
  • 调试日志 优先顺序 记录
    哪些事件取决于各种因素。这些因素包括跟踪标志、默认日志记录级别、API 标头、基于用户的系统日志启用以及入口点设置的日志级别。

在开发者控制台中使用日志

使用开发者控制台中的“日志”选项卡打开调试日志。

使用“日志”选项卡打开相关调试日志以供查看

日志在 Log Inspector 中打开。Log Inspector 是 开发者控制台。它显示操作的来源、触发操作的内容以及内容 接下来发生了。使用此工具检查调试日志,其中包括数据库事件、Apex 处理、 工作流和验证逻辑。

要了解有关在开发人员控制台中使用日志的更多信息,请参阅 Salesforce 联机帮助中的日志检查器。使用开发人员控制台或监控调试日志时,您可以指定 日志中包含的信息。日志类别记录的信息类型,例如来自 Apex 或工作流规则的信息。日志级别记录的信息量。事件类型日志类别和日志级别的组合,用于指定要记录的事件。每 事件可以记录其他信息,例如事件所在的行号和字符号 已开始、与事件关联的字段以及事件的持续时间。

调试日志类别

每个调试级别都包括以下每个日志类别的调试日志级别。这 每个类别记录的信息量取决于日志级别。

日志类别描述
数据库包括有关数据库活动的信息,包括每个数据操作 语言 (DML) 语句或内联 SOQL 或 SOSL 查询。
工作流程包括工作流规则、流和流程的信息,例如规则名称 以及所采取的行动。
NBA(英语:包括有关 Einstein Next Best Action 活动的信息,包括策略 来自策略生成器的执行详细信息。
验证包括有关验证规则的信息,例如规则的名称以及是否 规则的计算结果为 true 或 false。
标注包括服务器所在的请求-响应 XML 从外部 Web 服务发送和接收。在调试与以下内容相关的问题时很有用 使用 Lightning 平台 Web 服务 API 调用或对用户访问外部进行故障排除 对象通过 Salesforce Connect。
Apex 代码包括有关 Apex 代码的信息。可以包含日志消息等信息 由 DML 语句、内联 SOQL 或 SOSL 查询生成,任何 触发器,以及任何测试方法的开始和完成。
Apex 分析包括累积分析信息,例如命名空间的限制和 发送的电子邮件数量。
视觉力包括有关 Visualforce 事件的信息,包括序列化和 在 Visualforce 中反序列化视图状态或计算公式字段 页。
系统包括有关调用所有系统方法(如方法)的信息。System.debug

调试日志级别

对于每个日志类别,每个调试级别都包括以下日志级别之一。关卡 按从低到高的顺序列出。根据以下组合记录特定事件 类别和级别。大多数事件开始在 INFO 级别记录。水平是累积的, 也就是说,如果选择 FINE,则日志还包括在 DEBUG、INFO、WARN、 和 ERROR 级别。

注意

并非所有级别都适用于所有类别。只有级别 对应于一个或多个事件可用。

  • NONE
  • ERROR
  • WARN
  • INFO
  • DEBUG
  • FINE
  • FINER
  • FINEST

重要

在运行部署之前,请验证是否未设置 Apex Code 日志级别 到FINEST。否则,部署所需的时间可能比预期的要长。如果开发者 控制台处于打开状态,开发者控制台中的日志级别会影响所有日志,包括创建的日志 在部署期间。

调试事件类型

以下是写入调试日志的内容的示例。该事件是 。格式为 |:

USER_DEBUGtimestampevent identifier

  • timestamp:由事件发生的时间和 括弧。时间采用用户的时区,格式为 。括号中的值表示经过的时间 自请求开始以来的纳秒数。运行时间值将从日志中排除 当您使用“执行日志”视图时,在开发人员控制台中查看。但是,您可以看到 使用“原始日志”视图时经过的时间。要打开“原始日志”视图,请从 Developer 控制台的“日志”选项卡,右键单击日志的名称,然后选择“打开原始日志” 日志HH:mm:ss.SSS
  • event identifier:指定触发调试日志条目的事件(例如 作为 或 )。SAVEPOINT_RESETVALIDATION_RULE还包括与该事件一起记录的其他信息, 例如方法名称或执行代码的行号和字符号。如果 无法找到行号,已记录 相反。例如,为内置记录 托管包中的 Apex 类或代码。[EXTERNAL][EXTERNAL]对于某些事件(、、、、和)、 事件标识符的末尾包括一个管道 () 后跟 Apex 类或触发器的 typeRef。CODE_UNIT_STARTEDCODE_UNIT_FINISHEDVF_APEX_CALL_STARTVF_APEX_CALL_ENDCONSTRUCTOR_ENTRYCONSTRUCTOR_EXIT|对于触发器,typeRef 开始 替换为 SFDC 触发器前缀。为 example,或 .__sfdc_trigger/__sfdc_trigger/YourTriggerName__sfdc_trigger/YourNamespace/YourTriggerName为 一个类,typeRef 使用格式 或 。YourClassYourClass$YourInnerClass,YourNamespace/YourClass$YourInnerClass

以下是调试日志行的示例。

调试日志行示例调试日志行示例在此示例中,事件标识符由以下部分组成:

  • 活动名称:USER_DEBUG
  • 代码中事件的行号:[2]
  • 设置方法的日志记录级别 自:System.DebugDEBUG
  • 用户为该方法提供的字符串:System.DebugHello world!

此代码片段触发以下日志行示例。

调试日志行代码片段调试日志行代码片段当测试到达第 5 行时,将记录以下日志行 法典。

15:51:01.071 (55856000)|DML_BEGIN|[5]|Op:Insert|Type:Invoice_Statement__c|Rows:1

在此示例中,事件标识符由以下部分组成。

  • 活动名称:DML_BEGIN
  • 代码中事件的行号:[5]
  • DML 操作类型—:InsertOp:Insert
  • 对象名称:Type:Invoice_Statement__c
  • 传递到 DML 的行数 操作:Rows:1

将记录以下事件类型。该表列出了哪些字段或其他信息是 记录每个事件,以及日志级别和类别的哪个组合导致事件 登录。

活动名称与事件一起记录的字段或信息记录的类别记录的级别
BULK_HEAP_ALLOCATE分配的字节数Apex 代码最好
CALLOUT_REQUEST行号和请求标头标注INFO 及以上
CALLOUT_REQUEST(外部对象访问 用于 Salesforce Connect 的跨组织和 OData 适配器)外部终结点和方法标注INFO 及以上
CALLOUT_RESPONSE行号和响应正文标注INFO 及以上
CALLOUT_RESPONSE(外部对象访问 用于 Salesforce Connect 的跨组织和 OData 适配器)状态和状态代码标注INFO 及以上
CODE_UNIT_FINISHED行号、代码单元名称,例如 和:MyTrigger on Account trigger event BeforeInsert for [new]对于 Apex 方法,命名空间(如果适用)、类名和方法名;为 示例,或者YourNamespace.YourClass.yourMethod()YourClass.yourMethod()对于 Apex 触发器,typeRef;例如,或__sfdc_trigger/YourNamespace.YourTrigger__sfdc_trigger/YourTriggerApex 代码ERROR 及以上
CODE_UNIT_STARTED行号、代码单元名称,例如 和:MyTrigger on Account trigger event BeforeInsert for [new]对于 Apex 方法,命名空间(如果适用)、类名和方法名;为 示例,或者YourNamespace.YourClass.yourMethod()YourClass.yourMethod()对于 Apex 触发器,typeRef;例如__sfdc_trigger/YourTriggerApex 代码ERROR 及以上
CONSTRUCTOR_ENTRY行号、Apex 类 ID、括号内包含参数类型(如果有)的字符串,以及 类型Ref;例如,或<init>()YourClassYourClass.YourInnerClassApex 代码FINE 及以上
CONSTRUCTOR_EXIT行号,带有 括号之间的参数类型(如果有)和 typeRef;例如,或<init>()YourClassYourClass.YourInnerClassApex 代码FINE 及以上
CUMULATIVE_LIMIT_USAGE没有Apex 分析INFO 及以上
CUMULATIVE_LIMIT_USAGE_END没有Apex 分析INFO 及以上
CUMULATIVE_PROFILING没有Apex 分析FINE 及以上
CUMULATIVE_PROFILING_BEGIN没有Apex 分析FINE 及以上
CUMULATIVE_PROFILING_END没有Apex 分析FINE 及以上
DML_BEGIN行号、操作(如或)、记录名称或类型以及行数 传递到 DML 操作InsertUpdate分贝INFO 及以上
DML_END行号分贝INFO 及以上
EMAIL_QUEUE行号Apex 代码INFO 及以上
ENTERING_MANAGED_PKG包命名空间Apex 代码FINE 及以上
EVENT_SERVICE_PUB_BEGIN事件类型工作流程INFO 及以上
EVENT_SERVICE_PUB_DETAIL订阅 ID、发布事件的用户的 ID 和事件消息 数据工作流程FINER 及以上
EVENT_SERVICE_PUB_END事件类型工作流程INFO 及以上
EVENT_SERVICE_SUB_BEGIN事件类型和操作(订阅或取消订阅)工作流程INFO 及以上
EVENT_SERVICE_SUB_DETAIL订阅 ID、订阅实例 ID、引用数据(如 进程 API 名称)、激活或停用订阅的用户的 ID 和事件 消息数据工作流程FINER 及以上
EVENT_SERVICE_SUB_END事件类型和操作(订阅或取消订阅)工作流程INFO 及以上
EXCEPTION_THROWN行号、异常类型和消息Apex 代码INFO 及以上
EXECUTION_FINISHED没有Apex 代码ERROR 及以上
EXECUTION_STARTED没有Apex 代码ERROR 及以上
FATAL_ERROR异常类型、消息和堆栈跟踪Apex 代码ERROR 及以上
FLOW_ACTIONCALL_DETAIL采访 ID、元素名称、操作类型、操作枚举或 ID,无论是操作调用 成功,并显示错误消息工作流程FINER 及以上
FLOW_ASSIGNMENT_DETAIL采访 ID、引用、运算符和值工作流程FINER 及以上
FLOW_BULK_ELEMENT_BEGIN采访 ID 和元素类型工作流程FINE 及以上
FLOW_BULK_ELEMENT_DETAIL采访 ID、元素类型、元素名称、记录数工作流程FINER 及以上
FLOW_BULK_ELEMENT_END采访 ID、元素类型、元素名称、记录数和执行 时间工作流程FINE 及以上
FLOW_BULK_ELEMENT_LIMIT_USAGE将使用量递增,以达到此批量元素的限制。每个事件都显示使用情况 对于以下限制之一:SOQL queries SOQL query rows SOSL queries DML statements DML rows CPU time in ms Heap size in bytes Callouts Email invocations Future calls Jobs in queue Push notifications工作流程FINER 及以上
FLOW_BULK_ELEMENT_NOT_SUPPORTED不支持批量操作的操作、元素名称和实体名称工作流程INFO 及以上
FLOW_CREATE_INTERVIEW_BEGIN组织 ID、定义 ID 和版本 ID工作流程INFO 及以上
FLOW_CREATE_INTERVIEW_END面试 ID 和流名称工作流程INFO 及以上
FLOW_CREATE_INTERVIEW_ERROR消息、组织 ID、定义 ID 和版本 ID工作流程ERROR 及以上
FLOW_ELEMENT_BEGIN采访 ID、元素类型和元素名称工作流程FINE 及以上
FLOW_ELEMENT_DEFERRED元素类型和元素名称工作流程FINE 及以上
FLOW_ELEMENT_END采访 ID、元素类型和元素名称工作流程FINE 及以上
FLOW_ELEMENT_ERROR消息、元素类型和元素名称(流运行时异常)工作流程ERROR 及以上
FLOW_ELEMENT_ERROR消息、元素类型和元素名称(未找到 spark)工作流程ERROR 及以上
FLOW_ELEMENT_ERROR消息、元素类型和元素名称(设计器例外)工作流程ERROR 及以上
FLOW_ELEMENT_ERROR消息、元素类型和元素名称(超出设计器限制)工作流程ERROR 及以上
FLOW_ELEMENT_ERROR消息、元素类型和元素名称(设计器运行时异常)工作流程ERROR 及以上
FLOW_ELEMENT_FAULT消息、元素类型和元素名称(采用的错误路径)工作流程警告及以上
FLOW_ELEMENT_LIMIT_USAGE将使用量递增,以达到此元素的限制。每个事件都显示 这些限制之一。SOQL queries SOQL query rows SOSL queries DML statements DML rows CPU time in ms Heap size in bytes Callouts Email invocations Future calls Jobs in queue Push notifications工作流程FINER 及以上
FLOW_INTERVIEW_FINISHED_LIMIT_USAGEUsage toward a limit when the interview finishes. Each event displays the usage for one of these limits.SOQL queries SOQL query rows SOSL queries DML statements DML rows CPU time in ms Heap size in bytes Callouts Email invocations Future calls Jobs in queue Push notifications工作流程FINER 及以上
FLOW_INTERVIEW_PAUSED采访 ID、流名称以及用户暂停的原因工作流程INFO 及以上
FLOW_INTERVIEW_RESUMED面试 ID 和流名称工作流程INFO 及以上
FLOW_LOOP_DETAIL访谈 ID、索引和值索引是集合变量中的位置 对于循环正在操作的项。工作流程FINER 及以上
FLOW_RULE_DETAIL面试 ID、规则名称和结果工作流程FINER 及以上
FLOW_START_INTERVIEW_BEGIN面试 ID 和流名称工作流程INFO 及以上
FLOW_START_INTERVIEW_END面试 ID 和流名称工作流程INFO 及以上
FLOW_START_INTERVIEWS_BEGIN请求工作流程INFO 及以上
FLOW_START_INTERVIEWS_END请求工作流程INFO 及以上
FLOW_START_INTERVIEWS_ERROR消息、采访 ID 和流名称工作流程ERROR 及以上
FLOW_START_INTERVIEW_LIMIT_USAGE在面试开始时达到限制的使用量。每个事件都显示 以下限制之一:SOQL queries SOQL query rows SOSL queries DML statements DML rows CPU time in ms Heap size in bytes Callouts Email invocations Future calls Jobs in queue Push notifications工作流程FINER 及以上
FLOW_START_SCHEDULED_RECORDS运行流的消息和记录数工作流程INFO 及以上
FLOW_SUBFLOW_DETAIL采访 ID、姓名、定义 ID 和版本 ID工作流程FINER 及以上
FLOW_VALUE_ASSIGNMENT面试 ID、键和值工作流程FINER 及以上
FLOW_WAIT_EVENT_RESUMING_DETAIL采访 ID、元素名称、事件名称和事件类型工作流程FINER 及以上
FLOW_WAIT_EVENT_WAITING_DETAIL采访 ID、元素名称、事件名称、事件类型以及条件是否 遇到工作流程FINER 及以上
FLOW_WAIT_RESUMING_DETAIL采访 ID、元素名称和持久采访 ID工作流程FINER 及以上
FLOW_WAIT_WAITING_DETAIL采访 ID、元素名称、元素正在等待的事件数,以及 持久化采访 ID工作流程FINER 及以上
HEAP_ALLOCATE行号和字节数Apex 代码FINER 及以上
HEAP_DEALLOCATE行号和释放的字节数Apex 代码FINER 及以上
IDEAS_QUERY_EXECUTE行号分贝最好
LIMIT_USAGE_FOR_NS命名空间和以下内容 限制:Number of SOQL queries Number of query rows Number of SOSL queries Number of DML statements Number of DML rows Number of code statements Maximum heap size Number of callouts Number of Email Invocations Number of fields describes Number of record type describes Number of child relationships describes Number of picklist describes Number of future calls Number of find similar calls Number of System.runAs() invocationsApex 分析最好
METHOD_ENTRY行号、类的 Lightning 平台 ID 和方法签名(带有 命名空间(如果适用)Apex 代码FINE 及以上
METHOD_EXIT行号、类的 Lightning 平台 ID 和方法签名(带有 命名空间(如果适用)对于构造函数,将记录以下信息: 行 编号和类名。Apex 代码FINE 及以上
NAMED_CREDENTIAL_REQUEST命名凭据 ID、命名凭据名称、终结点、方法、外部凭据 类型、Http 标头授权、请求大小字节数和重试 401。如果使用 出站网络连接,还会记录以下附加字段: 出站网络 连接 ID、出站网络连接名称、出站网络连接状态、主机 类型、主机区域和专用连接出站每小时数据使用百分比。标注INFO 及以上
NAMED_CREDENTIAL_RESPONSE从 NamedCredential 返回的响应正文的截断部分 标注。标注INFO 及以上
NAMED_CREDENTIAL_RESPONSE_DETAIL命名凭据 ID、命名凭据名称、状态代码、响应大小字节、总体 标注时间毫秒和连接时间毫秒。如果使用出站网络连接,则这些 还会记录其他字段:出站网络连接 ID、出站网络 连接名称和专用连接出站每小时数据使用百分比。标注FINER 及以上
NBA_NODE_BEGIN元素名称、元素类型NBA(英语:FINE 及以上
NBA_NODE_DETAIL元素名称、元素类型、消息NBA(英语:FINE 及以上
NBA_NODE_END元素名称、元素类型、消息NBA(英语:FINE 及以上
NBA_NODE_ERROR元素名称、元素类型、错误消息NBA(英语:ERROR 及以上
NBA_OFFER_INVALID姓名、ID、原因NBA(英语:FINE 及以上
NBA_STRATEGY_BEGIN策略名称NBA(英语:FINE 及以上
NBA_STRATEGY_END策略名称、输出计数NBA(英语:FINE 及以上
NBA_STRATEGY_ERROR策略名称、错误消息NBA(英语:ERROR 及以上
POP_TRACE_FLAGS行号,具有其日志级别的类或触发器的 Lightning 平台 ID 设置,进入范围,此类或触发器的名称以及日志级别 离开此作用域后生效的设置系统INFO 及以上
PUSH_NOTIFICATION_INVALID_APP应用命名空间、应用名称当 Apex 代码尝试发送 通知组织中不存在或未启用推送的应用。Apex 代码错误
PUSH_NOTIFICATION_INVALID_CERTIFICATE应用命名空间、应用名称此事件表示证书无效。为 例如,它已过期。Apex 代码错误
PUSH_NOTIFICATION_INVALID_NOTIFICATION应用命名空间、应用名称、服务类型(Apple 或 Android GCM)、用户 ID、设备、有效负载 (子字符串),有效负载长度。当通知有效负载太 长。Apex 代码错误
PUSH_NOTIFICATION_NO_DEVICES应用命名空间、应用名称当我们尝试的所有用户都没有时,会发生此事件 发送通知以注册设备。Apex 代码调试
PUSH_NOTIFICATION_NOT_ENABLED当组织中未启用推送通知时,会发生此事件。Apex 代码信息
PUSH_NOTIFICATION_SENT应用命名空间、应用名称、服务类型(Apple 或 Android GCM)、用户 ID、设备、有效负载 (子字符串)此事件记录已接受发送通知。我们没有 保证通知的送达。Apex 代码调试
PUSH_TRACE_FLAGS行号、设置了日志级别的类或触发器的 Salesforce ID,以及 超出范围、此类或触发器的名称以及日志级别设置 进入此范围后生效的系统INFO 及以上
QUERY_MORE_BEGIN行号分贝INFO 及以上
QUERY_MORE_END行号分贝INFO 及以上
QUERY_MORE_ITERATIONS行号和迭代次数queryMore分贝INFO 及以上
SAVEPOINT_ROLLBACK行号和保存点名称分贝INFO 及以上
SAVEPOINT_SET行号和保存点名称分贝INFO 及以上
SLA_END案例数、加载时间、处理时间、要插入的案例里程碑数、 更新、删除和新建触发器工作流程INFO 及以上
SLA_EVAL_MILESTONE里程碑 ID工作流程INFO 及以上
SLA_NULL_START_DATE没有工作流程INFO 及以上
SLA_PROCESS_CASE案例 ID工作流程INFO 及以上
SOQL_EXECUTE_BEGIN行号、聚合数和查询源分贝INFO 及以上
SOQL_EXECUTE_END行号、行数和持续时间(以毫秒为单位)分贝INFO 及以上
SOQL_EXECUTE_EXPLAIN已执行的 SOQL 查询的查询计划详细信息。有关查看查询计划的信息 使用开发人员控制台,请参阅检索查询计划。获取反馈 有关查询性能的信息,请参阅获取有关 查询性能。分贝最好
SOSL_EXECUTE_BEGIN行号和查询源分贝INFO 及以上
SOSL_EXECUTE_END行号、行数和持续时间(以毫秒为单位)分贝INFO 及以上
STACK_FRAME_VARIABLE_LIST帧号和变量列表的形式:| .例如:Variable numberValuevar1:50 var2:'Hello World'Apex 分析FINE 及以上
STATEMENT_EXECUTE行号Apex 代码FINER 及以上
STATIC_VARIABLE_LIST格式为:| .例如:Variable numberValuevar1:50 var2:'Hello World'Apex 分析FINE 及以上
SYSTEM_CONSTRUCTOR_ENTRY行号和带有 括号之间的参数类型(如果有)<init>()系统FINE 及以上
SYSTEM_CONSTRUCTOR_EXIT行号和带有 括号之间的参数类型(如果有)<init>()系统FINE 及以上
SYSTEM_METHOD_ENTRY行号和方法签名系统FINE 及以上
SYSTEM_METHOD_EXIT行号和方法签名系统FINE 及以上
SYSTEM_MODE_ENTER模式名称系统INFO 及以上
SYSTEM_MODE_EXIT模式名称系统INFO 及以上
TESTING_LIMITS没有Apex 分析INFO 及以上
TOTAL_EMAIL_RECIPIENTS_QUEUED发送的电子邮件数量Apex 分析FINE 及以上
USER_DEBUG行号、日志记录级别和用户提供的字符串Apex 代码默认为 DEBUG 及以上版本。如果用户设置了方法的日志级别,则会在该级别记录事件 相反。System.Debug
USER_INFO行号、用户 ID、用户名、用户时区和用户时区(以 GMT 为单位)Apex 代码ERROR 及以上
VALIDATION_ERROR错误信息验证INFO 及以上
VALIDATION_FAIL没有验证INFO 及以上
VALIDATION_FORMULA公式来源和值验证INFO 及以上
VALIDATION_PASS没有验证INFO 及以上
VALIDATION_RULE规则名称验证INFO 及以上
VARIABLE_ASSIGNMENT行号、变量名称(包括变量的命名空间,如果适用)、 变量值和变量地址的字符串表示形式Apex 代码最好
VARIABLE_SCOPE_BEGIN行号、变量名称(包括变量的命名空间,如果适用)、类型、 一个值,指示是否可以引用变量,以及一个值,指示 变量是否为静态变量Apex 代码最好
VARIABLE_SCOPE_END没有Apex 代码最好
VF_APEX_CALL_STARTVisualforce 控制器的元素名称、方法名称、返回类型和 typeRef (例如,YourApexClass)Apex 代码INFO 及以上
VF_APEX_CALL_ENDVisualforce 控制器的元素名称、方法名称、返回类型和 typeRef (例如,YourApexClass)Apex 代码INFO 及以上
VF_DESERIALIZE_VIEWSTATE_BEGIN查看状态 ID视觉力INFO 及以上
VF_DESERIALIZE_VIEWSTATE_END没有视觉力INFO 及以上
VF_EVALUATE_FORMULA_BEGIN查看状态 ID 和公式视觉力FINER 及以上
VF_EVALUATE_FORMULA_END没有视觉力FINER 及以上
VF_PAGE_MESSAGE消息文本Apex 代码INFO 及以上
VF_SERIALIZE_VIEWSTATE_BEGIN查看状态 ID视觉力INFO 及以上
VF_SERIALIZE_VIEWSTATE_END没有视觉力INFO 及以上
WF_ACTION操作说明工作流程INFO 及以上
WF_ACTION_TASK任务主题、操作 ID、规则名称、规则 ID、所有者和截止日期工作流程INFO 及以上
WF_ACTIONS_END所执行操作的摘要工作流程INFO 及以上
WF_APPROVAL转换类型、 和 进程节点名称EntityName: NameField Id工作流程INFO 及以上
WF_APPROVAL_REMOVEEntityName: NameField Id工作流程INFO 及以上
WF_APPROVAL_SUBMITEntityName: NameField Id工作流程INFO 及以上
WF_APPROVAL_SUBMITTER提交者 ID、提交者全名和错误消息工作流程INFO 及以上
WF_ASSIGN所有者和受托人模板 ID工作流程INFO 及以上
WF_CRITERIA_BEGINEntityName: NameField Id、规则名称、规则 ID 和 (如果规则遵循触发器类型) 触发器类型和递归计数工作流程INFO 及以上
WF_CRITERIA_END指示成功的布尔值(true 或 false)工作流程INFO 及以上
WF_EMAIL_ALERT操作 ID、规则名称和规则 ID工作流程INFO 及以上
WF_EMAIL_SENT电子邮件模板 ID、收件人和抄送电子邮件工作流程INFO 及以上
WF_ENQUEUE_ACTIONS排队的操作摘要工作流程INFO 及以上
WF_ESCALATION_ACTION箱 ID 和升级日期工作流程INFO 及以上
WF_ESCALATION_RULE没有工作流程INFO 及以上
WF_EVAL_ENTRY_CRITERIA进程名称、电子邮件模板 ID 和指示结果的布尔值(true 或 false)工作流程INFO 及以上
WF_FIELD_UPDATEEntityName: NameField Id和对象或字段 名字工作流程INFO 及以上
WF_FLOW_ACTION_BEGIN流触发器的 ID工作流程INFO 及以上
WF_FLOW_ACTION_DETAIL流触发器的 ID、对象类型和记录的 ID,其创建或更新导致了 要触发的工作流规则、工作流规则的名称和 ID,以及流的名称和值 变量工作流程FINE 及以上
WF_FLOW_ACTION_END流触发器的 ID工作流程INFO 及以上
WF_FLOW_ACTION_ERROR流触发器 ID、流定义的 ID、流版本的 ID 和流错误 消息工作流程ERROR 及以上
WF_FLOW_ACTION_ERROR_DETAIL详细的流错误消息工作流程ERROR 及以上
WF_FORMULA公式来源和值工作流程INFO 及以上
WF_HARD_REJECT没有工作流程INFO 及以上
WF_NEXT_APPROVER所有者、下一个所有者类型和字段工作流程INFO 及以上
WF_NO_PROCESS_FOUND没有工作流程INFO 及以上
WF_OUTBOUND_MSGEntityName: NameField Id、操作 ID、规则名称、 和规则 ID工作流程INFO 及以上
WF_PROCESS_FOUND进程定义 ID 和进程标签工作流程INFO 及以上
WF_PROCESS_NODE进程名称工作流程INFO 及以上
WF_REASSIGN_RECORDEntityName: NameField Id和所有者工作流程INFO 及以上
WF_RESPONSE_NOTIFY通知程序名称、通知程序电子邮件、通知程序模板 ID 和回复电子邮件工作流程INFO 及以上
WF_RULE_ENTRY_ORDER表示顺序的整数工作流程INFO 及以上
WF_RULE_EVAL_BEGIN规则类型工作流程INFO 及以上
WF_RULE_EVAL_END没有工作流程INFO 及以上
WF_RULE_EVAL_VALUE价值工作流程INFO 及以上
WF_RULE_FILTER筛选条件工作流程INFO 及以上
WF_RULE_INVOCATIONEntityName: NameField Id工作流程INFO 及以上
WF_RULE_NOT_EVALUATED没有工作流程INFO 及以上
WF_SOFT_REJECT进程名称工作流程INFO 及以上
WF_SPOOL_ACTION_BEGIN节点类型工作流程INFO 及以上
WF_TIME_TRIGGEREntityName: NameField Id、时间动作、时间 操作容器和计算日期时间工作流程INFO 及以上
WF_TIME_TRIGGERS_BEGIN没有工作流程INFO 及以上
XDS_DETAIL(外部对象访问 用于 Salesforce Connect 的跨组织和 OData 适配器)对于 OData 适配器,自定义 HTTP 的 POST 正文和名称以及计算公式 头标注FINER 及以上
XDS_RESPONSE(外部对象访问 用于 Salesforce Connect 的跨组织和 OData 适配器)外部数据源、外部对象、请求详细信息、返回记录数、 和系统使用情况标注INFO 及以上
XDS_RESPONSE_DETAIL(外部对象访问 用于 Salesforce Connect 的跨组织和 OData 适配器)来自外部系统的截断响应,包括返回的记录标注FINER 及以上
XDS_RESPONSE_ERROR(外部对象访问 用于 Salesforce Connect 的跨组织和 OData 适配器)错误信息标注ERROR 及以上

调试Apex API 调用

调用 Apex 的所有 API 调用都支持允许访问 有关代码执行的详细信息,包括对 的任何调用。SOAP 输入标头的 categories 字段允许您 根据中概述的级别设置日志记录粒度 桌子。System.debug()DebuggingHeader

元素名称类型描述
类别日志类别指定 中返回的信息类型 调试日志。有效值为:DbWorkflowValidationCalloutApex_codeApex_profilingVisualforceSystemAll
水平日志类别级别指定 调试日志。有效的日志级别包括(从 从低到高):NONEERRORWARNINFODEBUGFINEFINERFINEST

此外,作为 for backwards 的一部分,仍支持以下日志级别 兼容性。DebuggingHeader

日志级别描述
NONE不包含任何日志消息。
DEBUGONLY包括较低级别的消息和消息 由对方法的调用生成。System.debug
DB包括调用 方法和每个数据 操作语言 (DML) 语句或内联 SOQL 或 SOSL 查询。System.debug
PROFILE包括调用 方法,每个 DML 语句 或内联 SOQL 或 SOSL 查询,以及入口和 退出每个用户定义的方法。另外 调试日志的末尾包含整个 各部分的分析信息 使用最多资源的请求。这 分析信息以 SOQL 和 SOSL 语句、DML 操作和 Apex 方法调用。这三个部分列出了 代码中消耗时间最多的位置, 按总累积时间的降序排列。也 列出的是类别的次数 执行。System.debug
CALLOUT包括请求-响应 XML 服务器正在从外部发送和接收 Web 服务。在调试相关问题时很有用 使用 Lightning 平台 Web 服务 API 调用 或对用户访问外部对象进行故障排除 通过 Salesforce Connect。
DETAIL包括级别和以下级别生成的所有消息。PROFILE变量声明语句循环执行的开始所有循环控制,例如 break 和 继续引发的异常 *静态和类初始化代码 *上下文中的任何更改with sharing

相应的输出标头 ,包含生成的调试日志。查看更多 信息,请参阅 SOAP API 开发人员指南 中的 DebuggingHeaderDebuggingInfo

调试日志优先顺序

记录哪些事件取决于各种因素。这些因素包括您的跟踪 标志、默认日志记录级别、API 标头、基于用户的系统日志启用和日志 由您的入口点设置的级别。调试日志级别的优先顺序为:

  1. 跟踪标志将覆盖所有其他日志记录逻辑。开发者控制台在以下情况下设置跟踪标志 它将加载,并且该跟踪标志将一直有效,直到它过期。您可以在 开发人员控制台或在安装程序中或使用 和 工具 API 对象。TraceFlagDebugLevel注意设置类和触发器跟踪标志不会导致 要生成或保存的日志。类和触发器跟踪标志覆盖其他日志记录级别, 包括由用户跟踪标志设置的日志记录级别,但它们不会导致日志记录发生。如果 日志记录在类或触发器执行时启用,日志在执行时生成 执行。
  2. 如果您没有活动跟踪标志,则同步和异步 Apex 测试将使用 默认日志记录级别。默认日志记录级别为:分贝信息APEX_CODE调试APEX_PROFILING信息工作流程信息验证信息标注信息视觉力信息系统调试
  3. 如果没有相关的跟踪标志处于活动状态,并且没有运行任何测试,则 API 标头将 日志记录级别。在没有调试标头的情况下发送的 API 请求会生成暂时性 日志 – 未保存的日志 – 除非其他日志记录规则生效。
  4. 如果入口点设置了日志级别,则使用该日志级别。例如,Visualforce 请求可以包含用于设置日志级别的调试参数。

如果这些情况都不适用,则不会生成或保留日志。

保护您的数据

可以使用类提供的方法保护数据。

Crypto

类中的方法提供标准 用于创建摘要、消息身份验证代码和签名的算法 作为加密和解密信息。这些可用于保护 Salesforce,或与 Google 或 Amazon 等外部服务集成 网络服务 (AWS)。Crypto

示例:集成 Amazon WebServices

以下示例演示了 Amazon WebServices 与 销售人员:

public class HMacAuthCallout {

   public void testAlexaWSForAmazon() {
 
   // The date format is yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
      DateTime d = System.now();
      String timestamp = ''+ d.year() + '-' +
      d.month() + '-' +
      d.day() + '\'T\'' +
      d.hour() + ':' +
      d.minute() + ':' +
      d.second() + '.' +
      d.millisecond() + '\'Z\'';
      String timeFormat = d.formatGmt(timestamp);

      String urlEncodedTimestamp = EncodingUtil.urlEncode(timestamp, 'UTF-8');
      String action = 'UrlInfo';
      String inputStr = action + timeFormat;
      String algorithmName = 'HMacSHA1';
      Blob mac = Crypto.generateMac(algorithmName,  Blob.valueOf(inputStr), 
                                                    Blob.valueOf('your_signing_key'));
      String macUrl = EncodingUtil.urlEncode(EncodingUtil.base64Encode(mac), 'UTF-8');
 
      String urlToTest = 'amazon.com';
      String version = '2005-07-11'; 
      String endpoint = 'http://awis.amazonaws.com/';
      String accessKey = 'your_key';
 
      HttpRequest req = new HttpRequest();
      req.setEndpoint(endpoint +
                      '?AWSAccessKeyId=' + accessKey +
                      '&Action=' + action +
                      '&ResponseGroup=Rank&Version=' + version +
                      '&Timestamp=' + urlEncodedTimestamp +
                      '&Url=' + urlToTest +
                      '&Signature=' + macUrl);
 
      req.setMethod('GET');
      Http http = new Http();
      try {
         HttpResponse res = http.send(req);
         System.debug('STATUS:'+res.getStatus());
         System.debug('STATUS_CODE:'+res.getStatusCode());
         System.debug('BODY: '+res.getBody());
      } catch(System.CalloutException e) {
         System.debug('ERROR: '+ e);
      }
   }
}

示例:加密和 解密

下面的示例使用 和 方法以及类的方法。encryptWithManagedIVdecryptWithManagedIVgenerateAesKeyCrypto

// Use generateAesKey to generate the private key
Blob cryptoKey = Crypto.generateAesKey(256);

// Generate the data to be encrypted.
Blob data = Blob.valueOf('Test data to encrypted');

// Encrypt the data and have Salesforce generate the initialization vector 
Blob encryptedData = Crypto.encryptWithManagedIV('AES256', cryptoKey, data);

// Decrypt the data
Blob decryptedData = Crypto.decryptWithManagedIV('AES256', cryptoKey, encryptedData);

下面是为 和 Crypto 方法编写单元测试的示例。

encryptWithManagedIVdecryptWithManagedIV

@isTest
private class CryptoTest {
    static testMethod void testValidDecryption() {

        // Use generateAesKey to generate the private key
        Blob key = Crypto.generateAesKey(128);
        // Generate the data to be encrypted.
        Blob data = Blob.valueOf('Test data');
        // Generate an encrypted form of the data using base64 encoding
        String b64Data = EncodingUtil.base64Encode(data);
        // Encrypt and decrypt the data
        Blob encryptedData = Crypto.encryptWithManagedIV('AES128', key, data);
        Blob decryptedData = Crypto.decryptWithManagedIV('AES128', key, encryptedData);
        String b64Decrypted = EncodingUtil.base64Encode(decryptedData);
        // Verify that the strings still match
        System.assertEquals(b64Data, b64Decrypted);
    }
    static testMethod void testInvalidDecryption() {
        // Verify that you must use the same key size for encrypting data
        // Generate two private keys, using different key sizes
        Blob keyOne = Crypto.generateAesKey(128);
        Blob keyTwo = Crypto.generateAesKey(256);
        // Generate the data to be encrypted.
        Blob data = Blob.valueOf('Test data');
        // Encrypt the data using the first key 
        Blob encryptedData = Crypto.encryptWithManagedIV('AES128', keyOne, data);
        try {
         // Try decrypting the data using the second key   
            Crypto.decryptWithManagedIV('AES256', keyTwo, encryptedData);
            System.assert(false);
        } catch(SecurityException e) {
            System.assertEquals('Given final block not properly padded', e.getMessage());
        }
    }
}

对数据进行编码

您可以对 URL 进行编码和解码,并将字符串转换为十六进制 使用类提供的方法设置格式。

EncodingUtil

此示例演示如何对 UTF-8 中的时间戳值进行 URL 编码 通过调用 .urlEncode

DateTime d = System.now();
String timestamp = ''+ d.year() + '-' +
    d.month() + '-' +
    d.day() + '\'T\'' +
    d.hour() + ':' +
    d.minute() + ':' +
    d.second() + '.' +
    d.millisecond() + '\'Z\'';
System.debug(timestamp);
String urlEncodedTimestamp = EncodingUtil.urlEncode(timestamp, 'UTF-8');
System.debug(urlEncodedTimestamp);

下一个示例演示如何使用 HTTP 摘要计算客户端响应 身份验证 (RFC2617)。convertToHex

@isTest
private class SampleTest {
   static testmethod void testConvertToHex() {
      String myData = 'A Test String';
      Blob hash = Crypto.generateDigest('SHA1',Blob.valueOf(myData));
      String hexDigest = EncodingUtil.convertToHex(hash);
      System.debug(hexDigest);  
    } 
}

使用模式和匹配器

Apex 提供模式和匹配器,使您能够使用 正则表达式。

模式是正则表达式的编译表示形式。 匹配器使用模式对字符执行匹配操作 字符串。

正则表达式是用于匹配的字符串 另一个字符串,使用特定语法。Apex 支持通过其 Pattern 和 Matcher 类使用正则表达式。

注意

在 Apex 中,Patterns 和 Matchers 以及正则表达式都是基于 在 Java 中的对应物上。请参见 http://java.sun.com/j2se/1.5.0/docs/api/index.html?java/util/regex/Pattern.html。

许多 Matcher 对象可以共享同一个 Pattern 对象,如下所示 在下图中:

可以从同一个 Pattern 对象创建许多 Matcher 对象A flow chart showing flow from Regular Expression to Matcher object

Apex 中的正则表达式遵循 Java 中使用的正则表达式的标准语法。 任何基于 Java 的正则表达式字符串都可以轻松导入到 您的 Apex 代码。

注意

Salesforce 限制常规输入序列的次数 表达式可以访问 1,000,000 次。如果达到该限制, 收到运行时错误。

所有正则表达式都指定为字符串。最常规 表达式首先编译到 Pattern 对象中:只有 String 方法采用正则表达式 这没有被编译。split通常,在将正则表达式编译为 Pattern 之后 对象,则只需使用 Pattern 对象一次即可创建 Matcher 对象。 然后,使用 Matcher 对象执行所有进一步的操作。为 例:

// First, instantiate a new Pattern object "MyPattern"
Pattern MyPattern = Pattern.compile('a*b');

// Then instantiate a new Matcher object "MyMatcher"
Matcher MyMatcher = MyPattern.matcher('aaaaab');

// You can use the system static method assert to verify the match
System.assert(MyMatcher.matches());

如果只打算使用一次正则表达式,请使用 class 方法编译表达式 并在单个调用中将字符串与其匹配。例如 以下内容等效于上面的代码:Patternmatches

Boolean Test = Pattern.matches('a*b', 'aaaaab');
  • 使用区域
  • 使用匹配操作
  • 使用边界
  • 了解捕获组
  • 模式和匹配器示例

使用区域

Matcher 对象在其输入字符串的子集中查找匹配项 称为区域。Matcher 对象的默认区域 始终是输入字符串的整个部分。但是,您可以更改 使用该方法获取某个区域的起点和终点,您可以查询 使用 和 方法的区域终结点。regionregionStartregionEnd

该方法需要 起始值和结束值。下表提供了示例 如何在不设置另一个值的情况下设置另一个值。region

区域开始区域结束代码示例
显式指定保持不变MyMatcher.region(start, MyMatcher.regionEnd());
保持不变显式指定MyMatcher.region(MyMatcher.regionStart(), end);
重置为默认值显式指定MyMatcher.region(0, end);

使用匹配操作

Matcher 对象对角色执行匹配操作 序列,通过解释 Pattern。

Matcher 对象是通过 Pattern 的方法从 Pattern 实例化的。创建后,匹配器 对象可用于执行以下类型的匹配操作:matcher

  • 将 Matcher 对象的整个输入字符串与模式进行匹配 使用方法matches
  • 将 Matcher 对象的输入字符串与模式匹配,从 在开始时,但不匹配整个区域,使用该方法lookingAt
  • 扫描 Matcher 对象的输入字符串以查找下一个子字符串 与使用方法的模式匹配find

这些方法中的每一个都返回一个指示成功或失败的布尔值。

使用这些方法中的任何一种后,您可以找到更多信息 关于上一个匹配项,即找到的内容,通过使用以下命令 Matcher 类方法:

  • end:一旦匹配 made,此方法返回匹配字符串中 匹配的最后一个字符。
  • start:一旦匹配 made,此方法返回第一个字符串中的位置 匹配的字符。
  • group:一旦匹配 made,此方法返回匹配的子序列。

使用边界

默认情况下,区域由定位边界分隔,这意味着线锚点(例如 或 )在区域边界处匹配,即使区域边界也是如此 已从输入字符串的开头和结尾移开。您可以 指定区域是否对方法使用定位边界。默认情况下, 区域始终使用定位边界。如果设置为 ,则线锚点仅匹配 输入字符串的真正末尾。^$useAnchoringBoundsuseAnchoringBoundsfalse

默认情况下,不会搜索位于区域之外的所有文本, 也就是说,该区域具有不透明的边界。但是,使用透明边界可以搜索外部的文本 一个区域。仅当区域不再使用透明边界时 包含整个输入字符串。您可以指定边界的类型 区域具有使用该方法。useTransparentBounds

假设您正在搜索以下字符串,以及您所在的地区 只是“STRING”这个词:

This is a concatenated STRING of cats and dogs.

如果你搜索“猫”这个词,你不会 接收匹配项,除非您设置了透明边界。

了解捕获组

在匹配操作期间,输入字符串的每个子字符串 与模式匹配的将被保存。这些匹配的子字符串称为捕获组

捕获组通过计算其左括号进行编号 从左到右。例如,在正则表达式字符串中,有四个捕获 组:((A)(B(C)))

  1. ((A)(B(C)))
  2. (A)
  3. (B(C))
  4. (C)

组 0 始终代表整个表达式。

与组关联的捕获输入始终是子字符串 最近匹配的组,即返回的组 Matcher 类匹配操作之一。

如果使用其中一个匹配操作第二次评估组, 如果第二次评估,则保留其先前捕获的值(如果有) 失败。

模式和匹配器示例

Matcher 类方法 返回匹配字符串中最后一个字符之后的位置 那是匹配的。在解析字符串时,您将使用它 并希望在找到匹配项后对其进行其他工作, 比如找到下一个匹配项。end

在正则表达式语法中,表示匹配一次或根本不匹配,表示匹配 1 次或多次。?+

在以下示例中,使用 Matcher 传入的字符串 对象与模式匹配,因为匹配字符串 – 后跟一次。然后它与最后一个匹配 – 然后是根本不匹配。(a(b)?)‘ab’‘a’‘b’‘a’‘a’‘b’

pattern myPattern = pattern.compile('(a(b)?)+'); 
matcher myMatcher = myPattern.matcher('aba');
System.assert(myMatcher.matches() && myMatcher.hitEnd());

// We have two groups: group 0 is always the whole pattern, and group 1 contains 
// the substring that most recently matched--in this case, 'a'. 
// So the following is true:

System.assert(myMatcher.groupCount() == 2 &&
              myMatcher.group(0) == 'aba' && 
              myMatcher.group(1) == 'a');
 
// Since group 0 refers to the whole pattern, the following is true:

System.assert(myMatcher.end() == myMatcher.end(0));

// Since the offset after the last character matched is returned by end, 
// and since both groups used the last input letter, that offset is 3
// Remember the offset starts its count at 0. So the following is also true:

System.assert(myMatcher.end() == 3 && 
              myMatcher.end(0) == 3 && 
              myMatcher.end(1) == 3);

在以下示例中,电子邮件地址被规范化并重复 如果存在不同的顶级域名或子域名,则会报告 对于相似的电子邮件地址。例如,规范化为 。john@fairway.smithcojohn@smithco

class normalizeEmailAddresses{

    public void hasDuplicatesByDomain(Lead[] leads) {
           // This pattern reduces the email address to 'john@smithco' 
           // from 'john@*.smithco.com' or 'john@smithco.*'
        Pattern emailPattern = Pattern.compile('(?<=@)((?![\\w]+\\.[\\w]+$)
                                               [\\w]+\\.)|(\\.[\\w]+$)');
           // Define a set for emailkey to lead:
        Map<String,Lead> leadMap = new Map<String,Lead>();
                for(Lead lead:leads) {
                    // Ignore leads with a null email
                    if(lead.Email != null) {
                           // Generate the key using the regular expression
                       String emailKey = emailPattern.matcher(lead.Email).replaceAll('');
                           // Look for duplicates in the batch
                       if(leadMap.containsKey(emailKey)) 
                            lead.email.addError('Duplicate found in batch');
                       else {
                           // Keep the key in the duplicate key custom field
                            lead.Duplicate_Key__c = emailKey;
                            leadMap.put(emailKey, lead);
                       }
                 }
             }
                // Now search the database looking for duplicates 
                for(Lead[] leadsCheck:[SELECT Id, duplicate_key__c FROM Lead WHERE 
                duplicate_key__c IN :leadMap.keySet()]) {
               for(Lead lead:leadsCheck) {
               // If there's a duplicate, add the error.
                   if(leadMap.containsKey(lead.Duplicate_Key__c)) 
                      leadMap.get(lead.Duplicate_Key__c).email.addError('Duplicate found 
                         in salesforce(Id: ' + lead.Id + ')');
            }
        }
    }
 }

XML 支持

Apex 提供了实用程序类,用于创建和解析 使用流和 DOM 的 XML 内容。

本节包含有关 XML 支持的详细信息。

  • 使用 Streams
    读取和写入 XML Apex 提供了用于使用流读取和写入 XML 内容的类。
  • 使用 DOM
    读取和写入 XML Apex 提供了使您能够使用 DOM(文档对象模型)处理 XML 内容的类。

使用流读取和写入 XML

Apex 提供了用于使用流读取和写入 XML 内容的类。

使用 XMLStreamReader 类可以读取 XML 内容和 XMLStreamWriter 类使您能够编写 XML 内容。

  • 使用流
    读取 XML XMLStreamReader 类方法支持对 XML 数据的正向只读访问。
  • 使用流
    编写 XML XmlStreamWriter 类方法允许写入 XML 数据。

使用流读取 XML

XMLStreamReader 类方法支持对 XML 的正向只读访问 数据。这些方法与 HTTP 标注结合使用,以解析 XML 数据或跳过 不需要的事件。您可以分析最

50节点深度。下面的示例演示如何 实例化新的 XmlStreamReader 对象:

String xmlString = '<books><book>My Book</book><book>Your Book</book></books>';
XmlStreamReader xsr = new XmlStreamReader(xmlString);

这些方法适用于以下 XML 事件:

  • 为特定元素指定属性事件。例如 该元素具有属性:。<book>title<book title=”Salesforce.com for Dummies”>
  • start 元素事件是元素的开始标记,例如 。<book>
  • 结束元素事件是元素的结束标记,例如 。</book>
  • 开始文档事件是文档的开始标记。
  • 结束文档事件是文档的结束标记。
  • 实体引用是代码中的实体引用,例如 。!ENTITY title = “My Book Title”
  • 字符事件是文本字符
  • 注释事件是 XML 文件中的注释。

使用 and 方法循环访问 XML 数据。使用 方法(如 the 方法)访问 XML 中的数据。nexthasNextgetgetNamespace

循环访问 XML 数据时,请始终在调用之前检查流数据是否可用,以避免尝试读取 XML 数据。hasNextnext

XmlStreamReader 示例

下面的示例处理一个 XML 字符串。

public class XmlStreamReaderDemo {

    // Create a class Book for processing
    public class Book {
        String name;
        String author;
    }

    public Book[] parseBooks(XmlStreamReader reader) {
        Book[] books = new Book[0];
        boolean isSafeToGetNextXmlElement = true;
        while(isSafeToGetNextXmlElement) {
            // Start at the beginning of the book and make sure that it is a book
            if (reader.getEventType() == XmlTag.START_ELEMENT) {
                if ('Book' == reader.getLocalName()) {
                    // Pass the book to the parseBook method (below) 
                    Book book = parseBook(reader);
                    books.add(book);
                }
            }
            // Always use hasNext() before calling next() to confirm 
            // that we have not reached the end of the stream
            if (reader.hasNext()) {
                reader.next();
            } else {
                isSafeToGetNextXmlElement = false;
                break;
            }
        }
        return books;
    }

    // Parse through the XML, determine the author and the characters
    Book parseBook(XmlStreamReader reader) {
        Book book = new Book();
        book.author = reader.getAttributeValue(null, 'author');
        boolean isSafeToGetNextXmlElement = true;
        while(isSafeToGetNextXmlElement) {
            if (reader.getEventType() == XmlTag.END_ELEMENT) {
                break;
            } else if (reader.getEventType() == XmlTag.CHARACTERS) {
                book.name = reader.getText();
            }
            // Always use hasNext() before calling next() to confirm 
            // that we have not reached the end of the stream
            if (reader.hasNext()) {
                reader.next();
            } else {
                isSafeToGetNextXmlElement = false;
                break;
            }
        }
        return book;
    }
}
@isTest
private class XmlStreamReaderDemoTest {
    // Test that the XML string contains specific values
    static testMethod void testBookParser() {

        XmlStreamReaderDemo demo = new XmlStreamReaderDemo();

        String str = '<books><book author="Chatty">Alpha beta</book>' +
            '<book author="Sassy">Baz</book></books>';

        XmlStreamReader reader = new XmlStreamReader(str);
        XmlStreamReaderDemo.Book[] books = demo.parseBooks(reader);

        System.debug(books.size());

        for (XmlStreamReaderDemo.Book book : books) {
            System.debug(book);
        }
    }
}

使用流编写 XML

XmlStreamWriter 类方法允许编写 XML 数据。这些方法与 HTTP 标注结合使用以构造 要在标注请求中发送到外部服务的 XML 文档。 下面的示例演示如何实例化新的 XmlStreamReader 对象:

String xmlString = '<books><book>My Book</book><book>Your Book</book></books>';
XmlStreamReader xsr = new XmlStreamReader(xmlString);

XML 编写器方法 例

下面的示例编写一个 XML 文档 并测试其有效性。

此 Hello World 示例需要自定义对象。您可以 您可以自行创建这些代码,或将对象和 Apex 代码下载为非托管包 来自 Salesforce AppExchange。要获取组织中的示例资产,请安装 Apex 教程包。此套餐 还包含装运发票示例的示例代码和对象。

public class XmlWriterDemo {

     public String getXml() {
          XmlStreamWriter w = new XmlStreamWriter();
          w.writeStartDocument(null, '1.0');
          w.writeProcessingInstruction('target', 'data');
          w.writeStartElement('m', 'Library', 'http://www.book.com');
          w.writeNamespace('m', 'http://www.book.com');
          w.writeComment('Book starts here');
          w.setDefaultNamespace('http://www.defns.com');
          w.writeCData('<Cdata> I like CData </Cdata>');
          w.writeStartElement(null, 'book', null);
          w.writedefaultNamespace('http://www.defns.com');
          w.writeAttribute(null, null, 'author', 'Manoj');
          w.writeCharacters('This is my book');
          w.writeEndElement(); //end book
          w.writeEmptyElement(null, 'ISBN', null);
          w.writeEndElement(); //end library
          w.writeEndDocument();
          String xmlOutput = w.getXmlString();
          w.close();
          return xmlOutput;
        }
}
@isTest
private class XmlWriterDemoTest {
    static TestMethod void basicTest() {
        XmlWriterDemo demo = new XmlWriterDemo();
        String result = demo.getXml();
        String expected = '<?xml version="1.0"?><?target data?>' +
            '<m:Library xmlns:m="http://www.book.com">' + 
            '<!--Book starts here-->' +
            '<![CDATA[<Cdata> I like CData </Cdata>]]>' +
'<book xmlns="http://www.defns.com" author="Manoj">This is my book</book><ISBN/></m:Library>';
        
        System.assert(result == expected);
    }
}

使用 DOM 读取和写入 XML

Apex 提供了使您能够使用 DOM 处理 XML 内容的类 (文档对象模型)。

DOM 类可帮助您解析或生成 XML 内容。您可以使用这些类来工作 替换为任何 XML 内容。一个常见的应用是使用类来生成正文 由 HttpRequest 创建的请求或解析 HttpResponse 访问的响应。The DOM 将 XML 文档表示为节点层次结构。某些节点可能是分支节点 并且有子节点,而其他节点是没有子节点的叶节点。您可以 分析嵌套的 XML 内容,该内容最多50节点 深。

DOM 类包含在命名空间中。Dom

使用 Document 类处理 XML 文档正文中的内容。

使用 XmlNode 类处理 XML 中的节点 公文。

使用 Document 类类处理 XML 内容。一个常见的应用是使用它 为 HttpRequest 创建请求的正文,或分析 HttpResponse 访问的响应。

XML 命名空间

XML 命名空间是由 URI 引用标识的名称集合,用于 XML 文档,用于唯一标识元素类型和属性名称。XML 中的名称 命名空间可能显示为限定名称,其中包含一个冒号,分隔 名称转换为命名空间前缀和本地部分。前缀,映射到 URI 引用,选择命名空间。通用管理 URI 的组合 命名空间和文档自己的命名空间生成通用的标识符 独特。

以下 XML 元素的命名空间和前缀为 。http://my.name.spacemyprefix

<sampleElement xmlns:myprefix="http://my.name.space" />

在下面的示例中,XML 元素具有两个属性:

  • 第一个属性的键为 ; 值为 。dimension2
  • 第二个属性的键命名空间为 http://ns1;这 value 命名空间为 http://ns2;关键是;值为 。exampletest
<square dimension="2" ns1:example="ns2:test" xmlns:ns1="http://ns1" xmlns:ns2="http://ns2" />

公文例

对于下面的示例,假定传递到方法中的参数返回此 XML 响应:urlparseResponseDom

<address>
    <name>Kirk Stevens</name>
    <street1>808 State St</street1>
    <street2>Apt. 2</street2>
    <city>Palookaville</city>
    <state>PA</state>
    <country>USA</country>
</address>

下面的示例演示如何使用 DOM 类来分析 XML 响应 在请求正文中返回:GET

public class DomDocument {
 
    // Pass in the URL for the request
    // For the purposes of this sample,assume that the URL
    // returns the XML shown above in the response body
    public void parseResponseDom(String url){
        Http h = new Http();
        HttpRequest req = new HttpRequest();
        // url that returns the XML in the response body
        req.setEndpoint(url);
        req.setMethod('GET');
        HttpResponse res = h.send(req);
        Dom.Document doc = res.getBodyDocument();
        
        //Retrieve the root element for this document.
        Dom.XMLNode address = doc.getRootElement();
        
        String name = address.getChildElement('name', null).getText();
        String state = address.getChildElement('state', null).getText();
        // print out specific elements
        System.debug('Name: ' + name);
        System.debug('State: ' + state);
        
        // Alternatively, loop through the child elements.
        // This prints out all the elements of the address
        for(Dom.XMLNode child : address.getChildElements()) {
           System.debug(child.getText());
        }
    }
}

使用 XML 节点

使用该类处理 XML 文档。DOM 将 XML 文档表示为节点的层次结构。一些 节点可以是分支节点并具有子节点,而其他节点是没有子节点的叶节点 孩子。XmlNode

Apex 中有不同类型的 DOM 节点可用。 是这些不同类型的枚举。 这些值为:XmlNodeType

  • 评论
  • 元素
  • 发短信

区分 XML 文档中的元素和节点非常重要。这 下面是一个简单的 XML 示例:

<name>
    <firstName>Suvain</firstName>
    <lastName>Singh</lastName>
</name>

此示例包含三个 XML 元素:、 和 。它包含五个节点:三个节点 、 和 元素 节点,以及两个文本节点 – 和 .请注意,元素中的文本 节点被视为单独的文本节点。namefirstNamelastNamenamefirstNamelastNameSuvainSingh

有关所有枚举共享的方法的详细信息,请参阅枚举方法。

XmlNode的例

此示例演示如何使用方法 和命名空间来创建 XML 请求。XmlNode

public class DomNamespaceSample
{
    public void sendRequest(String endpoint)
    {
        // Create the request envelope
        DOM.Document doc = new DOM.Document();
        
        String soapNS = 'http://schemas.xmlsoap.org/soap/envelope/';
        String xsi = 'http://www.w3.org/2001/XMLSchema-instance';
        String serviceNS = 'http://www.myservice.com/services/MyService/';
        
        dom.XmlNode envelope
            = doc.createRootElement('Envelope', soapNS, 'soapenv');
        envelope.setNamespace('xsi', xsi);
        envelope.setAttributeNS('schemaLocation', soapNS, xsi, null);
        
        dom.XmlNode body
            = envelope.addChildElement('Body', soapNS, null);
        
        body.addChildElement('echo', serviceNS, 'req').
           addChildElement('category', serviceNS, null).
           addTextNode('classifieds');
        
        System.debug(doc.toXmlString());
        
        // Send the request
        HttpRequest req = new HttpRequest();
        req.setMethod('POST');
        req.setEndpoint(endpoint);
        req.setHeader('Content-Type', 'text/xml');
        
        req.setBodyDocument(doc);
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        System.assertEquals(200, res.getStatusCode());
        
        dom.Document resDoc = res.getBodyDocument();
        
        envelope = resDoc.getRootElement();
        
        String wsa = 'http://schemas.xmlsoap.org/ws/2004/08/addressing';
        
        dom.XmlNode header = envelope.getChildElement('Header', soapNS);
        System.assert(header != null);
        
        String messageId
            = header.getChildElement('MessageID', wsa).getText();
        
        System.debug(messageId);
        System.debug(resDoc.toXmlString());
        System.debug(resDoc);
        System.debug(header);
        
        System.assertEquals(
         'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous',
         header.getChildElement(
           'ReplyTo', wsa).getChildElement('Address', wsa).getText());
        
        
        System.assertEquals(
          envelope.getChildElement('Body', soapNS).
              getChildElement('echo', serviceNS).
              getChildElement('something', 'http://something.else').
              getChildElement(
                'whatever', serviceNS).getAttribute('bb', null),
                'cc');
        
        System.assertEquals('classifieds',
          envelope.getChildElement('Body', soapNS).
              getChildElement('echo', serviceNS).
              getChildElement('category', serviceNS).getText());
    }
}

JSON 支持

Apex 中的 JavaScript 对象表示法 (JSON) 支持可实现 Apex 的序列化 对象转换为JSON格式,并对序列化的JSON内容进行反序列化。Apex 提供了一组类,用于公开 JSON 序列化的方法和 反序列化。下表描述了可用的类。

描述
System.JSON包含用于将 Apex 对象序列化为 JSON 格式的方法,以及 反序列化使用此类中的方法序列化的 JSON 内容。serialize
System.JSONGenerator包含用于使用标准将对象序列化为 JSON 内容的方法 JSON 编码。
System.JSONParser表示 JSON 编码内容的分析器。

枚举包含标记 用于 JSON 解析。System.JSONToken

这些类中的方法会抛出一个 if 在执行过程中遇到问题。JSONExceptionJSON 支持注意事项

  • JSON 序列化和反序列化支持可用于 sObjects(标准 对象和自定义对象)、Apex 原语和集合类型、返回类型 数据库方法(例如 SaveResult 和 DeleteResult)和 Apex 实例 类。
  • 只有托管包的自定义对象(类型)才能从以下代码进行序列化: 托管包的外部。作为 中定义的 Apex 类实例的对象 无法序列化托管包。sObject
  • 仅当 Map 对象使用 以下数据类型作为键。
    • 布尔
    • 日期
    • 日期时间
    • 十进制
    • 枚举
    • 同上
    • 整数
    • 字符串
    • 时间
  • 当对象被声明为父类型,但被设置为 子类型,某些数据可能会丢失。该对象被序列化和反序列化为 父类型和特定于子类型的任何字段都将丢失。
  • 具有自身引用的对象不会被序列化,并导致抛出。JSONException
  • 两次引用同一对象的引用图将被反序列化并导致 要生成的引用对象的多个副本。
  • 数据类型不是 序列 化。如果尝试创建可序列化类的实例,例如 Visualforce 控制器,其成员变量类型为 ,您会收到异常。若要在可序列化类中使用,请使用 local 变量。System.JSONParserSystem.JSONParserJSONParser

版本化行为更改

在 API 版本 53.0 及更高版本中,DateTime 格式和处理已更新。The API 正确处理 JSON 请求中使用 3 位以上数字的 JSON请求中的DateTime值 小数点。使用不支持的 DateTime 格式(如 )的请求会导致错误。Salesforce 建议您 严格遵守 https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_valid_date_formats.htm 中指定的 DateTime 格式。123456000

  • 往返序列化和反序列化
    使用类方法对 JSON 内容执行往返序列化和反序列化。通过这些方法,可以将对象序列化为 JSON 格式的字符串,并将 JSON 字符串反序列化回对象。JSON
  • JSON 生成器
    使用类方法,可以生成标准的 JSON 编码内容。JSONGenerator
  • JSON 解析
    使用类方法解析 JSON 编码的内容。通过这些方法,可以分析从对外部服务(如 Web 服务标注)的调用返回的 JSON 格式的响应。JSONParser

往返序列化和反序列化

使用类方法执行往返 JSON 内容的序列化和反序列化。这些方法使您能够 将对象序列化为 JSON 格式的字符串,并将 JSON 字符串反序列化回 对象。

JSON

示例:序列化和反序列化发票列表

此示例创建一个对象列表并序列化该列表。接下来,序列化 JSON 字符串用于再次反序列化列表,示例验证 新列表包含与原始列表中相同的发票 列表。

InvoiceStatement

public class JSONRoundTripSample {
  
    public class InvoiceStatement {
        Long invoiceNumber;
        Datetime statementDate;
        Decimal totalPrice;
        
        public InvoiceStatement(Long i, Datetime dt, Decimal price)
        {
            invoiceNumber = i;
            statementDate = dt;
            totalPrice = price;
        }
    }
    
    public static void SerializeRoundtrip() {
        Datetime dt = Datetime.now(); 
        // Create a few invoices.
        InvoiceStatement inv1 = new InvoiceStatement(1,Datetime.valueOf(dt),1000);
        InvoiceStatement inv2 = new InvoiceStatement(2,Datetime.valueOf(dt),500);
        // Add the invoices to a list.
        List<InvoiceStatement> invoices = new List<InvoiceStatement>();
        invoices.add(inv1);
        invoices.add(inv2);
              
        // Serialize the list of InvoiceStatement objects.
        String JSONString = JSON.serialize(invoices);
        System.debug('Serialized list of invoices into JSON format: ' + JSONString);
        
        // Deserialize the list of invoices from the JSON string.
        List<InvoiceStatement> deserializedInvoices = 
          (List<InvoiceStatement>)JSON.deserialize(JSONString, List<InvoiceStatement>.class);
        System.assertEquals(invoices.size(), deserializedInvoices.size());
        Integer i=0;
        for (InvoiceStatement deserializedInvoice :deserializedInvoices) {
            system.debug('Deserialized:' + deserializedInvoice.invoiceNumber + ',' 
            + deserializedInvoice.statementDate.formatGmt('MM/dd/yyyy  HH:mm:ss.SSS')
            + ', ' + deserializedInvoice.totalPrice); 
            system.debug('Original:' + invoices[i].invoiceNumber + ',' 
            + invoices[i].statementDate.formatGmt('MM/dd/yyyy  HH:mm:ss.SSS') 
            + ', ' + invoices[i].totalPrice); 
            i++;
        }
    }
}

JSON 序列化注意事项

该方法的行为不同 取决于保存的 Apex 代码的 Salesforce API 版本。serialize使用设置其他字段的查询 sObject 的序列化对于使用 Salesforce API 版本 27.0 及更早版本保存的 Apex,如果查询 sObjects 设置了其他字段,这些字段不包含在 方法返回的序列化 JSON 字符串。从使用 Salesforce 保存的 Apex 开始 API 版本 28.0,其他字段包含在序列化的 JSON 中 字符串。serialize本示例在查询联系人后向该联系人添加一个字段,然后 序列化联系人。断言语句验证 JSON string 包含附加字段。断言传递 Apex 保存 使用 Salesforce API 版本 28.0 和 后。

Contact con = [SELECT Id, LastName, AccountId FROM Contact LIMIT 1]; 
// Set additional field
con.FirstName = 'Joe'; 
String jsonstring = Json.serialize(con); 
System.debug(jsonstring); 
System.assert(jsonstring.contains('Joe') == true);

聚合查询结果字段的序列化对于使用 Salesforce API 版本 27.0 保存的 Apex,聚合结果 在以下情况下,查询不包括 SELECT 语句中的字段 使用该方法序列化。对于早期 API 版本或 API 版本 28.0 及更高版本, 序列化聚合查询结果包括 SELECT 中的所有字段 陈述。serialize此聚合查询返回两个字段:ID 字段计数和 帐户名称。

String jsonString = JSON.serialize(
    Database.query('SELECT Count(Id),Account.Name FROM Contact WHERE Account.Name != null GROUP BY Account.Name LIMIT 1'));
    System.debug(jsonString);

// Expected output in API v 26 and earlier or v28 and later
// [{"attributes":{"type":"AggregateResult"},"expr0":2,"Name":"acct1"}]

空字段的序列化从 API 版本 28.0 开始,null 字段不会序列化,也不会 包含在 JSON 字符串中,这与早期版本不同。此更改不会 影响使用 JSON 方法(例如 Json.deserialize())反序列化 JSON 字符串。此更改是 检查 JSON 字符串时会注意到这一点。为 例:

String jsonString = JSON.serialize(
                 [SELECT Id, Name, Website FROM Account WHERE Website = null LIMIT 1]);
System.debug(jsonString);

// In v27.0 and earlier, the string includes the null field and looks like the following.
// {"attributes":{...},"Id":"001D000000Jsm0WIAR","Name":"Acme","Website":null}

// In v28.0 and later, the string doesn’t include the null field and looks like 
//  the following.
// {"attributes":{...},"Name":"Acme","Id":"001D000000Jsm0WIAR"}}

ID 序列化在 API 版本 34.0 及更早版本中,对于已通过 往返 JSON 序列化和反序列化。==

JSON 反序列化注意事项

聚合结果中的 JSON 无法反序列化回 Apex AggregateResult 对象,因为它们没有命名字段。

JSON生成器

使用类方法,可以生成标准的 JSON 编码内容。

JSONGenerator

您可以使用标准逐个元素构造 JSON 内容 JSON 编码。为此,请使用类中的方法。JSONGenerator

JSONGenerator 示例

此示例生成 使用类的方法以漂亮的打印格式创建 JSON 字符串。首先举个例子 添加一个数字字段和一个字符串字段,然后添加一个要包含的字段 整数列表的 object 字段,该字段已正确反序列化。 接下来,它添加对象 进入现场,其中 也会被反序列化。JSONGeneratorAObject A

public class JSONGeneratorSample{

    public class A { 
        String str;
        
        public A(String s) { str = s; }
    }

    static void generateJSONContent() {
        // Create a JSONGenerator object.
        // Pass true to the constructor for pretty print formatting.
        JSONGenerator gen = JSON.createGenerator(true);
        
        // Create a list of integers to write to the JSON string.
        List<integer> intlist = new List<integer>();
        intlist.add(1);
        intlist.add(2);
        intlist.add(3);
        
        // Create an object to write to the JSON string.
        A x = new A('X');
        
        // Write data to the JSON string.
        gen.writeStartObject();
        gen.writeNumberField('abc', 1.21);
        gen.writeStringField('def', 'xyz');
        gen.writeFieldName('ghi');
        gen.writeStartObject();
        
        gen.writeObjectField('aaa', intlist);
        
        gen.writeEndObject();
        
        gen.writeFieldName('Object A');
        
        gen.writeObject(x);
        
        gen.writeEndObject();
        
        // Get the JSON string.
        String pretty = gen.getAsString();
        
        System.assertEquals('{\n' +
        '  "abc" : 1.21,\n' +
        '  "def" : "xyz",\n' +
        '  "ghi" : {\n' +
        '    "aaa" : [ 1, 2, 3 ]\n' +
        '  },\n' +
        '  "Object A" : {\n' +
        '    "str" : "X"\n' +
        '  }\n' +
        '}', pretty);
    }
}

JSON解析

使用类方法进行解析 JSON 编码的内容。这些方法使你能够分析 JSON 格式的响应,该响应是 从对外部服务(如 Web 服务标注)的调用返回。

JSONParser

以下示例演示如何分析 JSON 字符串。

示例:解析来自 Web 服务标注的 JSON 响应

此示例使用方法分析 JSON 格式的响应。它对返回 JSON 格式的响应。接下来,解析响应以从 api 构建映射 版本号添加到版本标签中。JSONParser

public class JSONParserUtil {
    public static void parseJSONResponse() {        
        
        // Create HTTP request to send.
        HttpRequest request = new HttpRequest();
        // Set the endpoint URL.
        String endpoint = URL.getOrgDomainUrl().toExternalForm() + '/services/data';
        request.setEndPoint(endpoint);
        // Set the HTTP verb to GET.
        request.setMethod('GET');
        // Set the request header for JSON content type
        request.setHeader('Accept', 'application/json');
        
        // Send the HTTP request and get the response.
        // The response is in JSON format.
        Http httpProtocol = new Http();
        HttpResponse response = httpProtocol.send(request);
        System.debug(response.getBody());
        /* The JSON response returned is the following:
            {"label":"Summer '14","url":"/services/data/v31.0","version":"31.0"},
            {"label":"Winter '15","url":"/services/data/v32.0","version":"32.0"},
            {"label":"Spring '15","url":"/services/data/v33.0","version":"33.0"},
        */
        // Parse JSON response to build a map from API version numbers to labels
        JSONParser parser = JSON.createParser(response.getBody());
        Map<double, string> apiVersionToReleaseNameMap = new Map<double, string>();
        
        string label = null;
        double version = null;
        
        while (parser.nextToken() != null) {
            
            if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
                switch on parser.getText() {
                    when 'label' {
                    // Advance to the label value.
                    parser.nextToken();
                        label = parser.getText();
                    }
                    when 'version' {
                        // Advance to the version value.
                        parser.nextToken();
                        version = Double.valueOf(parser.getText());
                    }
                }
            }
            
            if(version != null && String.isNotEmpty(label)) {
                apiVersionToReleaseNameMap.put(version, label);
                version = null;
                label = null; 
            }
        }
        system.debug('Release with Rainbow logo = ' +
            apiVersionToReleaseNameMap.get(39.0D));
    }
}

示例:解析 JSON 字符串并将其反序列化为对象

此示例使用硬编码的 JSON 字符串,该字符串与 返回的 JSON 字符串相同 上一示例中的标注。在此示例中,将分析整个字符串 使用该方法添加到对象中。 此代码还使用 跳过子数组和子对象,并解析 列表。分析的对象是定义为内部类的类的实例。因为每张发票 包含行项,表示相应行项类型的类,该类,也被定义为 内部类。将此示例代码添加到要使用的类中 它。

InvoicereadValueAsskipChildrenInvoiceLineItem

public static void parseJSONString() {
    String jsonStr = 
        '{"invoiceList":[' +
        '{"totalPrice":5.5,"statementDate":"2011-10-04T16:58:54.858Z","lineItems":[' +
            '{"UnitPrice":1.0,"Quantity":5.0,"ProductName":"Pencil"},' +
            '{"UnitPrice":0.5,"Quantity":1.0,"ProductName":"Eraser"}],' +
                '"invoiceNumber":1},' +
        '{"totalPrice":11.5,"statementDate":"2011-10-04T16:58:54.858Z","lineItems":[' +
            '{"UnitPrice":6.0,"Quantity":1.0,"ProductName":"Notebook"},' +
            '{"UnitPrice":2.5,"Quantity":1.0,"ProductName":"Ruler"},' +
            '{"UnitPrice":1.5,"Quantity":2.0,"ProductName":"Pen"}],"invoiceNumber":2}' +
        ']}';

    // Parse entire JSON response.
    JSONParser parser = JSON.createParser(jsonStr);
    while (parser.nextToken() != null) {
        // Start at the array of invoices.
        if (parser.getCurrentToken() == JSONToken.START_ARRAY) {
            while (parser.nextToken() != null) {
                // Advance to the start object marker to
                //  find next invoice statement object.
                if (parser.getCurrentToken() == JSONToken.START_OBJECT) {
                    // Read entire invoice object, including its array of line items.
                    Invoice inv = (Invoice)parser.readValueAs(Invoice.class);
                    system.debug('Invoice number: ' + inv.invoiceNumber);
                    system.debug('Size of list items: ' + inv.lineItems.size());
                    // For debugging purposes, serialize again to verify what was parsed.
                    String s = JSON.serialize(inv);
                    system.debug('Serialized invoice: ' + s);

                    // Skip the child start array and start object markers.
                    parser.skipChildren();
                }
            }
        }
    }
} 

// Inner classes used for serialization by readValuesAs(). 

public class Invoice {
    public Double totalPrice;
    public DateTime statementDate;
    public Long invoiceNumber;
    List<LineItem> lineItems;
    
    public Invoice(Double price, DateTime dt, Long invNumber, List<LineItem> liList) {
        totalPrice = price;
        statementDate = dt;
        invoiceNumber = invNumber;
        lineItems = liList.clone();
    }
}  

public class LineItem {
    public Double unitPrice;
    public Double quantity;
    public String productName;
}