集成和 Apex 实用程序

Apex 允许您使用标注与外部 SOAP 和 REST Web 服务集成。 您可以使用用于 JSON、XML、数据安全和编码的实用程序。通用实用程序 对于带有文本字符串的正则表达式,也提供了。

  • 使用 Apex 调用标注
  • JSON 支持 Apex 中的 JavaScript 对象表示法 (JSON) 支持
    将 Apex 对象序列化为 JSON 格式,并对序列化的 JSON 内容进行反序列化。
  • XML Support
    Apex 提供实用程序类,这些类支持使用流和 DOM 创建和解析 XML 内容。
  • 保护数据
    可以使用类提供的方法保护数据。Crypto
  • 对数据
    进行编码 可以使用类提供的方法对 URL 进行编码和解码,并将字符串转换为十六进制格式。EncodingUtil
  • 使用模式和匹配器 Apex 提供了模式和匹配器
    ,使您能够使用正则表达式搜索文本。

使用 Apex 调用标注

借助 Apex 标注,您可以通过以下方式将 Apex 与外部服务紧密集成 调用外部 Web 服务或从 Apex 代码发送 HTTP 请求,然后接收 响应。Apex 提供与利用 SOAP 和 WSDL 或 HTTP 的 Web 服务的集成 服务(RESTful 服务)。

注意

在任何 Apex 标注可以调用外部站点之前,该站点 必须在“远程站点设置”页中注册,否则标注将失败。Salesforce的 防止调用未经授权的网络地址。

如果标注指定了命名 凭据作为终结点,则无需配置远程站点设置。一个命名的 credential 指定标注端点的 URL 及其所需的身份验证 一个定义中的参数。若要设置命名凭据,请参阅“定义命名凭据” 在 Salesforce 帮助中。

若要详细了解标注类型,请参阅:

  • SOAP 服务:从 WSDL 文档定义类
  • 调用 HTTP 标注
  • 异步 长时间运行的请求的标注

提示

标注使 Apex 能够调用外部 Web 或 HTTP 服务。Apex Web 服务允许外部应用程序调用 Apex 方法 通过 Web 服务。

  1. 添加远程站点设置
  2. 命名凭据作为标注端点 命名凭据在一个定义中指定标注端点
    的 URL 及其所需的身份验证参数。Salesforce 管理将指定命名凭据作为标注端点的 Apex 标注的所有身份验证,以便您的代码不必这样做。对于命名凭据中定义的站点,还可以跳过远程站点设置,否则,外部站点的标注将需要这些设置。
  3. SOAP 服务:从 WSDL 文档定义类
  4. 调用 HTTP 标注
  5. 使用证书
  6. 标注限制和局限性
  7. 使用延续创建长时间运行的标注
    使用异步标注从 Visualforce 页面或 Lightning 组件向外部 Web 服务发出长时间运行的请求,并在回调方法中处理响应。

添加远程站点设置

在任何 Apex 标注可以调用外部站点之前,该站点 必须在“远程站点设置”页中注册,否则标注将失败。Salesforce的 防止调用未经授权的网络地址。

注意

如果标注指定命名凭据作为终结点,则无需配置 远程站点设置。命名凭据指定标注端点的 URL 及其 一个定义中所需的身份验证参数。若要设置命名凭据,请参阅“定义” 命名凭据”。

若要添加远程站点设置,请执行以下操作:

  1. 在“设置”中,输入“快速” “查找”框,然后选择“远程站点设置”。Remote Site Settings
  2. 单击“新建远程站点”。
  3. 输入“远程站点名称”的描述性术语。
  4. 输入远程站点的 URL。
  5. (可选)输入站点的描述。
  6. 点击保存

提示

为了获得最佳性能,请验证远程 HTTPS 加密站点是否具有 OCSP (联机证书状态协议)装订已打开。

作为标注端点的命名凭据

命名凭据指定标注端点的 URL 及其所需的 URL 一个定义中的身份验证参数。Salesforce 管理 Apex 的所有身份验证 将命名凭据指定为标注终结点的标注,以便代码没有 自。您还可以跳过远程站点设置,否则,外部标注将需要这些设置 sites,用于命名凭据中定义的站点。

命名凭据还包括可用于路由的 OutboundNetworkConnection 字段 通过专用连接进行标注。通过将端点 URL 和身份验证与 标注定义、命名凭据使标注更易于维护。例如,如果 终结点 URL 更改时,仅更新命名凭据。引用命名的所有标注 凭据只是继续工作。

如果您有多个组织,则可以创建具有相同名称但具有 每个组织中不同的端点 URL。然后,您可以在所有 orgs – 一个标注定义,引用这些命名凭据的共享名称。 例如,每个组织中的命名凭据可以具有不同的终结点 URL 来容纳 开发和生产环境的差异。如果 Apex 标注指定了共享的 这些命名凭据的名称,定义标注的 Apex 类可以打包,并且 部署在所有这些组织上,而无需以编程方式检查环境。

若要从标注定义中引用命名凭据,请使用命名凭据 URL。一个 命名凭证 URL 包含方案、名称 的命名凭据和可选路径。例如:。callout:callout:My_Named_Credential/some_path

您可以将查询字符串追加到命名凭据 URL。使用问号 (?) 作为命名凭据 URL 之间的分隔符 和查询字符串。例如:。callout:My_Named_Credential/some_path?format=json

在以下 Apex 代码中,命名凭据和附加路径指定标注的 端点。

HttpRequest req = new HttpRequest();
req.setEndpoint('callout:My_Named_Credential/some_path');
req.setMethod('GET');
Http http = new Http();
HTTPResponse res = http.send(req);
System.debug(res.getBody());

引用的命名凭据指定终结点 URL 和外部凭据 指定身份验证设置。

命名凭据详细信息页

无论您使用哪种身份验证,Apex 代码都保持不变。这 外部凭据中的身份验证设置不同,外部凭据引用身份验证 组织中定义的提供程序。

具有 OAuth 选项的命名凭据身份验证设置

在 相比之下,让我们看看没有命名凭据的 Apex 代码是什么样子的。请注意, 代码处理身份验证变得更加复杂,即使我们坚持使用基本密码 认证。编码 OAuth 甚至更加复杂,是命名 凭据。

HttpRequest req = new HttpRequest();
req.setEndpoint('https://my_endpoint.example.com/some_path');
req.setMethod('GET');

// Because we didn't set the endpoint as a named credential, 
// our code has to specify:
// - The required username and password to access the endpoint
// - The header and header information
 
String username = 'myname';
String password = 'mypwd';
  
Blob headerValue = Blob.valueOf(username + ':' + password);
String authorizationHeader = 'BASIC ' +
EncodingUtil.base64Encode(headerValue);
req.setHeader('Authorization', authorizationHeader);
   
// Create a new http object to send the request object
// A response object is generated as a result of the request  
  
Http http = new Http();
HTTPResponse res = http.send(req);
System.debug(res.getBody());
  1. 使用命名凭据
    的 Apex 标注的自定义标头和正文 Salesforce 为命名凭据定义的端点的每个标注生成一个标准授权标头,但您可以禁用此选项。您的 Apex 代码还可以使用合并字段来构造每个标注的 HTTP 标头和正文。
  2. 使用命名凭证
    的 Apex 标注的合并字段 要构造 HTTP 标头并向指定为命名凭证的端点发出标注正文,请在 Apex 代码中使用这些合并字段。

使用 named 的 apex 标注的自定义标头和正文 凭据

Salesforce 为每个标注生成一个标准授权标头,以 named-credential-defined 端点,但您可以禁用此选项。您的 Apex 代码也可以使用 merge 字段来构造每个标注的 HTTP 标头和正文。

这种灵活性使您能够在特殊情况下使用命名凭据。例如,一些 远程终结点需要在请求标头中使用安全令牌或加密凭据。一些偏远的 终结点需要 XML 或 JSON 消息正文中的用户名和密码。自定义标注 标题和正文。

Salesforce 管理员必须设置命名凭据,以允许 Apex 代码构造标头 或在 HTTP 标头或正文中使用合并字段。下表描述了这些标注 命名凭据的选项。

描述
生成授权 页眉默认情况下,Salesforce 会生成一个授权标头,并将其应用于每个标注 引用命名凭据。仅当出现以下情况之一时才取消选择此选项 声明适用。远程终结点不支持授权标头。授权标头通过其他方式提供。例如,在 Apex 标注中, 开发人员可以让代码为每个 标注。如果从外部引用命名凭据,则此选项是必需的 数据源。
允许在 HTTP 中合并字段 页眉允许合并 HTTP 正文中的字段在每个 Apex 标注中,代码指定 HTTP 标头和请求正文的 构建。例如,Apex 代码可以在授权中设置 cookie 的值 页眉。这些选项使 Apex 代码能够使用合并字段来填充 HTTP 标头 并在进行标注时请求带有组织数据的正文。这些选项不是 如果从外部数据源引用命名凭据,则可用。

合并使用命名凭证的 Apex 标注的字段

构造 HTTP 标头和指向端点的标注正文 指定为命名凭据,请在 Apex 代码中使用这些合并字段。

合并字段描述
{!$Credential.Username}{!$Credential.Password}正在运行的用户的用户名和密码。仅当命名凭据使用 密码 认证。// non-standard authentication req.setHeader('X-Username', '{!$Credential.Username}'); req.setHeader('X-Password', '{!$Credential.Password}');
{!$Credential.OAuthToken}正在运行的用户的 OAuth 令牌。仅当命名凭据使用 OAuth 时才可用 认证。req.setHeader('Authorization', '{!$Credential.OAuthToken}');
{!$Credential.AuthorizationMethod}有效值取决于 命名凭据的身份验证协议。Basic—密码验证Bearer– OAuth 2.0null– 无身份验证
{!$Credential.AuthorizationHeaderValue}有效值取决于命名凭据的身份验证协议。Base-64 encoded username and password—密码验证OAuth token– OAuth 2.0null– 无身份验证
{!$Credential.OAuthConsumerKey}使用者密钥。仅当命名凭据使用 OAuth 身份验证时才可用。

注意

  • 在标注的 HTTP 请求正文中使用这些合并字段时,可以应用公式函数来转义特殊字符。 不支持其他公式函数,并且不能用于 HTTP 标头中的合并字段。以下示例对特殊字符进行转义 在 凭据。HTMLENCODEHTMLENCODEreq.setBody('Username:{!HTMLENCODE($Credential.Username)}') req.setBody('Password:{!HTMLENCODE($Credential.Password)}')
  • 在 SOAP API 调用中使用这些合并字段时,OAuth 访问令牌不会 刷新。

SOAP 服务:从 WSDL 文档定义类

可以从 WSDL 文档自动生成类,该文档具有 存储在本地硬盘驱动器或网络上。通过使用 WSDL 创建类 文档允许开发人员在其 Apex 代码中对外部 Web 服务进行标注。

注意

如果可能,请使用出站消息传递来处理集成解决方案。使用标注可以 仅在必要时才使用第三方 Web 服务。

要从 WSDL 生成 Apex 类,请执行以下操作:

  1. 在应用程序中,从“设置”中输入“快速查找”框,然后选择“Apex 类”。Apex Classes
  2. 单击“从 WSDL 生成”。
  3. 单击 Browse 导航到本地硬盘上的 WSDL 文档 驱动器或网络,或键入完整路径。此 WSDL 文档是 Apex 的基础 您正在创建的类。注意您指定的 WSDL 文档可能包含一个 SOAP 端点位置,该位置 引用出站端口。出于安全原因,Salesforce 限制了出站端口 您可以指定以下选项之一:
    • 80:此端口仅接受 HTTP 连接。
    • 443:此端口仅接受 HTTPS 连接。
    • 1024–66535(含):这些端口接受 HTTP 或 HTTPS 连接。
  4. 单击解析 WSDL 以验证 WSDL 文档内容。这 应用程序为 WSDL 文档中的每个名称空间生成一个缺省类名,并且 报告任何错误。如果 WSDL 包含以下模式类型或构造,则解析将失败 不受 Apex 类支持,或者如果生成的类超过 100 万个字符 对 Apex 类的限制。例如,无法解析 Salesforce SOAP API WSDL。
  5. 根据需要修改类名。虽然您可以保存多个 通过对每个命名空间 Apex 使用相同的类名,将 WSDL 命名空间转换为单个类 类的总长度不能超过 100 万个字符。
  6. 单击 Generate Apex。向导的最后一页显示哪个 已成功生成类,以及来自其他类的任何错误。该页面还 提供用于查看成功生成的代码的链接。

成功生成的 Apex 类包括用于调用 由 WSDL 文档表示的第三方 Web 服务。这些类允许您调用 来自 Apex 的外部 Web 服务。对于每个生成的类,将创建第二个类,并使用 同名且前缀为 .第一类是 用于同步标注。第二类用于异步标注。更多信息 关于异步标注,请参阅使用延续创建长时间运行的标注。Async

关于生成的 Apex,请注意以下几点:

  • 如果 WSDL 文档包含 Apex 保留字,则在生成 Apex 类时会追加该字。例如,在 WSDL 文档中转换为在生成的 Apex 类中。请参阅保留关键字。详情请见 处理 WSDL 中 Apex 变量不支持的元素名称中的字符 名称,请参阅使用 WSDL 的注意事项。_xlimitlimit_x
  • 如果 WSDL 中的操作具有包含多个元素的输出消息,那么 生成的 Apex 将元素包装在内部类中。表示 WSDL 操作返回内部类,而不是单个元素。
  • 由于 Apex 类中不允许使用句点 () names,则用于生成 Apex 类的 WSDL 名称中的任何句点都将替换为下划线 () 在生成的 Apex 代码中。._

从 WSDL 生成类后,可以调用外部服务 由 WSDL 引用。

注意

在使用本主题其余部分中的示例之前,必须从生成的 WSDL2Apex 代码中复制 Apex 类,并将其添加到 组织。docSampleClass

调用外部服务

在使用外部服务的 WSDL 文档后调用外部服务,以执行以下操作 生成一个 Apex 类,在 Apex 代码中创建存根的实例,然后调用 方法。例如,要从 Apex 调用 StrikeIron IP 地址查找服务,您可以 编写类似于以下内容的代码:

  // Create the stub
  strikeironIplookup.DNSSoap dns = new strikeironIplookup.DNSSoap();

  // Set up the license header
  dns.LicenseInfo = new strikeiron.LicenseInfo();
  dns.LicenseInfo.RegisteredUser = new strikeiron.RegisteredUser();
  dns.LicenseInfo.RegisteredUser.UserID = 'you@company.com';
  dns.LicenseInfo.RegisteredUser.Password = 'your-password';

  // Make the Web service call
  strikeironIplookup.DNSInfo info = dns.DNSLookup('www.myname.com');

HTTP 标头支持

可以在 Web 服务标注上设置 HTTP 标头。例如,您可以使用此功能 在授权标头中设置 Cookie 的值。要设置 HTTP 标头,请将 和 添加到存根。inputHttpHeaders_xoutputHttpHeaders_x

注意

在 API 版本 16.0 及更早版本中,HTTP 响应 标注始终使用 UTF-8 进行解码,而不考虑 Content-Type 标头。在 API 中 版本 17.0 及更高版本中,HTTP 响应使用 Content-Type 标头。

以下示例使用生成的 WSDL2Apex 代码中的示例 WSDL 文件:

在 Web 服务标注上发送 HTTP 标头

docSample.DocSamplePort stub = new docSample.DocSamplePort();
stub.inputHttpHeaders_x = new Map<String, String>();

//Setting a basic authentication header

stub.inputHttpHeaders_x.put('Authorization', 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==');

//Setting a cookie header
stub.inputHttpHeaders_x.put('Cookie', 'name=value');

//Setting a custom HTTP header
stub.inputHttpHeaders_x.put('myHeader', 'myValue');

String input = 'This is the input string';
String output = stub.EchoString(input);

如果指定了 for 的值,则 覆盖标准标头集。inputHttpHeaders_x

从 Web 服务标注响应访问 HTTP 响应标头

docSample.DocSamplePort stub = new docSample.DocSamplePort();
stub.outputHttpHeaders_x = new Map<String, String>();
String input = 'This is the input string';
String output = stub.EchoString(input);

//Getting cookie header
String cookie = stub.outputHttpHeaders_x.get('Set-Cookie');

//Getting custom header
String myHeader = stub.outputHttpHeaders_x.get('My-Header');

的值为 null 违约。您必须在之前设置 您可以访问响应中标头的内容。outputHttpHeaders_xoutputHttpHeaders_x

支持的 WSDL 功能

Apex 仅支持文档文本包装的 WSDL 样式以及以下基元和 内置数据类型:

架构类型顶点类型
xsd:anyURI字符串
xsd:boolean布尔
xsd:date日期
xsd:dateTime日期时间
xsd:double
xsd:float
xsd:int整数
xsd:integer整数
xsd:language字符串
xsd:long
xsd:Name字符串
xsd:NCName字符串
xsd:nonNegativeInteger整数
xsd:NMTOKEN字符串
xsd:NMTOKENS字符串
xsd:normalizedString字符串
xsd:NOTATION字符串
xsd:positiveInteger整数
xsd:QName字符串
xsd:short整数
xsd:string字符串
xsd:time日期时间
xsd:token字符串
xsd:unsignedInt整数
xsd:unsignedLong
xsd:unsignedShort整数

注意

Salesforce 数据类型 anyType 在用于 生成使用 API 版本 15.0 及更高版本保存的 Apex 代码。对于使用 API 保存的代码 版本 14.0 及更早版本,anyType 映射到 String。

Apex 还支持以下架构构造:

  • xsd:all,在使用 API 版本保存的 Apex 代码中 15.0 及更高版本
  • xsd:annotation,在使用 API 保存的 Apex 代码中 版本 15.0 及更高版本
  • xsd:attribute,在使用 API 保存的 Apex 代码中 版本 15.0 及更高版本
  • xsd:choice,在使用 API 保存的 Apex 代码中 版本 15.0 及更高版本
  • xsd:element.在使用 API 保存的 Apex 代码中 版本 15.0 及更高版本,该属性也是 受以下限制支持:ref
    • 您不能在不同的 命名空间。ref
    • 全局元素不能使用 。ref
    • 如果一个元素包含 ,则它不能也 contain 或 .refnametype
  • xsd:sequence

仅当用作呼入时才支持以下数据类型,即 当外部 Web 服务调用 Apex Web 服务方法时。这些数据类型不是 支持作为标注,即当 Apex Web 服务方法调用 外部 Web 服务。

  • 斑点
  • 十进制
  • 枚举

Apex 不支持任何其他 WSDL 构造、类型或服务,包括:

  • RPC/编码服务
  • 具有多个 、 多个 的 WSDL 文件 服务或多个绑定portTypes
  • 导入外部模式的 WSDL 文件。例如,以下 WSDL 片段 导入外部架构,该架构不是 支持:<wsdl:types> <xsd:schema elementFormDefault="qualified" targetNamespace="http://s3.amazonaws.com/doc/2006-03-01/"> <xsd:include schemaLocation="AmazonS3.xsd"/> </xsd:schema> </wsdl:types>但是,支持在同一架构中导入。在以下内容中 例如,外部 WSDL 被粘贴到您所在的 WSDL 中 转换:<wsdl:types> <xsd:schema xmlns:tns="http://s3.amazonaws.com/doc/2006-03-01/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://s3.amazonaws.com/doc/2006-03-01/"> <xsd:element name="CreateBucket"> <xsd:complexType> <xsd:sequence> [...] </xsd:schema> </wsdl:types>
  • 上表中未记录的任何架构类型
  • 超过大小限制的 WSDL,包括 Salesforce WSDL
  • 不使用文档文本换行样式的 WSDL。以下 WSDL 代码段不使用文档文字换行样式,并导致“无法 查找 complexType“错误时 洋。<wsdl:types> <xsd:schema targetNamespace="http://test.org/AccountPollInterface/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsd:element name="SFDCPollAccountsResponse" type="tns:SFDCPollResponse"/> <xsd:simpleType name="SFDCPollResponse"> <xsd:restriction base="xsd:string" /> </xsd:simpleType> </xsd:schema> </wsdl:types>此修改后的版本将元素包装为 包含元素序列。这遵循文档文本样式,并且是 支持。simpleTypecomplexType<wsdl:types> <xsd:schema targetNamespace="http://test.org/AccountPollInterface/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsd:element name="SFDCPollAccountsResponse" type="tns:SFDCPollResponse" /> <xsd:complexType name="SFDCPollResponse"> <xsd:sequence> <xsd:element name="SFDCOutput" type="xsd:string" /> </xsd:sequence> </xsd:complexType> </xsd:schema> </wsdl:types>
  1. 生成的 WSDL2Apex 代码
    您可以使用 WSDL2Apex 工具从 WSDL 文档生成 Apex 类。WSDL2Apex 工具是开源的,可在 GitHub 上找到。
  2. 测试 Web 服务标注
    生成的代码将另存为 Apex 类,其中包含可用于调用 Web 服务的方法。若要部署或打包此 Apex 类和其他随附代码,75% 的代码必须具有测试覆盖率,包括生成的类中的方法。默认情况下,测试方法不支持 Web 服务标注,并且执行 Web 服务标注的测试会失败。为了防止测试失败并增加代码覆盖率,Apex 提供了内置接口和方法。在测试方法中使用和接收虚假响应。WebServiceMockTest.setMockWebServiceMockTest.setMock
  3. 执行 DML 操作和模拟标注
  4. 使用 WSDL 的注意事项

生成的 WSDL2Apex 代码

您可以使用 WSDL2Apex 工具从 WSDL 文档生成 Apex 类。这 WSDL2Apex 工具是开源的,可在 GitHub 上找到。

您可以在 GitHub 上的 WSDL2Apex 存储库中找到并参与 WSDL2Apex 源代码。

下面的示例演示如何从 WSDL 文档创建 Apex 类。顶点 类是在导入 WSDL 时自动生成的。下面的代码显示了一个示例 WSDL 公文。

<wsdl:definitions xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:tns="http://doc.sample.com/docSample"
targetNamespace="http://doc.sample.com/docSample"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">

<!-- Above, the schema targetNamespace maps to the Apex class name. -->


<!-- Below, the type definitions for the parameters are listed. 
     Each complexType and simpleType parameteris mapped to an Apex class inside the parent class for the WSDL.  Then, each element in the complexType is mapped to a public field inside the class. -->

<wsdl:types>
<s:schema elementFormDefault="qualified"
targetNamespace="http://doc.sample.com/docSample">
<s:element name="EchoString">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="input" type="s:string" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="EchoStringResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="EchoStringResult"
type="s:string" />
</s:sequence>
</s:complexType>
</s:element>
</s:schema>
</wsdl:types>

<!--The stub below defines operations. -->

<wsdl:message name="EchoStringSoapIn">
<wsdl:part name="parameters" element="tns:EchoString" />
</wsdl:message>
<wsdl:message name="EchoStringSoapOut">
<wsdl:part name="parameters" element="tns:EchoStringResponse" />
</wsdl:message>
<wsdl:portType name="DocSamplePortType">
<wsdl:operation name="EchoString">
<wsdl:input message="tns:EchoStringSoapIn" />
<wsdl:output message="tns:EchoStringSoapOut" />
</wsdl:operation>
</wsdl:portType>

<!--The code below defines how the types map to SOAP. -->

<wsdl:binding name="DocSampleBinding" type="tns:DocSamplePortType">
<wsdl:operation name="EchoString">
<soap:operation soapAction="urn:dotnet.callouttest.soap.sforce.com/EchoString"
style="document" />
<wsdl:input>
<soap:body use="literal" />
</wsdl:input>
<wsdl:output>
<soap:body use="literal" />
</wsdl:output>
</wsdl:operation>
</wsdl:binding>

<!-- Finally, the code below defines the endpoint, which maps to the endpoint in the class -->

<wsdl:service name="DocSample">
<wsdl:port name="DocSamplePort" binding="tns:DocSampleBinding">
<soap:address location="http://YourServer/YourService" />
</wsdl:port>
</wsdl:service>
</wsdl:definitions>

在此 WSDL 文档中,自动生成以下 Apex 类。类名是您在以下情况下指定的名称 导入 WSDL.docSample

//Generated by wsdl2apex

public class docSample {
    public class EchoStringResponse_element {
        public String EchoStringResult;
        private String[] EchoStringResult_type_info = new String[]{
                            'EchoStringResult',
                            'http://doc.sample.com/docSample',
                             null,'0','1','false'};
        private String[] apex_schema_type_info = new String[]{
                             'http://doc.sample.com/docSample',
                             'true','false'};
        private String[] field_order_type_info = new String[]{
                             'EchoStringResult'};
    }
    public class EchoString_element {
        public String input;
        private String[] input_type_info = new String[]{
                              'input',
                              'http://doc.sample.com/docSample',
                               null,'0','1','false'};
        private String[] apex_schema_type_info = new String[]{
                               'http://doc.sample.com/docSample',
                               'true','false'};
        private String[] field_order_type_info = new String[]{'input'};
    }
    public class DocSamplePort {
        public String endpoint_x = 'http://YourServer/YourService';
        public Map<String,String> inputHttpHeaders_x;
        public Map<String,String> outputHttpHeaders_x;
        public String clientCertName_x;
        public String clientCert_x;
        public String clientCertPasswd_x;
        public Integer timeout_x;
        private String[] ns_map_type_info = new String[]{
                          'http://doc.sample.com/docSample', 'docSample'};
        public String EchoString(String input) {
            docSample.EchoString_element request_x = new 
                                           docSample.EchoString_element();
            request_x.input = input;
            docSample.EchoStringResponse_element response_x;
            Map<String, docSample.EchoStringResponse_element> response_map_x = 
                       new Map<String, docSample.EchoStringResponse_element>();
            response_map_x.put('response_x', response_x);
            WebServiceCallout.invoke(
              this,
              request_x,
              response_map_x,
              new String[]{endpoint_x,
              'urn:dotnet.callouttest.soap.sforce.com/EchoString',
              'http://doc.sample.com/docSample',
              'EchoString',
              'http://doc.sample.com/docSample',
              'EchoStringResponse',
              'docSample.EchoStringResponse_element'}
            );
            response_x = response_map_x.get('response_x');
            return response_x.EchoStringResult;
        }
    }
}

请注意原始 WSDL 文档中的以下映射:

  • WSDL 目标命名空间映射到 Apex 类名。
  • 每个复杂类型都成为一个类。类型中的每个元素都是 类。
  • WSDL 端口名称映射到存根类。
  • WSDL 中的每个操作都映射到一个公共方法。

您可以使用自动生成的类来 调用外部 Web 服务。下面的代码在外部服务器上调用该方法。docSampleechoString

docSample.DocSamplePort stub = new docSample.DocSamplePort();
String input = 'This is the input string';
String output = stub.EchoString(input);

测试 Web 服务标注

生成的代码将另存为 Apex 类,其中包含您可以调用的方法 调用 Web 服务。若要部署或打包此 Apex 类和其他随附代码, 75% 的代码必须具有测试覆盖率,包括生成的类中的方法。由 默认情况下,测试方法不支持 Web 服务标注和执行 Web 的测试 服务标注失败。为了防止测试失败并增加代码覆盖率,Apex 提供内置接口和 方法。在测试方法中使用和接收虚假响应。

WebServiceMockTest.setMockWebServiceMockTest.setMock

指定用于测试 Web 服务标注的模拟响应

从 WSDL 创建 Apex 类时,自动生成的类中的方法 调用 ,其中 执行对外部服务的标注。测试这些方法时,您可以 指示 Apex 运行时在调用时生成虚假响应。为此, 实现接口和 指定要发送的 Apex 运行时的虚假响应。以下是更多步骤 细节。WebServiceCallout.invokeWebServiceCallout.invokeWebServiceMock

首先,实现接口 并在方法中指定 false 响应。WebServiceMockdoInvoke

global class YourWebServiceMockImpl implements WebServiceMock {
   global void doInvoke(
           Object stub,
           Object request,
           Map<String, Object> response,
           String endpoint,
           String soapAction,
           String requestName,
           String responseNS,
           String responseName,
           String responseType) {

        // Create response element from the autogenerated class.
        // Populate response element.
        // Add response element to the response parameter, as follows:
        response.put('response_x', responseElement); 
   }
}

注意

  • 实现接口的类可以是全局类,也可以是公共类。WebServiceMock
  • 您可以使用@isTest对此类进行批注,因为它仅在测试上下文中使用。 这样,您就可以将其从组织的代码大小限制 6 中排除 MB的。

现在,您已经指定了假响应的值,请指示 Apex 运行时通过调用测试方法发送此虚假响应。对于第一个参数,传递 ,对于第二个参数 参数,则传递 的接口实现的新实例,如下所示:Test.setMockWebServiceMock.classWebServiceMock

Test.setMock(WebServiceMock.class, new YourWebServiceMockImpl());

在此之后,如果在测试上下文中调用 Web 服务标注,则标注为 不是制造的。您会收到方法实现中指定的模拟响应。doInvoke

注意

如果执行标注的代码位于 一个托管包,从 测试方法。Test.setMock

此示例演示如何测试 Web 服务标注。列出了接口的实现 第一。此示例实现该方法,该方法返回您指定的响应。在本例中,响应元素 的自动生成的类被创建并分配一个值。接下来,响应映射 参数填充了此虚假响应。此示例基于 WSDL 在生成的 WSDL2Apex 代码中列出。导入此 WSDL 并生成 保存之前调用的类 这个班级。WebServiceMockdoInvokedocSample

@isTest
global class WebServiceMockImpl implements WebServiceMock {
   global void doInvoke(
           Object stub,
           Object request,
           Map<String, Object> response,
           String endpoint,
           String soapAction,
           String requestName,
           String responseNS,
           String responseName,
           String responseType) {
       docSample.EchoStringResponse_element respElement = 
           new docSample.EchoStringResponse_element();
       respElement.EchoStringResult = 'Mock response';
       response.put('response_x', respElement); 
   }
}

此方法生成 Web 服务标注。

public class WebSvcCallout {
    public static String callEchoString(String input) {
        docSample.DocSamplePort sample = new docSample.DocSamplePort();
        sample.endpoint_x = 'https://example.com/example/test';
        
        // This invokes the EchoString method in the generated class
        String echo = sample.EchoString(input);
        
        return echo;
    }   
}

此测试类包含设置模拟标注模式的测试方法。它调用 上一个方法 类,并验证是否收到模拟响应。callEchoString

@isTest
private class WebSvcCalloutTest {
    @isTest static void testEchoString() {              
        // This causes a fake response to be generated
        Test.setMock(WebServiceMock.class, new WebServiceMockImpl());
        
        // Call the method that invokes a callout
        String output = WebSvcCallout.callEchoString('Hello World!');
        
        // Verify that a fake result is returned
        System.assertEquals('Mock response', output); 
    }
}

执行 DML 操作和模拟标注

默认情况下,标注不是 允许在同一事务中执行 DML 操作后,因为 DML 操作 导致待处理的未提交工作,从而阻止标注执行。 有时,您可能希望在测试方法中插入测试数据 在进行标注之前使用 DML。要启用此功能,请将部分括起来 在 AND 语句中执行标注的代码。语句必须出现 在声明之前。 此外,对 DML 操作的调用不得是 / 块的一部分。Test.startTestTest.stopTestTest.startTestTest.setMockTest.startTestTest.stopTest

DML 操作 在模拟标注被允许且不需要之后发生 测试方法的任何变化。

在模拟标注之前执行 DML

此示例基于前面的示例。该示例显示 如何使用 AND 语句允许 在模拟标注之前在测试方法中执行的 DML 操作。 测试方法()首先插入一个测试帐户,调用,使用设置模拟标注模式,调用一个方法 执行标注,验证模拟响应值,最后 调用。Test.startTestTest.stopTesttestEchoStringTest.startTestTest.setMockTest.stopTest

@isTest
private class WebSvcCalloutTest {
    @isTest static void testEchoString() {              
        // Perform some DML to insert test data
        Account testAcct = new Account('Test Account');
        insert testAcct;

        // Call Test.startTest before performing callout
        // but after setting test data.
        Test.startTest();

        // Set mock callout class 
        Test.setMock(WebServiceMock.class, new WebServiceMockImpl());
        
        // Call the method that invokes a callout
        String output = WebSvcCallout.callEchoString('Hello World!');
        
        // Verify that a fake result is returned
        System.assertEquals('Mock response', output);

        Test.stopTest();
    }
}

异步顶点和模拟标注

与 DML 类似,异步 Apex 操作会导致挂起的未提交工作,从而阻止 稍后在同一事务中执行的标注。示例 异步 Apex 操作是对未来方法、批处理 Apex 或计划的调用 顶点。这些异步调用通常包含在测试方法的 and 语句中,以便 它们在 之后执行。在这个 情况下,可以在异步调用后执行模拟标注,并且不会进行任何更改 必要。但是,如果异步调用没有包含在 and 语句中,你将得到一个 异常,因为未提交的工作处于待处理状态。要防止此异常,请执行以下任一操作 其中:

Test.startTestTest.stopTestTest.stopTestTest.startTestTest.stopTest

  • 将异步调用包含在 and 语句中。Test.startTestTest.stopTestTest.startTest(); MyClass.asyncCall(); Test.stopTest(); Test.setMock(..); // Takes two arguments MyClass.mockCallout();
  • 遵循与 DML 调用相同的规则:将代码部分括起来 在 AND 语句中执行标注。语句必须出现在语句之前。此外, 异步调用不能是 / 块的一部分。Test.startTestTest.stopTestTest.startTestTest.setMockTest.startTestTest.stopTestMyClass.asyncCall(); Test.startTest(); Test.setMock(..); // Takes two arguments MyClass.mockCallout(); Test.stopTest();

模拟标注之后发生的异步调用是 允许并且不需要对测试方法进行任何更改。

使用 WSDL 的注意事项

从 WSDL 生成 Apex 类时,请注意以下事项。

映射标头

WSDL 文档中定义的标头将成为生成的类中存根上的公共字段。 这类似于 AJAX 工具包和 .NET 的工作方式。

了解运行时事件

当 Apex 代码对外部服务进行标注时,将执行以下检查。

  • 有关发出 HTTP 请求或 Web 服务调用时的超时限制的信息,请参见标注限制和局限性。
  • 不允许在 Apex 类中使用循环引用。
  • 不允许与 Salesforce 域建立多个环回连接。
  • 若要允许访问终结点,请从安装程序中输入“快速查找”框,然后选择“远程站点设置”来注册该终结点。Remote Site Settings
  • 为了防止数据库连接被阻止,没有事务 可以打开。

了解变量名称中不支持的字符

WSDL 文件可以包含 Apex 变量名称中不允许的元素名称。这 从 WSDL 生成 Apex 变量名称时,以下规则适用 文件:

  • 如果元素名称的第一个字符不是字母顺序,则会在生成的 Apex 前面附加一个字符 变量名称。x
  • 如果 Apex 变量名称中不允许使用元素名称的最后一个字符,则会在 生成的 Apex 变量名称。x
  • 如果元素名称包含 Apex 变量名称中不允许的字符,则 字符替换为下划线 () 字符。_
  • 如果元素名称在一行中包含两个 Apex 变量中不允许的字符 name,第一个字符替换为下划线 () 字符,第二个字符 替换为字符。这样可以避免生成具有两个连续变量名称的变量名称 下划线,这在 Apex 中是不允许的。_x
  • 假设您有一个操作,该操作采用两个参数和 . 生成的 Apex 有两个变量,均命名为 .该类不编译。 手动编辑 Apex 并更改其中一个变量名称。a_a_xa_x

调试从 WSDL 文件生成的类

Salesforce 使用 SOAP API、.NET 和 Axis 测试代码。如果您使用其他工具,您可能会遇到 问题。

您可以使用调试标头在请求和响应 SOAP 消息中返回 XML 以帮助 您诊断问题。有关更多信息,请参阅使用 SOAP API 部署 Apex。

调用 HTTP 标注

Apex 提供了几个内置类来处理 HTTP 服务并创建 HTTP 请求,例如 GET、POST、PUT 和 DELETE。

您可以使用这些 HTTP 类集成到基于 REST 的服务。它们还允许您 集成到基于 SOAP 的 Web 服务中,作为从 WSDL 生成 Apex 代码的替代选项。 通过使用 HTTP 类,而不是从 WSDL 开始,您可以承担更多的责任 处理请求和响应的 SOAP 消息的构造。

  1. HTTP 类
  2. 测试 HTTP 标注
    要部署或打包 Apex,75% 的代码必须具有测试覆盖率。默认情况下,测试方法不支持 HTTP 标注,因此执行标注的测试会失败。通过使用指示 Apex 在测试中生成模拟响应来启用 HTTP 标注测试。Test.setMock

HTTP 类

这些类公开 HTTP 请求和响应功能。

  • Http的 类。使用此类来启动 HTTP 请求和 响应。
  • HttpRequest 类:使用此类可以 以编程方式创建 HTTP 请求,如 GET、POST、PATCH、PUT 和 DELETE。
  • HttpResponse 类:使用此类可以 处理 返回的 HTTP 响应。HTTP

和类支持这些元素。

HttpRequestHttpResponse

  • Http请求
    • HTTP 请求类型,例如 GET、POST、PATCH、PUT、DELETE、TRACE、 连接、头部和选项
    • 请求标头(如果需要)
    • 读取和连接超时
    • 必要时重定向
    • 邮件正文的内容
  • HttpResponse
    • HTTP 状态代码
    • 响应标头(如果需要)
    • 响应正文的内容

此示例向外部服务器发出 HTTP GET 请求,该请求传递给参数中的方法。此示例还访问 返回的响应。getCalloutResponseContentsurl

public class HttpCalloutSample {

  // Pass in the endpoint to be used using the string url
  public String getCalloutResponseContents(String url) {

    // Instantiate a new Http object
    Http h = new Http();

     // Instantiate a new HTTP request, specify the method (GET) as well as the endpoint
    HttpRequest req = new HttpRequest();
    req.setEndpoint(url);
    req.setMethod('GET');

    // Send the request, and return a response
    HttpResponse res = h.send(req);
    return res.getBody();
  }
}

前面的示例是同步运行的,这意味着没有进一步的处理 在外部 Web 服务返回响应之前发生。或者,您可以使用 @future注解,使 标注异步运行。

此示例向外部服务器发出 HTTP POST 请求,该请求传递给参数中的方法。替换为 要在标注中发送的 JSON 内容。getPostCalloutResponseContentsurlYour_JSON_Content

public class HttpPostCalloutSample {

  // Pass in the endpoint to be used using the string url
  public String getPostCalloutResponseContents(String url) {

    // Instantiate a new Http object
    Http h = new Http();

    // Instantiate a new HTTP request
    // Specify request properties such as the endpoint, the POST method, etc. 
    HttpRequest req = new HttpRequest();
    req.setEndpoint(url);
    req.setMethod('POST');
    req.setHeader('Content-Type', 'application/json');
    req.setBody('{Your_JSON_Content}');

    // Send the request, and return a response
    HttpResponse res = h.send(req);
    return res.getBody();
  }
}

若要从终结点或重定向终结点访问外部服务器,请添加远程站点 添加到授权远程站点的列表中。登录到 Salesforce,然后从“设置”的“快速”中 “查找”框,输入 ,然后选择“远程站点设置”。Remote Site Settings

使用 XML 类或 JSON 类来解析 XML 或 JSON 中的内容 由 HttpRequest 创建的请求的正文,或由 HttpResponse 访问的响应。

考虑

  • AJAX 代理处理重定向和身份验证质询 (401/407 responses) 自动。有关 AJAX 代理的详细信息,请参阅 AJAX 工具包 文档。
  • 您可以将终结点设置为命名凭据 URL。命名凭据 URL 包含方案,名称 命名凭据和可选路径。例如:。一个命名的 credential 指定标注端点的 URL 及其所需的 URL 一个定义中的身份验证参数。Salesforce 管理所有 对指定命名凭据作为标注的 Apex 标注进行身份验证 端点,这样您的代码就不必这样做了。您还可以跳过远程站点 设置,否则,向外部站点进行标注时需要这些设置,用于 命名凭据中定义的站点。请参阅作为标注的命名凭据 端点。callout:callout:My_Named_Credential/some_path
  • 在标注中设置请求正文时,请将方法设置为 。如果设置了请求正文和请求 方法为 ,则执行请求。POSTGETPOST
  • 如果您有来自 DML 的待处理未提交交易,则会阻止标注 操作、可排队作业(与 、 或将来的方法一起排队)。System.enqueueJobDatabase.executeBatch

测试 HTTP 标注

要部署或打包 Apex,75% 的代码必须具有测试覆盖率。默认情况下,test 方法不支持 HTTP 标注,因此执行标注的测试会失败。启用 HTTP 通过指示 Apex 在测试中生成模拟响应来进行标注测试,使用 .

Test.setMock通过以下方式之一指定模拟响应。

  • 通过实现 HttpCalloutMock 接口
  • 通过将静态资源与 StaticResourceCalloutMock 或 MultiStaticResourceCalloutMock 一起使用

若要在测试方法中的模拟标注之前启用运行 DML 操作,请参阅执行 DML 操作和模拟标注。

  • 通过实现 HttpCalloutMock 接口测试 HTTP 标注
  • 使用静态资源测试 HTTP 标注
  • 执行 DML 操作和模拟标注

通过实现 HttpCalloutMock 接口测试 HTTP 标注

为接口提供一个实现,以指定 Apex 运行时调用的方法中发送的响应 发送标注的响应。HttpCalloutMockrespond

global class YourHttpCalloutMockImpl implements HttpCalloutMock {
    global HTTPResponse respond(HTTPRequest req) {
        // Create a fake response.
        // Set response values, and 
        // return response.
    }
}

注意

  • 实现接口的类可以是全局类,也可以是公共类。HttpCalloutMock
  • 您可以使用@isTest注释此类,因为它将仅在测试上下文中使用。 这样,就可以将其从组织的代码大小中排除 限制为 6 MB。

现在,您已经指定了假响应的值,请指示 Apex 运行时通过调用测试方法发送此虚假响应。对于第一个参数,传递 ,对于第二个参数 参数,则传递 的接口实现的新实例,如下所示:Test.setMockHttpCalloutMock.classHttpCalloutMock

Test.setMock(HttpCalloutMock.class, new YourHttpCalloutMockImpl());

在此之后,如果在测试上下文中调用 HTTP 标注,则标注不会 made 时,您会收到在方法实现中指定的模拟响应。respond

注意

如果执行标注的代码位于托管包中,则要模拟标注, 从 测试方法调用 具有相同命名空间的相同包。Test.setMock

这是一个完整的示例,演示如何测试 HTTP 标注。界面 实现 () 列在最前面。它后面是一个包含测试方法的类和另一个 包含测试调用的方法。测试方法通过在调用之前调用来设置模拟标注模式。然后,它会验证 返回的响应是已实现的方法发送的响应。单独保存每个类,并在 中运行测试。MockHttpResponseGeneratortestCalloutTest.setMockgetInfoFromExternalServicerespondCalloutClassTest

@isTest
global class MockHttpResponseGenerator implements HttpCalloutMock {
    // Implement this interface method
    global HTTPResponse respond(HTTPRequest req) {
        // Optionally, only send a mock response for a specific endpoint
        // and method.
        System.assertEquals('https://example.com/example/test', req.getEndpoint());
        System.assertEquals('GET', req.getMethod());
        
        // Create a fake response
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json');
        res.setBody('{"example":"test"}');
        res.setStatusCode(200);
        return res;
    }
}
public class CalloutClass {
    public static HttpResponse getInfoFromExternalService() {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://example.com/example/test');
        req.setMethod('GET');
        Http h = new Http();
        HttpResponse res = h.send(req);
        return res;
    }
}
@isTest
private class CalloutClassTest {
     @isTest static void testCallout() {
        // Set mock callout class 
        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());
        
        // Call method to test.
        // This causes a fake response to be sent
        // from the class that implements HttpCalloutMock. 
        HttpResponse res = CalloutClass.getInfoFromExternalService();
        
        // Verify response received contains fake values
        String contentType = res.getHeader('Content-Type');
        System.assert(contentType == 'application/json');
        String actualValue = res.getBody();
        String expectedValue = '{"example":"test"}';
        System.assertEquals(actualValue, expectedValue);
        System.assertEquals(200, res.getStatusCode());
    }
}

使用静态资源测试 HTTP 标注

您可以通过指定 您希望在静态资源中接收的响应,并使用 两个内置类之一 – StaticResourceCalloutMock 或 MultiStaticResourceCalloutMock

使用 StaticResourceCalloutMock 测试 HTTP 标注

Apex 提供了内置类,您可以通过在静态中指定响应正文来测试标注 资源。使用此类时,您不必提供自己的 接口的实现。相反,只需创建一个实例并设置静态资源以用于 响应正文以及其他响应属性,如状态代码和 内容类型。StaticResourceCalloutMockHttpCalloutMockStaticResourceCalloutMock首先,必须从要包含的文本文件创建静态资源 响应正文:

  1. 创建一个包含要返回的响应正文的文本文件。 响应正文可以是任意字符串,但它必须与 内容类型(如果指定)。例如,如果您的回复没有内容 指定类型,则该文件可以包含任意字符串。如果指定 application/json 的内容类型 对于响应,文件内容应为 JSON 字符串,例如 {“呵呵”:“骗了你”}。abc
  2. 为文本文件创建静态资源:
    1. 在“设置”中,输入“快速查找”框,然后选择“静态资源”。Static Resources
    2. 单击“新建”。
    3. 为静态资源命名。
    4. 选择要上传的文件。
    5. 点击保存

要了解有关静态资源的更多信息,请参阅 Salesforce 联机帮助。

接下来,创建一个实例并设置静态资源,然后 任何其他属性。StaticResourceCalloutMock

StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
mock.setStaticResource('myStaticResourceName');
mock.setStatusCode(200);
mock.setHeader('Content-Type', 'application/json');

在测试方法中,调用以设置模拟标注模式并将其作为第一个传递 参数,以及您为其创建的第二个变量名称 论点。Test.setMockHttpCalloutMock.classStaticResourceCalloutMock

Test.setMock(HttpCalloutMock.class, mock);

在此之后,如果您的测试方法执行标注,则不会进行标注,并且 Apex 运行时发送您在 实例中指定的模拟响应。StaticResourceCalloutMock

注意

如果执行标注的代码位于 一个托管包,从 测试方法。Test.setMock

这是一个完整的示例,包含测试方法 () 和 它所测试的方法 () 执行标注。在运行此示例之前,请创建 名为 based 的静态资源 在包含以下内容的文本文件上。单独保存每个类,并在 中运行测试。testCalloutWithStaticResourcesgetInfoFromExternalServicemockResponse{"hah":"fooled you"}CalloutStaticClassTest

public class CalloutStaticClass {
    public static HttpResponse getInfoFromExternalService(String endpoint) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(endpoint);
        req.setMethod('GET');
        Http h = new Http();
        HttpResponse res = h.send(req);
        return res;
    }
}
@isTest
private class CalloutStaticClassTest {
    @isTest static void testCalloutWithStaticResources() {
        // Use StaticResourceCalloutMock built-in class to
        // specify fake response and include response body 
        // in a static resource.
        StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
        mock.setStaticResource('mockResponse');
        mock.setStatusCode(200);
        mock.setHeader('Content-Type', 'application/json');
        
        // Set the mock callout mode
        Test.setMock(HttpCalloutMock.class, mock);
        
        // Call the method that performs the callout
        HTTPResponse res = CalloutStaticClass.getInfoFromExternalService(
            'https://example.com/example/test');
        
        // Verify response received contains values returned by
        // the mock response.
        // This is the content of the static resource.
        System.assertEquals('{"hah":"fooled you"}', res.getBody());
        System.assertEquals(200,res.getStatusCode());
        System.assertEquals('application/json', res.getHeader('Content-Type'));   
    }
}

使用 MultiStaticResourceCalloutMock 测试 HTTP 标注

Apex 提供了内置类,您可以通过在静态中指定响应正文来测试标注 资源。此类类似于 除了它允许您指定 多个响应机构。使用此类时,您不必提供 您自己的接口实现。相反,只需创建一个实例并将 每个终结点要使用的静态资源。您还可以设置其他响应属性,例如 状态代码和内容类型。MultiStaticResourceCalloutMockStaticResourceCalloutMockHttpCalloutMockMultiStaticResourceCalloutMock

首先,必须从文本文件创建静态资源以包含响应正文。请参阅 使用 StaticResourceCalloutMock 测试 HTTP 标注中概述的过程。

接下来,创建 和 的实例 设置静态资源和任何其他属性。MultiStaticResourceCalloutMock

MultiStaticResourceCalloutMock multimock = new MultiStaticResourceCalloutMock();
multimock.setStaticResource('https://example.com/example/test', 'mockResponse');
multimock.setStaticResource('https://example.com/example/sfdc', 'mockResponse2');
multimock.setStatusCode(200);
multimock.setHeader('Content-Type', 'application/json');

在测试方法中,调用以设置模拟标注模式并将其作为第一个传递 参数,以及您为 AS 创建的变量名称 第二个参数。Test.setMockHttpCalloutMock.classMultiStaticResourceCalloutMock

Test.setMock(HttpCalloutMock.class, multimock);

在此之后,如果测试方法对某个终结点执行 HTTP 标注 https://example.com/example/test 或 https://example.com/example/sfdc,则不会进行标注 并且 Apex 运行时会发送您在 的实例。MultiStaticResourceCalloutMock

这是一个完整的示例,其中包含测试方法 () 和它正在测试的执行标注的方法 ()。在运行此示例之前,请创建 名为 based 的静态资源 在一个文本文件上,其中包含内容,另一个基于 包含以下内容的文本文件。单独保存每个类,并在 中运行测试。testCalloutWithMultipleStaticResourcesgetInfoFromExternalServicemockResponse{"hah":"fooled you"}mockResponse2{"hah":"fooled you twice"}CalloutMultiStaticClassTest

public class CalloutMultiStaticClass {
    public static HttpResponse getInfoFromExternalService(String endpoint) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint(endpoint);
        req.setMethod('GET');
        Http h = new Http();
        HttpResponse res = h.send(req);
        return res;
    }
}
@isTest
private class CalloutMultiStaticClassTest {
    @isTest static void testCalloutWithMultipleStaticResources() {
        // Use MultiStaticResourceCalloutMock to
        // specify fake response for a certain endpoint and 
        // include response body in a static resource.    
        MultiStaticResourceCalloutMock multimock = new MultiStaticResourceCalloutMock();
        multimock.setStaticResource(
            'https://example.com/example/test', 'mockResponse');
        multimock.setStaticResource(
            'https://example.com/example/sfdc', 'mockResponse2');
        multimock.setStatusCode(200);
        multimock.setHeader('Content-Type', 'application/json');
        
        // Set the mock callout mode
        Test.setMock(HttpCalloutMock.class, multimock);
        
        // Call the method for the first endpoint
        HTTPResponse res = CalloutMultiStaticClass.getInfoFromExternalService(
            'https://example.com/example/test');
        // Verify response received 
        System.assertEquals('{"hah":"fooled you"}', res.getBody());
        
        // Call the method for the second endpoint
        HTTPResponse res2 = CalloutMultiStaticClass.getInfoFromExternalService(
            'https://example.com/example/sfdc');
        // Verify response received 
        System.assertEquals('{"hah":"fooled you twice"}', res2.getBody());
    }
}

执行 DML 操作和模拟标注

默认情况下,标注不是 允许在同一事务中执行 DML 操作后,因为 DML 操作 导致待处理的未提交工作,从而阻止标注执行。 有时,您可能希望在测试方法中插入测试数据 在进行标注之前使用 DML。要启用此功能,请将部分括起来 在 AND 语句中执行标注的代码。语句必须出现 在声明之前。 此外,对 DML 操作的调用不得是 / 块的一部分。Test.startTestTest.stopTestTest.startTestTest.setMockTest.startTestTest.stopTest

DML 操作 在模拟标注被允许且不需要之后发生 测试方法的任何变化。

DML 操作支持适用于所有实现 使用接口和静态资源(或 )。以下示例使用已实现的接口,但您 在使用静态资源时可以应用相同的技术。HttpCalloutMockStaticResourceCalloutMockMultiStaticResourceCalloutMockHttpCalloutMock

在模拟标注之前执行 DML

此示例基于前面提供的 HttpCalloutMock 示例。该示例演示如何使用 和 语句来允许 在模拟标注之前在测试方法中执行的 DML 操作。 测试方法()首先插入一个测试帐户,调用,使用设置模拟标注模式,调用一个方法 执行标注,验证模拟响应值,最后 调用。Test.startTestTest.stopTesttestCalloutTest.startTestTest.setMockTest.stopTest

@isTest
private class CalloutClassTest {
     @isTest static void testCallout() {
        // Perform some DML to insert test data
        Account testAcct = new Account('Test Account');
        insert testAcct;

        // Call Test.startTest before performing callout
        // but after setting test data.
        Test.startTest();

        // Set mock callout class 
        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());
        
        // Call method to test.
        // This causes a fake response to be sent
        // from the class that implements HttpCalloutMock. 
        HttpResponse res = CalloutClass.getInfoFromExternalService();
        
        // Verify response received contains fake values
        String contentType = res.getHeader('Content-Type');
        System.assert(contentType == 'application/json');
        String actualValue = res.getBody();
        String expectedValue = '{"example":"test"}';
        System.assertEquals(actualValue, expectedValue);
        System.assertEquals(200, res.getStatusCode());

        Test.stopTest();
    }
}

异步顶点和模拟标注

与 DML 类似,异步 Apex 操作会导致挂起的未提交工作,从而阻止 稍后在同一事务中执行的标注。示例 异步 Apex 操作是对未来方法、批处理 Apex 或计划的调用 顶点。这些异步调用通常包含在测试方法的 and 语句中,以便 它们在 之后执行。在这个 情况下,可以在异步调用后执行模拟标注,并且不会进行任何更改 必要。但是,如果异步调用没有包含在 and 语句中,你将得到一个 异常,因为未提交的工作处于待处理状态。要防止此异常,请执行以下任一操作 其中:

Test.startTestTest.stopTestTest.stopTestTest.startTestTest.stopTest

  • 将异步调用包含在 and 语句中。Test.startTestTest.stopTestTest.startTest(); MyClass.asyncCall(); Test.stopTest(); Test.setMock(..); // Takes two arguments MyClass.mockCallout();
  • 遵循与 DML 调用相同的规则:将代码部分括起来 在 AND 语句中执行标注。语句必须出现在语句之前。此外, 异步调用不能是 / 块的一部分。Test.startTestTest.stopTestTest.startTestTest.setMockTest.startTestTest.stopTestMyClass.asyncCall(); Test.startTest(); Test.setMock(..); // Takes two arguments MyClass.mockCallout(); Test.stopTest();

模拟标注之后发生的异步调用是 允许并且不需要对测试方法进行任何更改。

使用证书

要使用双向 SSL 身份验证,请发送一个证书,其中包含您的标注,该证书是 在 Salesforce 中生成或由证书颁发机构 (CA) 签名。发送证书 增强了安全性,因为标注的目标接收证书并可以使用 它根据其密钥库对请求进行身份验证。

要为标注启用双向 SSL 身份验证,请执行以下操作:

  1. 生成一个 证书。
  2. 将证书与代码集成。请参阅对 SOAP 服务使用证书和对 HTTP 请求使用证书。
  3. 如果要连接到第三方并使用自签名证书,请共享 Salesforce 证书,以便他们可以将证书添加到他们的 密钥库。如果要连接到组织内的其他应用程序, 配置 Web 服务器或应用程序服务器以请求客户端证书。这 进程取决于您使用的 Web 或应用程序服务器的类型。
  4. 配置远程站点设置 标注。在任何 Apex 标注可以调用外部站点之前,该站点必须 在“远程站点设置”页中注册,否则标注失败。如果标注 指定命名凭据作为终结点,无需配置远程 网站设置。若要设置命名凭据,请参阅《定义命名凭据》中的“定义命名凭据” Salesforce 帮助。
  1. 生成证书
  2. 将证书用于 SOAP 服务
    要支持对 SOAP Web 服务的标注进行双向身份验证,请在 Salesforce 中生成证书或将密钥对从密钥库导入 Salesforce。然后将证书与您的 Apex 集成。
  3. 对 HTTP 请求使用证书

生成证书

您可以使用在 Salesforce 中生成的自签名证书或由 证书颁发机构 (CA)。要为标注生成证书,请参阅生成证书。

成功保存 Salesforce 证书后,证书和相应的 密钥是自动生成的。

创建 CA 签名证书后,必须先上传签名证书 你可以使用它。请参阅“生成由证书签名的证书 权限“在 Salesforce 在线帮助中。

将证书用于 SOAP 服务

要支持 SOAP Web Service 标注的双向身份验证,请生成 证书或将密钥对从密钥库导入 Salesforce。然后 将证书与您的 Apex 集成。

重要

我们建议存储外部的相互身份验证证书 Java 密钥库中的 Web 服务。有关详细信息,请参阅证书和密钥。

要将证书与您的 Apex 集成,请执行以下操作:

  1. 从第三方接收 Web Service 的 WSDL,或从 要连接到的应用程序。
  2. 从 Web 服务的 WSDL 生成 Apex 类。请参阅 SOAP 服务:从 WSDL 文档定义类。
  3. 生成的 Apex 类包括用于调用第三方 Web 服务的存根 由 WSDL 文档表示。编辑 Apex 类,并为实例上的变量赋值 存根类。该值必须与 在“证书和密钥管理”页面上生成的证书。clientCertName_x

此示例演示如何编辑 Apex 类,并使用生成的 WSDL2Apex 代码中的示例 WSDL 文件。示例 假定您生成的证书的唯一名称为 。DocSampleCert

docSample.DocSamplePort stub = new docSample.DocSamplePort();
stub.clientCertName_x = 'DocSampleCert';
String input = 'This is the input string';
String output = stub.EchoString(input);

对 HTTP 请求使用证书

在 Salesforce 中生成证书后,您可以使用它来支持双向 对 HTTP 请求的标注进行身份验证。

要将证书与您的 Apex 集成,请执行以下操作:

  1. 生成证书。记下证书的唯一名称。
  2. 在 Apex 中,使用 类。用于参数的值 对于此方法,必须与证书的唯一名称匹配 在上一步中生成。setClientCertificateNameHttpRequest

以下示例演示了上一步的最后一步 程序。此示例假定您之前生成了一个证书 唯一名称为 。DocSampleCert

HttpRequest req = new HttpRequest();
req.setClientCertificateName('DocSampleCert');

标注限制和局限性

当 Apex 代码对 HTTP 请求进行标注时,存在以下限制和限制 或 Web 服务调用。Web Service 调用可以是 SOAP API 调用,也可以是任何外部 Web 服务调用。

  • 单个 Apex 事务最多可以对 HTTP 请求或 API 发出 100 个标注 叫。
  • 在 Developer Edition 组织,您最多只能对外部的端点进行 20 个并发标注 您的 Salesforce 组织的域。此限制不适用于非开发人员版 组织。
  • 默认超时为 10 秒。可以为每个标注定义自定义超时。这 最小值为 1 毫秒,最大值为 120,000 毫秒。请参阅 下一节了解如何为 Web 服务或 HTTP 标注设置自定义超时。
  • 单个 Apex 事务的标注的最大累积超时为 120 秒。 此时间是 Apex 事务调用的所有标注的累加时间。
  • 每个组织对运行时间超过 5 秒的长时间运行请求都有限制(总计 执行时间)。计算此限制时,不包括 HTTP 标注处理时间。 我们暂停标注的计时器,并在标注完成后恢复。请参阅 Lightning Platform Apex 的执行调控器和限制 限制。
  • 当同一事务中有待处理的操作时,无法进行标注。 导致挂起操作的内容是 DML 语句、异步 Apex(例如 未来方法和批处理 Apex 作业)、计划的 Apex 或发送电子邮件。您可以进行标注 在执行这些类型的操作之前。
  • 挂起的操作可能在同一事务中的模拟标注之前发生。请参阅对基于 WSDL 的标注执行 DML 操作和模拟标注或对 HTTP 标注执行 DML 操作和模拟标注。
  • 当标头添加到 标注请求和响应 未由外部服务器返回,则会发生超时。Expect: 100-ContinueHTTP/1.1 100 Continue

只读模式下的 Apex 标注

在只读模式下,外部服务的 Apex 标注会执行,并且不会被 系统。通常,您会在以下情况下在同一事务中执行一些后续操作 接收来自标注的响应。例如,您可以进行 DML 调用以更新 Salesforce 记录。但 Salesforce 中的写入操作(如记录更新)被阻止 在只读模式下。只读模式下的这种行为不一致可能会破坏 程序流程和原因问题。为避免不正确的程序行为,我们建议您 防止在只读模式下进行标注。要检查组织是否处于只读模式, 叫。System.getApplicationReadWriteMode()

下面的示例检查 的返回值。如果返回值等于枚举值,则组织 在只读模式下,将跳过标注。否则 ( value),则执行标注。System.getApplicationReadWriteMode()ApplicationReadWriteMode.READ_ONLYApplicationReadWriteMode.DEFAULT

注意

此类使用 Apex HTTP 类作为示例进行标注。您还可以制作一个 通过 WSDL2Apex 使用导入的 WSDL 进行标注。检查只读的过程 无论哪种情况,模式都是相同的。

public class HttpCalloutSampleReadOnly {
    public class MyReadOnlyException extends Exception {}

    // Pass in the endpoint to be used using the string url
    public String getCalloutResponseContents(String url) {
        
        // Get Read-only mode status
        ApplicationReadWriteMode mode = System.getApplicationReadWriteMode();
        String returnValue = '';
        
        if (mode == ApplicationReadWriteMode.READ_ONLY) {
            // Prevent the callout
            throw new MyReadOnlyException('Read-only mode. Skipping callouts!');
        } else if (mode == ApplicationReadWriteMode.DEFAULT) {
            // Instantiate a new http object
            Http h = new Http();
            
            // Instantiate a new HTTP request, specify the method (GET) 
            // as well as the endpoint.
            HttpRequest req = new HttpRequest();
            req.setEndpoint(url);
            req.setMethod('GET');
            
            // Send the request, and return a response
            HttpResponse res = h.send(req);
            returnValue = res.getBody();                        
        }
        return returnValue;
    }
}

在某些 Salesforce 维护活动期间,您的 Salesforce 组织处于只读模式。 例如计划的站点切换和实例刷新。作为连续站点切换的一部分, 您的 Salesforce 组织大约每六个月切换到一次其就绪站点。为 有关站点切换的详细信息,请参阅连续站点切换。

要在沙盒中测试只读模式,请联系 Salesforce 以启用只读模式测试 选择。启用测试选项后,您可以打开只读模式并验证您的 应用程序。

设置注解超时

下面的示例为 Web 服务标注设置自定义超时。该示例有效 替换为示例 WSDL 文件和生成的类,如生成的 WSDL2Apex 代码中所述。设置超时值(以毫秒为单位) 为特殊变量赋值 在 存根。

DocSamplePorttimeout_x

docSample.DocSamplePort stub = new docSample.DocSamplePort();
stub.timeout_x = 2000; // timeout in milliseconds

以下是为 HTTP 标注设置自定义超时的示例:

HttpRequest req = new HttpRequest();
req.setTimeout(2000); // timeout in milliseconds

使用延续进行长时间的标注

使用异步标注从 Visualforce 页面或 Lightning 组件到外部 Web 服务,并在 回调方法。

异步标注是从 Visualforce 页面或 Lightning 制作的标注 通过回调方法返回响应的组件。异步 标注也称为延续

Visualforce 示例

下图显示了异步标注的执行路径。 从 Visualforce 页面开始。用户在 Visualforce 页面上调用一个操作,该操作 从 Web 服务请求信息(步骤 1)。应用服务器发出标注请求 复制到 Continuation 服务器,然后返回到 Visualforce 页面(步骤 2-3)。这 Continuation 服务器将请求发送到 Web 服务并接收响应(步骤 4–7),然后将响应交还给应用服务器(步骤 8)。最后,回应 返回到 Visualforce 页面(步骤 9)。

异步标注的执行流程

延续的执行流程图示

受益于异步标注的典型 Salesforce 应用程序 包含一个带有按钮的 Visualforce 页面。用户单击该按钮可从 外部 Web 服务。例如,获取 Web 服务中的某些产品。组织中数以千计的代理可以使用此功能 页。因此,一百个代理商可以点击同一个按钮来处理保修 同时提供产品信息。这一百个同时动作超过了 并发长时间运行请求数限制为 10。但是通过使用异步标注, 请求不受此限制的约束,可以执行。

在以下示例应用程序中,按钮操作是在 Apex 中实现的 controller 方法。action 方法创建一个并返回它。将请求发送到服务后, Visualforce 请求已暂停。用户必须等待返回响应,然后才能返回 继续使用页面并调用新操作。当外部服务返回 响应时,Visualforce 请求将恢复,页面将收到此响应。Continuation

这是我们示例应用程序的 Visualforce 页面。此页面包含一个按钮,用于 调用控制器的方法 与此页面相关联。返回延续结果并回调后 方法,按钮将再次呈现组件以显示响应的主体。startRequestoutputText

<apex:page controller="ContinuationController" showChat="false" showHeader="false">
   <apex:form >
      <!-- Invokes the action method when the user clicks this button. -->
      <apex:commandButton action="{!startRequest}" 
              value="Start Request" reRender="result"/> 
   </apex:form>

   <!-- This output text component displays the callout response body. -->
   <apex:outputText id="result" value="{!result}" />
</apex:page>

以下是与 Visualforce 页面关联的 Apex 控制器。这 controller 包含 action 和 callback 方法。

注意

在调用外部服务之前,必须将远程站点添加到 Salesforce 用户界面中授权的远程站点。在“设置”中,在“快速查找”框中输入, ,然后选择“远程站点设置”,然后单击“新建” 远程站点Remote Site Settings

如果标注将命名凭据指定为终结点, 无需配置远程站点设置。命名凭据指定 URL 一个定义中的标注端点及其所需的身份验证参数。设置 命名凭据,请参阅 Salesforce 帮助中的“定义命名凭据”。在代码中, 指定命名凭据 URL,而不是长时间运行的服务 URL。一个命名的 credential URL 包含方案、名称 的命名凭据和可选路径。例如:。callout:callout:My_Named_Credential/some_path

public with sharing class ContinuationController {
    // Unique label corresponding to the continuation
    public String requestLabel;
    // Result of callout
    public String result {get;set;}
    // Callout endpoint as a named credential URL 
    // or, as shown here, as the long-running service URL
    private static final String LONG_RUNNING_SERVICE_URL = 
        '<Insert your service URL>';
   
   // Action method
    public Object startRequest() {
      // Create continuation with a timeout
      Continuation con = new Continuation(40);
      // Set callback method
      con.continuationMethod='processResponse';
      
      // Create callout request
      HttpRequest req = new HttpRequest();
      req.setMethod('GET');
      req.setEndpoint(LONG_RUNNING_SERVICE_URL);
      
      // Add callout request to continuation
      this.requestLabel = con.addHttpRequest(req);
      
      // Return the continuation
      return con;  
    }
    
    // Callback method 
    public Object processResponse() {   
      // Get the response by using the unique label
      HttpResponse response = Continuation.getResponse(this.requestLabel);
      // Set the result variable that is displayed on the Visualforce page
      this.result = response.getBody();
      
      // Return null to re-render the original Visualforce page
      return null;
    }
}

注意

  • 在单个延续中最多可以创建三个异步标注。添加这些 使用类的方法对同一延续的标注请求。标注并行运行,用于此延续和 暂停 Visualforce 请求。只有在外部服务返回所有标注后, Visualforce 进程将恢复。addHttpRequestContinuation
  • 异步标注仅通过 Visualforce 页面受支持。制作一个 通过在 Visualforce 页面外部调用操作方法进行异步标注,例如 在开发者控制台中,不受支持。
  • 异步标注可用于保存的 Apex 控制器和 Visualforce 页面 在版本 30.0 及更高版本中。如果使用 JavaScript 远程处理,则版本 31.0 或更高版本是 必填。
  • 专用连接不支持异步标注。
  • 使用异步标注的过程
    若要使用异步标注,请在控制器的操作方法中创建一个对象,并实现回调方法。Continuation
  • 测试异步标注
    编写测试以测试控制器并满足部署或打包 Apex 的代码覆盖率要求。由于 Apex 测试不支持进行标注,因此您可以模拟标注请求和响应。模拟标注时,请求不会发送到外部服务,而是使用模拟响应。
  • 异步标注限制
    执行延续时,将应用特定于延续的限制。当延续返回并且请求恢复时,将启动新的 Apex 事务。所有 Apex 和 Visualforce 限制均适用,并在新事务中重置,包括 Apex 标注限制。
  • 创建多个异步标注
    要从 Visualforce 页面同时对长时间运行的服务进行多个标注,您最多可以向 Continuation 实例添加三个请求。例如,当您向服务发出独立请求时,例如获取两种产品的库存统计信息,何时同时进行宣传。
  • 链接异步标注
    如果标注的顺序很重要,或者当一个标注以另一个标注的响应为条件时,则可以链接标注请求。链接标注意味着只有在前一个标注的响应返回后才会进行下一个标注。例如,您可能需要在保修服务响应指示保修过期后链接标注以获取保修延期信息。您最多可以链接三个标注。
  • 从导入的 WSDL
    创建异步标注 除了基于 的标注之外,从 WSDL 生成的类进行的 Web 服务调用中还支持异步标注。从 WSDL 生成的类创建异步标注的过程与使用该类的过程类似。HttpRequestHttpRequest

使用异步标注的过程

若要使用异步标注,请在控制器的操作方法中创建一个对象,并实现回调 方法。

Continuation

在操作方法中调用异步标注

若要调用异步标注,请使用 Visualforce 操作方法中的实例调用外部服务。 创建延续时,可以指定超时值和回调的名称 方法。例如,以下代码将创建一个具有 60 秒超时和 processResponse 的回调方法名称。Continuation

Continuation cont = new Continuation(60);
cont.continuationMethod = 'processResponse';

接下来,将对象关联到 外部标注。为此,请创建 HTTP 请求,然后将此请求添加到 延续如下:Continuation

String requestLabel = cont.addHttpRequest(request);

注意

此过程基于使用 HttpRequest 类进行标注。举个例子 使用基于 WSDL 的类,请参阅从导入的 WSDL 创建异步标注。

调用标注的方法(操作方法)必须返回对象,以指示 Visualforce 挂起 系统发送标注并等待标注响应后的当前请求。该对象包含 要执行的标注。ContinuationContinuation

这是调用标注的方法的签名。Object 返回类型 表示 .Continuation

public Object calloutActionMethodName()

定义回调方法

在外部服务完成标注处理后,将返回响应。你 可以指定 callout 返回后异步执行的回调方法。这 回调方法必须在 Controller 类中定义,其中 callout 调用方法 是定义的。您可以定义一个回调方法来处理返回的响应,例如 检索响应以显示在 Visualforce 页面上。

回调方法不接受任何参数,并具有此签名。

public Object callbackMethodName()

Object 返回类型表示 、 或 。渲染原始 Visualforce 页面并完成 Visualforce request,在回调方法中返回。ContinuationPageReferencenullnull

如果操作方法使用 JavaScript 远程处理(带有 注释),则回调方法必须是静态的,并且具有 支持以下签名。@RemoteAction

public static Object callbackMethodName(List< String> labels, Object state)

艺术

public static Object callbackMethodName(Object state)

该参数由系统在调用 callback 方法,并保存与发出的标注请求关联的标签。该参数是通过在控制器中设置 Continuation.state 属性来提供的。labelsstate

下表列出了回调方法的返回值。每个返回值对应 到不同的行为。

Callback 方法返回值请求生命周期和结果
null系统完成 Visualforce 页面请求并呈现原始 Visualforce 页面(或其中的一部分)。
PageReference系统完成 Visualforce 页面请求并重定向到新的 Visualforce 页面。(使用 中的查询参数将 的结果传递到新页面。PageReferenceContinuation
Continuation系统再次暂停 Visualforce 请求,并等待 新的标注。返回新的 用于链接异步标注的回调方法。Continuation

注意

如果未为延续设置属性,则使用相同的操作方法 这使得标注在标注响应返回时再次调用。continuationMethod

测试异步标注

编写测试以测试控制器并满足部署的代码覆盖率要求 或包装 Apex。由于 Apex 测试不支持进行标注,因此您可以模拟 标注请求和响应。模拟标注时,请求不会得到 发送到外部服务,并使用模拟响应。

下面的示例演示如何在 Web 测试中调用模拟异步标注 使用 的服务调用。模拟 标注,调用类的以下方法:Test.setContinuationResponse() 和 Test.invokeContinuationMethod()。HTTPRequestTest

首先列出要测试的控制器类,然后是测试类。控制器 此处重用了 Make Long-Running Callouts with Continuations 中的类。

public with sharing class ContinuationController {
    // Unique label corresponding to the continuation request
    public String requestLabel;
    // Result of callout
    public String result {get;set;}
    // Endpoint of long-running service
    private static final String LONG_RUNNING_SERVICE_URL = 
        '<Insert your service URL>';
   
   // Action method
    public Object startRequest() {
      // Create continuation with a timeout
      Continuation con = new Continuation(40);
      // Set callback method
      con.continuationMethod='processResponse';
      
      // Create callout request
      HttpRequest req = new HttpRequest();
      req.setMethod('GET');
      req.setEndpoint(LONG_RUNNING_SERVICE_URL);
      
      // Add callout request to continuation
      this.requestLabel = con.addHttpRequest(req);
      
      // Return the continuation
      return con;  
    }
    
    // Callback method 
    public Object processResponse() {   
      // Get the response by using the unique label
      HttpResponse response = Continuation.getResponse(this.requestLabel);
      // Set the result variable that is displayed on the Visualforce page
      this.result = response.getBody();
      
      // Return null to re-render the original Visualforce page
      return null;
    }
}

此示例显示与控制器对应的测试类。此测试类 包含用于测试异步标注的测试方法。在测试方法中,设置一个模拟响应, 并导致 callback 方法。该测试确保回调 方法通过验证控制器的结果变量 设置为预期的响应。Test.setContinuationResponseTest.invokeContinuationMethod

@isTest
public class ContinuationTestingForHttpRequest {
    public static testmethod void testWebService() {
        ContinuationController controller = new ContinuationController();
        // Invoke the continuation by calling the action method
        Continuation conti = (Continuation)controller.startRequest();
        
        // Verify that the continuation has the proper requests
        Map<String, HttpRequest> requests = conti.getRequests();
        system.assert(requests.size() == 1);
        system.assert(requests.get(controller.requestLabel) != null);
        
        // Perform mock callout 
        // (i.e. skip the callout and call the callback method)
        HttpResponse response = new HttpResponse();
        response.setBody('Mock response body');   
        // Set the fake response for the continuation     
        Test.setContinuationResponse(controller.requestLabel, response);
        // Invoke callback method
        Object result = Test.invokeContinuationMethod(controller, conti);
        // result is the return value of the callback
        System.assertEquals(null, result);
        // Verify that the controller's result variable
        //   is set to the mock response.
        System.assertEquals('Mock response body', controller.result);
    }
}

异步标注限制

执行延续时,将适用特定于延续的限制。当 延续返回,请求恢复,新的 Apex 事务开始。所有 Apex 和 Visualforce 限制适用,并在新事务中重置,包括 Apex 标注 限制。

延续特定限制

以下是特定于延续的 Apex 和 Visualforce 限制。

描述限制
单个延续中并行 Apex 标注的最大数量3
链式 Apex 标注的最大数量3
单个延续的最大超时1120 秒
最大 Visualforce 控制器状态大小280 KB
最大 HTTP 响应大小1 兆字节
最大 HTTP POST 表单大小 – 所有键和值的大小 形式31 兆字节
HTTP POST 表单中的最大密钥数3500

1在自动生成的 Web 服务存根和 HttpRequest 对象将被忽略。对于延续,仅强制执行此超时限制。

2执行延续时,将对 Visualforce 控制器进行序列化。 延续完成后,控制器被反序列化,回调 调用。使用 Apex 修饰符可以 指定不序列化的变量。该框架仅使用序列化 成员,当它恢复时。控制器状态大小限制与视图状态是分开的 限制。请参见延续控制器状态和 Visualforce 视图状态。transient

3此限制适用于具有以下内容类型标头的HTTP POST表单:content-type=’application/x-www-form-urlencoded’和content-type=’multipart/form-data’

延续控制器状态和 Visualforce 视图状态

控制器状态和视图状态是不同的。延续的控制器状态包括 请求中涉及的所有控制器的序列化,而不仅仅是 调用延续的控制器。序列化控制器包括控制器 扩展,以及自定义和内部组件控制器。控制器状态大小为 作为事件记录在调试日志中。USER_DEBUG

视图状态比控制器状态保存的数据更多,并且具有更高的最大大小 (170KB)。 视图状态包含状态和组件结构。状态是所有 控制器和页面上每个组件的所有属性,包括子页面和 子组件。组件结构是组件的父子关系 在页面中。您可以在开发者控制台或页脚中监控视图状态大小 启用开发模式时的 Visualforce 页面。有关详细信息,请参阅“查看 状态选项卡“,或参考 Visualforce 开发人员的 指南。

进行多个异步标注

从 Visualforce 同时对长时间运行的服务进行多个标注 页面上,您最多可以向 Continuation 实例添加三个请求。何时 “同时标注”是指向服务发出独立请求,例如 获取两种产品的库存统计信息。

当您在同一延续中制作多个标注时,标注请求会运行 并行并挂起 Visualforce 请求。仅在返回所有标注响应后 Visualforce 进程是否恢复。

以下 Visualforce 和 Apex 示例显示了如何进行两个异步标注 同时使用单个延续。首先显示 Visualforce 页面。这 Visualforce 页面包含一个按钮,用于调用控制器中的操作方法。当 Visualforce 进程恢复,outputPanel 组件被渲染 再。此面板显示两个异步标注的响应。startRequestsInParallel

<apex:page controller="MultipleCalloutController" showChat="false" showHeader="false">
   <apex:form >
      <!-- Invokes the action method when the user clicks this button. -->
      <apex:commandButton action="{!startRequestsInParallel}" value="Start Request" reRender="panel"/>  
   </apex:form>

   <apex:outputPanel id="panel">
       <!-- Displays the response body of the initial callout. -->   
       <apex:outputText value="{!result1}" />
       
       <br/>
       <!-- Displays the response body of the chained callout. -->
       <apex:outputText value="{!result2}" />
   </apex:outputPanel> 
   
</apex:page>

此示例显示 Visualforce 页面的控制器类。该方法将两个请求添加到 延续。返回所有标注响应后,将调用回调方法 () 并处理 反应。startRequestsInParallelprocessAllResponses

public with sharing class MultipleCalloutController {

    // Unique label for the first request
    public String requestLabel1;
    // Unique label for the second request
    public String requestLabel2;
    // Result of first callout
    public String result1 {get;set;}
   // Result of second callout
    public String result2 {get;set;}
    // Endpoints of long-running service
    private static final String LONG_RUNNING_SERVICE_URL1 = 
        '<Insert your first service URL>';
    private static final String LONG_RUNNING_SERVICE_URL2 = 
        '<Insert your second service URL>';
           
    // Action method
    public Object startRequestsInParallel() {
      // Create continuation with a timeout
      Continuation con = new Continuation(60);
      // Set callback method
      con.continuationMethod='processAllResponses';
      
      // Create first callout request
      HttpRequest req1 = new HttpRequest();
      req1.setMethod('GET');
      req1.setEndpoint(LONG_RUNNING_SERVICE_URL1);
      
      // Add first callout request to continuation
      this.requestLabel1 = con.addHttpRequest(req1);     
      
      // Create second callout request
      HttpRequest req2 = new HttpRequest();
      req2.setMethod('GET');
      req2.setEndpoint(LONG_RUNNING_SERVICE_URL2);
      
      // Add second callout request to continuation
      this.requestLabel2 = con.addHttpRequest(req2);     
      
      // Return the continuation
      return con;  
    }
    
    // Callback method.
    // Invoked only when responses of all callouts are returned.
    public Object processAllResponses() {   
      // Get the response of the first request
      HttpResponse response1 = Continuation.getResponse(this.requestLabel1);
      this.result1 = response1.getBody();

      // Get the response of the second request
      HttpResponse response2 = Continuation.getResponse(this.requestLabel2);
      this.result2 = response2.getBody();
                 
      // Return null to re-render the original Visualforce page
      return null;
    }
}

链接异步标注

如果标注的顺序很重要,或者标注以响应为条件 在另一个标注中,您可以链接标注请求。链接标注意味着下一个 只有在前一个标注的响应返回后,才会进行标注。例如,您 可能需要在保修服务后链接标注以获取保修延期信息 响应表示保修已过期。您最多可以链接三个标注。

以下 Visualforce 和 Apex 示例演示如何将一个标注链接到另一个标注。 首先显示 Visualforce 页面。Visualforce 页面包含一个按钮,用于调用 操作方法 控制器。每次返回延续时,Visualforce 进程都会暂停。 Visualforce 进程在返回每个响应后恢复,并呈现每个响应 outputPanel 组件中的响应。invokeInitialRequest

<apex:page controller="ChainedContinuationController" showChat="false" showHeader="false">
   <apex:form >
      <!-- Invokes the action method when the user clicks this button. -->
      <apex:commandButton action="{!invokeInitialRequest}" value="Start Request" reRender="panel"/>  
   </apex:form>

   <apex:outputPanel id="panel">
       <!-- Displays the response body of the initial callout. -->   
       <apex:outputText value="{!result1}" />
       
       <br/>
       <!-- Displays the response body of the chained callout. -->
       <apex:outputText value="{!result2}" />
   </apex:outputPanel> 
   
</apex:page>

此示例显示 Visualforce 页面的控制器类。该方法创建第一个 延续。回调方法 () 处理第一个标注的响应。如果 此响应满足特定条件,该方法通过返回来链接另一个标注 第二个延续。返回链式延续的响应后, 第二个回调方法 () 被调用并处理第二个响应。invokeInitialRequestprocessInitialResponseprocessChainedResponse

public with sharing class ChainedContinuationController {

    // Unique label for the initial callout request
    public String requestLabel1;
    // Unique label for the chained callout request
    public String requestLabel2;
    // Result of initial callout
    public String result1 {get;set;}
    // Result of chained callout
    public String result2 {get;set;}
    // Endpoint of long-running service
    private static final String LONG_RUNNING_SERVICE_URL1 = 
        '<Insert your first service URL>';
    private static final String LONG_RUNNING_SERVICE_URL2 = 
        '<Insert your second service URL>';
           
    // Action method
    public Object invokeInitialRequest() {
      // Create continuation with a timeout
      Continuation con = new Continuation(60);
      // Set callback method
      con.continuationMethod='processInitialResponse';
      
      // Create first callout request
      HttpRequest req = new HttpRequest();
      req.setMethod('GET');
      req.setEndpoint(LONG_RUNNING_SERVICE_URL1);
      
      // Add initial callout request to continuation
      this.requestLabel1 = con.addHttpRequest(req);              
      
      // Return the continuation
      return con;  
    }
    
    // Callback method for initial request
    public Object processInitialResponse() {   
      // Get the response by using the unique label
      HttpResponse response = Continuation.getResponse(this.requestLabel1);
      // Set the result variable that is displayed on the Visualforce page
      this.result1 = response.getBody();
           
      Continuation chainedContinuation = null;
      // Chain continuation if some condition is met
      if (response.getBody().toLowerCase().contains('expired')) {
          // Create a second continuation 
          chainedContinuation = new Continuation(60);
          // Set callback method
          chainedContinuation.continuationMethod='processChainedResponse';
          
          // Create callout request
          HttpRequest req = new HttpRequest();
          req.setMethod('GET');
          req.setEndpoint(LONG_RUNNING_SERVICE_URL2);
          
          // Add callout request to continuation
          this.requestLabel2 = chainedContinuation.addHttpRequest(req); 
      }
      
      // Start another continuation 
      return chainedContinuation;  
    }    
    
    // Callback method for chained request
    public Object processChainedResponse() {   
      // Get the response for the chained request
      HttpResponse response = Continuation.getResponse(this.requestLabel2);
      // Set the result variable that is displayed on the Visualforce page
      this.result2 = response.getBody();
           
      // Return null to re-render the original Visualforce page
      return null;
    }
}

注意

在创建新的延续之前,必须检索延续的响应 以及 Visualforce 请求再次暂停之前。您无法检索旧的 来自延续链中较早延续的回应。

从导入的 WSDL 进行异步标注

除了基于 -的标注之外, 在从 WSDL 生成的 Web 服务调用中支持异步标注 类。从 WSDL 生成的类进行异步标注的过程与此类似 到使用类的过程。

HttpRequestHttpRequest

在 Salesforce 中导入 WSDL 时,Salesforce 会为每个类自动生成两个 Apex 类 命名空间。一个类是同步的服务类 服务,另一个是异步服务的修改版本。这 自动生成的异步类名以 Async 前缀开头 其格式为 AsyncServiceName。 是原始未修改的服务类的名称。 异步类在以下方面与标准类不同。ServiceName

  • 公共服务方法包含一个额外的参数作为第一个参数。Continuation
  • Web 服务操作是异步调用的,其响应是 用 response 元素。getValue
  • 和 习惯于 分别调用服务和获取响应。WebServiceCallout.beginInvokeWebServiceCallout.endInvoke

您可以在 Salesforce 用户界面中从 WSDL 生成 Apex 类。从设置中, 在“快速查找”框中输入 Apex 类, 然后选择 Apex 类

若要进行异步 Web 服务标注,请在自动生成的 异步类,方法是将实例传递给这些方法。以下示例基于假设的股票报价 服务。此示例假定组织具有一个名为 的类,该类是自动生成的 通过 WSDL 导入。该示例演示如何对服务进行异步标注 通过使用自动生成的类。首先,此示例创建一个 以 60 秒的超时时间继续,并设置回调方法。接下来,代码 示例调用方法 将 Continuation 实例传递给它。方法调用对应于异步标注 执行。ContinuationAsyncSOAPStockQuoteServiceAsyncSOAPStockQuoteServicebeginStockQuotebeginStockQuote

public Continuation startRequest() {
   Integer TIMEOUT_INT_SECS = 60;  
   Continuation cont = new Continuation(TIMEOUT_INT_SECS);
   cont.continuationMethod = 'processResponse';
   
   AsyncSOAPStockQuoteService.AsyncStockQuoteServiceSoap 
      stockQuoteService = 
        new AsyncSOAPStockQuoteService.AsyncStockQuoteServiceSoap();
   stockQuoteFuture = stockQuoteService.beginStockQuote(cont,'CRM');    

   return cont;   
}

当外部服务返回异步标注(方法)的响应时,此回调方法是 执行。它通过调用响应对象上的方法来获取响应。beginStockQuotegetValue

public Object processResponse() {
   result = stockQuoteFuture.getValue();
   return null; 
}

以下是带有 action 和 callback 方法的整个控制器。

public class ContinuationSOAPController {
 
    AsyncSOAPStockQuoteService.GetStockQuoteResponse_elementFuture
           stockQuoteFuture;
    public String result {get;set;}

    // Action method
    public Continuation startRequest() {    
       Integer TIMEOUT_INT_SECS = 60;  
       Continuation cont = new Continuation(TIMEOUT_INT_SECS);
       cont.continuationMethod = 'processResponse';
       
       AsyncSOAPStockQuoteService.AsyncStockQuoteServiceSoap 
          stockQuoteService = 
            new AsyncSOAPStockQuoteService.AsyncStockQuoteServiceSoap();
           stockQuoteFuture = stockQuoteService.beginGetStockQuote(cont,'CRM');     
       return cont;   
    }    
    
    // Callback method
    public Object processResponse() {    
       result = stockQuoteFuture.getValue();
       // Return null to re-render the original Visualforce page
       return null; 
    }
}

此示例显示了调用该方法并显示结果的相应 Visualforce 页面 田。startRequest

<apex:page controller="ContinuationSOAPController" showChat="false" showHeader="false">
   <apex:form >      
      <!-- Invokes the action method when the user clicks this button. -->
      <apex:commandButton action="{!startRequest}" 
              value="Start Request" reRender="result"/> 
   </apex:form>

   <!-- This output text component displays the callout response body. -->
   <apex:outputText value="{!result}" />
</apex:page>

测试基于 WSDL 的异步标注

测试基于 WSDL 中的 Apex 类的异步标注与此类似 添加到与基于类的标注一起使用的流程。在测试 ContinuationSOAPController.cls 之前,请创建一个 实现。本类 启用对 ContinuationTestForWSDL.cls 的安全测试,该测试 我们稍后将通过启用模拟延续并确保 测试没有实际效果。HttpRequestWebServiceMock

public class AsyncSOAPStockQuoteServiceMockImpl implements WebServiceMock {
    public void doInvoke(
        Object stub, 
        Object request, 
        Map<String, Object> response,
        String endpoint, 
        String soapAction, 
        String requestName,
        String responseNS, 
        String responseName, 
        String responseType) {
        // do nothing
    }
}

此示例是对应于控制器的测试类。类中的测试方法 设置一个假响应并调用一个模拟的延续。标注不会发送到 外部服务。为了执行模拟标注,测试调用类的以下方法:Test.setContinuationResponse() 和 Test.invokeContinuationMethod()。ContinuationSOAPControllerTest

@isTest
public class ContinuationTestingForWSDL {
    public static testmethod void testWebService() {

        ContinuationSOAPController demoWSDLClass = 
            new ContinuationSOAPController();

        // Invoke the continuation by calling the action method
        Continuation conti = demoWSDLClass.startRequest();

        // Verify that the continuation has the proper requests
        Map<String, HttpRequest> requests = conti.getRequests();
        System.assertEquals(requests.size(), 1);

        // Perform mock callout 
        // (i.e. skip the callout and call the callback method)
        HttpResponse response = new HttpResponse();
        response.setBody('<SOAP:Envelope'
            + ' xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">'
            + '<SOAP:Body>'
            + '<m:getStockQuoteResponse '
            + 'xmlns:m="http://soap.sforce.com/schemas/class/StockQuoteServiceSoap">'
            + '<m:result>Mock response body</m:result>' 
            + '</m:getStockQuoteResponse>' 
            + '</SOAP:Body>'
            + '</SOAP:Envelope>');

        // Set the fake response for the continuation
        String requestLabel = requests.keyset().iterator().next();
        Test.setContinuationResponse(requestLabel, response);

        // Invoke callback method
        Object result = Test.invokeContinuationMethod(demoWSDLClass, conti);
        System.debug(demoWSDLClass);

        // result is the return value of the callback
        System.assertEquals(null, result);

        // Verify that the controller's result variable
        //   is set to the mock response.
        System.assertEquals('Mock response body', demoWSDLClass.result);
    }
}

支持类

支持类允许您与支持中心常用的记录进行交互, 例如营业时间和案例。

与企业合作 小时

营业时间用于指定营业时间 您的客户支持团队运作的地方,包括多项业务 多个时区的小时数。本示例查找时间 1 从 startTime 开始的工作时间,返回本地的 Datetime 时区。它通过查询 BusinessHours 来获取默认工作时间。 此外,它还调用该方法。

BusinessHoursadd

// Get the default business hours
BusinessHours bh = [SELECT Id FROM BusinessHours WHERE IsDefault=true];

// Create Datetime on May 28, 2008 at 1:06:08 AM in local timezone.
Datetime startTime = Datetime.newInstance(2008, 5, 28, 1, 6, 8);

// Find the time it will be one business hour from May 28, 2008, 1:06:08 AM using the
// default business hours.  The returned Datetime will be in the local timezone.
Datetime nextTime = BusinessHours.add(bh.id, startTime, 60 * 60 * 1000L);

本示例查找距离 1 个工作小时的时间 startTime,以 GMT 为单位返回日期时间:

// Get the default business hours
BusinessHours bh = [SELECT Id FROM BusinessHours WHERE IsDefault=true];

// Create Datetime on May 28, 2008 at 1:06:08 AM in local timezone.
Datetime startTime = Datetime.newInstance(2008, 5, 28, 1, 6, 8);

// Find the time it will be one business hour from May 28, 2008, 1:06:08 AM using the
// default business hours.  The returned Datetime will be in GMT.
Datetime nextTimeGmt = BusinessHours.addGmt(bh.id, startTime, 60 * 60 * 1000L);

下一个示例查找 startTime 之间的差异 和 nextTime:

// Get the default business hours
BusinessHours bh = [select id from businesshours where IsDefault=true];

// Create Datetime on May 28, 2008 at 1:06:08 AM in local timezone.
Datetime startTime = Datetime.newInstance(2008, 5, 28, 1, 6, 8);

// Create Datetime on May 28, 2008 at 4:06:08 PM in local timezone.
Datetime endTime = Datetime.newInstance(2008, 5, 28, 16, 6, 8);

// Find the number of business hours milliseconds between startTime and endTime as
// defined by the default business hours.  Will return a negative value if endTime is
// before startTime, 0 if equal, positive value otherwise.
Long diff = BusinessHours.diff(bh.id, startTime, endTime);

使用案例

传入和传出 可以使用以下方法将电子邮件与其相应的案例相关联 类方法。 此方法与 Email-to-Case(一个自动化过程)一起使用 这将从客户那里收到的电子邮件转变为客户服务案例。CasesgetCaseIdFromEmailThreadId

以下示例使用电子邮件线程 ID 来检索 相关案例 ID。

public class GetCaseIdController {

   public static void getCaseIdSample() {
        // Get email thread ID 
        String emailThreadId = '_00Dxx1gEW._500xxYktg';
        // Call Apex method to retrieve case ID from email thread ID 
        ID caseId = Cases.getCaseIdFromEmailThreadId(emailThreadId);
    
    }
}

区域管理2.0

通过对 Territory2 和 UserTerritory2Association 标准对象的触发器支持, 您可以自动执行与这些区域管理中的更改相关的操作和流程 记录。

Territory2 的示例触发器

此示例触发器在创建或删除 Territory2 记录后触发。 此示例触发器假定组织具有一个名为 TerritoryCount__c 的自定义字段,该字段在 Territory2Model 对象上定义为 跟踪每个区域模型中的区域净数量。触发器代码 每次创建或删除区域时,递增或递减TerritoryCount__c字段中的值。

trigger maintainTerritoryCount on Territory2 (after insert, after delete) {
    // Track the effective delta for each model
    Map<Id, Integer> modelMap = new Map<Id, Integer>();
    for(Territory2 terr : (Trigger.isInsert ? Trigger.new : Trigger.old)) {
       Integer offset = 0;
       if(modelMap.containsKey(terr.territory2ModelId)) {
           offset = modelMap.get(terr.territory2ModelId);
       }
       offset += (Trigger.isInsert ? 1 : -1);
       modelMap.put(terr.territory2ModelId, offset);
    }
    // We have a custom field on Territory2Model called TerritoryCount__c
    List<Territory2Model> models = [SELECT Id, TerritoryCount__c FROM 
                            Territory2Model WHERE Id IN :modelMap.keySet()];
    for(Territory2Model tm : models) {
       // In case the field is not defined with a default of 0
       if(tm.TerritoryCount__c == null) {
           tm.TerritoryCount__c = 0;
       }
       tm.TerritoryCount__c += modelMap.get(tm.Id);
    }
    // Bulk update the field on all the impacted models
    update(models);
}

UserTerritory2Association 的示例触发器

此示例触发器在创建 UserTerritory2Association 记录后触发。 此示例触发器向 Sales Operations 组发送电子邮件通知 让他们知道用户已添加到区域。它标识用户 谁将用户添加到区域。然后,它标识每个添加的用户以及 用户被添加到的区域以及区域所属的区域模型 自。

trigger notifySalesOps on UserTerritory2Association (after insert) {
    // Query the details of the users and territories involved
    List<UserTerritory2Association> utaList = [SELECT Id, User.FirstName, User.LastName, 
       Territory2.Name, Territory2.Territory2Model.Name 
       FROM UserTerritory2Association WHERE Id IN :Trigger.New];
            
    // Email message to send
    Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
    mail.setToAddresses(new String[]{'salesOps@acme.com'}); 
    mail.setSubject('Users added to territories notification');     
            
    // Build the message body   
    List<String> msgBody = new List<String>();
    String addedToTerrStr = '{0}, {1} added to territory {2} in model {3} \n';    
    msgBody.add('The following users were added to territories by ' +  
        UserInfo.getFirstName() + ', ' + UserInfo.getLastName() + '\n');
    for(UserTerritory2Association uta : utaList) {
       msgBody.add(String.format(addedToTerrStr, 
           new String[]{uta.User.FirstName, uta.User.LastName,
                        uta.Territory2.Name, uta.Territory2.Territory2Model.Name}));    
    } 
    
    // Set the message body and send the email
    mail.setPlainTextBody(String.join(msgBody,''));
    Messaging.sendEmail(new Messaging.Email[] { mail });
}

Salesforce 站点

Salesforce Sites 允许您通过继承来构建自定义页面和 Web 应用程序 闪电平台功能,包括分析、工作流程和审批,以及可编程 逻辑。

您可以使用 和 类的方法在 Apex 中管理您的 Salesforce 站点。SiteCookie

  • 重写 Salesforce 站点的 URL 站点提供内置逻辑,可帮助您显示用户友好的 URL 和指向站点
    访问者的链接。创建规则以重写在地址栏中键入的、从书签启动的或从外部网站链接的 URL 请求。您还可以创建规则来重写网站页面中链接的 URL。URL 重写不仅使 URL 对用户更具描述性和直观性,还允许搜索引擎更好地索引您的网站页面。

重写 Salesforce 站点的 URL

协作平台提供了内置逻辑,可帮助您显示用户友好的网址和指向 网站访问者。

创建规则以重写键入的 URL 请求 进入地址栏,从书签启动,或从外部网站链接。您可以 此外,还要创建规则来重写网站页面中链接的 URL。URL 重写不 只会使 URL 对用户更具描述性和直观性,它允许搜索引擎 更好地为您的网站页面编制索引。

例如,假设您有一个博客网站。如果没有 URL 重写,博客条目的 URL 可能如下所示: https://myblog.my.salesforce-sites.com/posts?id=003D000000Q0PcN

通过 URL 重写,您的用户可以按日期和标题访问博客文章,而不是 按记录 ID。您的新年前夜帖子的网址可能是:https://myblog.my.salesforce-sites.com/posts/2019/12/31/auld-lang-syne

您还可以重写网站页面中显示的链接的 URL。如果你的新年前夜帖子 包含指向您的情人节帖子的链接,链接网址可能会显示:https://myblog.my.salesforce-sites.com/posts/2019/02/14/last-minute-roses

若要重写站点的 URL,请创建一个 Apex 类,该类将原始 URL 映射到 用户友好的 URL,然后将 Apex 类添加到您的网站。

若要了解 中的方法,请参阅 UrlRewriter 接口。Site.UrlRewriter interface

创建 Apex 类

您创建的 Apex 类必须实现提供的接口。通常,它必须具有 以后 形式:

Site.UrlRewriter

global class yourClass implements Site.UrlRewriter {
    global PageReference mapRequestUrl(PageReference
            yourFriendlyUrl)
    global PageReference[] generateUrlFor(PageReference[]
            yourSalesforceUrls);
}

在创建 Apex 类时,请考虑以下限制和建议:类和方法必须是全局的Apex 类和方法必须全部为 。global类必须同时包含这两种方法Apex 类必须同时实现 和 方法。如果您不想使用以下之一 方法,只需让它返回即可。mapRequestUrlgenerateUrlFornull重写仅适用于 Visualforce 网站页面传入的 URL 请求只能映射到关联的 Visualforce 页面 与您的网站。无法映射到标准页面、图像或其他 实体。要重写网站网页上链接的网址,请使用带有 merge 变量的函数。例如 以下链接到名为 我的页面:!URLFOR$Page

<apex:outputLink value="{!URLFOR($Page.myPage)}"></apex:outputLink>

注意

Visualforce 元素 不受 的影响。<apex:form>forceSSL=”true”urlRewriter请参阅 Visualforce 开发人员的 指南。编码的 URL使用该接口获取的 URL 已编码。如果需要 访问 URL 的未编码值,请使用 EncodingUtil 类的方法。Site.urlRewriterurlDecode受限制的字符用户友好的 URL 必须与 Salesforce URL 不同。带有 3 个字符的实体前缀或 15 或 18 个字符的 ID 不是 重写。您不能在用户友好网址或重写的网址中使用句点,但以下情况除外 对于路径 组件,不能在 URL 末尾使用。.well-known受限制的字符串不能使用以下保留字符串作为第一个路径组件 在用户友好的 URL 或重写的站点基本 URL 之后 网址。网站基本 URL 之后的第一个过去组件的一些示例 是 baseURL https://.my.salesforce-sites.com/baseURL, https://.my.salesforce-sites.com/pathPrefix/baseURL, https://custom-domain/pathPrefix/baseURL,以及 https://.my.salesforce-sites.com/pathPrefix/baseURL/another/path。MyDomainNameMyDomainNameMyDomainName

  • apexcomponent
  • apexpages
  • aura
  • chatter
  • chatteranswers
  • chatterservice
  • cometd
  • ex
  • faces
  • flash
  • flex
  • google
  • home
  • id
  • ideas
  • idp
  • images
  • img
  • javascript
  • js
  • knowledge
  • lightning
  • login
  • m
  • mobile
  • ncsphoto
  • nui
  • push
  • resource
  • saml
  • sccommunities
  • search
  • secur
  • services
  • servlet
  • setup
  • sfc
  • sfdc
  • sfdc_ns
  • sfsites
  • site
  • style
  • vote
  • WEB-INF
  • widg

不能在重写的末尾使用以下保留字符串 URL 路径:

  • /光环
  • /auraFW
  • /auraResource
  • /AuraJLoggingRPCService
  • /AuraJLVRPC服务
  • /AuraJRPC服务
  • /dbcthumbnail
  • /HelpAndTrainingDoor
  • /htmldbcthumbnail
  • /升
  • /米
  • /移动

仅相对路径PageReference.getUrl() 方法 仅返回紧跟在主机名后面的 URL 部分,或者 站点前缀(如果有)。例如,如果您的 URL 是 https://mycompany.my.salesforce-sites.com/sales/MyPage?id=12345, 其中“sales”是网站前缀,仅返回 /MyPage?id=12345。您无法重写域或网站前缀。仅限唯一路径您无法将 URL 映射到与您的网站同名的目录 前缀。例如,如果您的网站网址为 https://acme.my.salesforce-sites.com/help, 其中“help”是站点前缀,则不能指向 URL 到帮助/页面。生成的路径 https://acme.my.salesforce-sites.com/help/help/page、 将作为 https://acme.my.salesforce-sites.com/help/page 返回。批量查询为了提高页面生成性能,请批量执行任务 而不是一次一个的方法。generateUrlFor强制执行字段唯一性确保您选择用于重写 URL 的字段是唯一的。用 SOQL 中用于查询的唯一字段或索引字段可能会得到改进 性能。

向站点添加 URL 重写

创建 URL 重写 Apex 类后,请按照以下步骤将其添加到 您的网站:

  1. 在“设置”中,输入“快速” “查找”框,然后选择“站点”。Sites
  2. 单击“新建”或单击“编辑” 现有站点。
  3. 在“站点编辑”页上,为“URL 重写器”选择一个 Apex 类 类。
  4. 点击保存

注意

如果您的网站上启用了 URL 重写,则所有 PageReferences 都是 通过 URL 重写器传递。设置为 且不是 0 的 PageReferences 返回重定向的 URL,而不是 重写的 URL。redirecttrueredirectCode

代码示例

在此示例中,我们有一个由两个 Visualforce 页面组成的简单站点:mycontact 和 myaccount。确保您为两者启用了“读取”权限 在尝试示例之前。每个页面都使用标准控制器作为其对象 类型。联系页面包括指向父帐户的链接以及联系人 详。

在实现重写之前,地址栏和链接 URL 显示记录 ID(a 随机 15 位字符串),如“之前”图所示。启用重写后, 地址栏和链接显示更多用户友好的重写 URL,如“之后”所示 图。

用于重写这些页面的 URL 的 Apex 类显示在 URL 重写 Apex 类示例中,并附有详细的注释。

示例网站页面

本部分显示此部分中使用的客户和联系人页面的 Visualforce 例。帐户页面对帐户使用标准控制器,只不过是一个 标准详情页面。此页面应命名为 我的帐户。

<apex:page standardController="Account">
    <apex:detail relatedList="false"/>
</apex:page>

联系人页面使用标准控制器进行联系人,由两部分组成。 第一部分使用函数和合并变量链接到父帐户;第二个只是提供联系方式。请注意, Visualforce 页面不包含任何重写逻辑,但 .此页面应命名为 mycontact。

URLFOR$PageURLFOR

<apex:page standardController="contact">
    <apex:pageBlock title="Parent Account">
        <apex:outputLink value="{!URLFOR($Page.mycontact,null, 
                [id=contact.account.id])}">{!contact.account.name}
                </apex:outputLink>
    </apex:pageBlock>
    <apex:detail relatedList="false"/>
</apex:page>

示例 URL 重写 Apex 类

用作站点的 URL 重写器的 Apex 类使用该方法来映射传入的 URL 请求到正确的 Salesforce 记录。它还使用该方法重写 链接到帐户页面更加人性化 形式。

mapRequestUrlgenerateUrlFor

global with sharing class myRewriter implements Site.UrlRewriter {

    //Variables to represent the user-friendly URLs for
    //account and contact pages
    String ACCOUNT_PAGE = '/myaccount/';
    String CONTACT_PAGE = '/mycontact/';
    //Variables to represent my custom Visualforce pages
    //that display account and contact information
    String ACCOUNT_VISUALFORCE_PAGE = '/myaccount?id=';
    String CONTACT_VISUALFORCE_PAGE = '/mycontact?id=';

    global PageReference mapRequestUrl(PageReference
            myFriendlyUrl){
        String url = myFriendlyUrl.getUrl();

        if(url.startsWith(CONTACT_PAGE)){
            //Extract the name of the contact from the URL
            //For example: /mycontact/Ryan returns Ryan
            String name = url.substring(CONTACT_PAGE.length(),
                    url.length());

            //Select the ID of the contact that matches
            //the name from the URL
            Contact con = [SELECT Id FROM Contact WHERE Name =:
                    name LIMIT 1];

            //Construct a new page reference in the form
            //of my Visualforce page
            return new PageReference(CONTACT_VISUALFORCE_PAGE + con.id);
        }
        if(url.startsWith(ACCOUNT_PAGE)){
            //Extract the name of the account
            String name = url.substring(ACCOUNT_PAGE.length(),
                    url.length());

            //Query for the ID of an account with this name
            Account acc = [SELECT Id FROM Account WHERE Name =:name LIMIT 1];

           //Return a page in Visualforce format
            return new PageReference(ACCOUNT_VISUALFORCE_PAGE + acc.id);
        }
        //If the URL isn't in the form of a contact or
        //account page, continue with the request
        return null;
    }
    global List<PageReference> generateUrlFor(List<PageReference> 
            mySalesforceUrls){
        //A list of pages to return after all the links 
        //have been evaluated
        List<PageReference> myFriendlyUrls = new List<PageReference>();
        
        //a list of all the ids in the urls
        List<id> accIds = new List<id>();
        
        // loop through all the urls once, finding all the valid ids
        for(PageReference mySalesforceUrl : mySalesforceUrls){
        //Get the URL of the page
        String url = mySalesforceUrl.getUrl();
 
            //If this looks like an account page, transform it
            if(url.startsWith(ACCOUNT_VISUALFORCE_PAGE)){
                //Extract the ID from the query parameter
                //and store in a list
                //for querying later in bulk.
                        String id= url.substring(ACCOUNT_VISUALFORCE_PAGE.length(),
                        url.length());
                        accIds.add(id);
            }
        }

    // Get all the account names in bulk
    List <account> accounts = [SELECT Name FROM Account WHERE Id IN :accIds];

    // make the new urls
    Integer counter = 0;

    // it is important to go through all the urls again, so that the order
    // of the urls in the list is maintained. 
    for(PageReference mySalesforceUrl : mySalesforceUrls) {

       //Get the URL of the page
       String url = mySalesforceUrl.getUrl();

       if(url.startsWith(ACCOUNT_VISUALFORCE_PAGE)){
         myFriendlyUrls.add(new PageReference(ACCOUNT_PAGE + accounts.get(counter).name));
         counter++;
       } else {
         //If this doesn't start like an account page,
         //don't do any transformations
         myFriendlyUrls.add(mySalesforceUrl);
       }
    }
   
    //Return the full list of pages
    return myFriendlyUrls;
  }

}

重写之前和之后

下面是实现 Apex 类以重写 原始站点 URL。请注意第一张图中基于 ID 的 URL,以及 第二个用户友好的 URL。

重写之前的网站 URL之前的网站网址此图中编号的元素是:

  1. 重写之前联系人页面的原始 URL
  2. 从联系页面到父帐户页面的链接
  3. 重写前指向帐户页面的链接的原始 URL,如 浏览器的状态栏

重写后的网站 URL之后的网站网址此图中编号的元素是:

  1. 重写后联系人页面的重写 URL
  2. 从联系页面到父帐户页面的链接
  3. 重写后指向帐户页面的链接的重写 URL,如 浏览器的状态栏

通过 Apex 的 Salesforce 报告和仪表板 API

通过 Apex 的 Salesforce 报告和仪表板 API,您可以以编程方式访问您的 报表生成器中定义的报表数据。

该 API 使您能够将报表数据集成到任何 Web 或移动应用程序中,内部或 在 Salesforce 平台之外。例如,您可以使用 API 触发带有 每个季度表现最好的销售代表的快照。

通过 Apex 的 Salesforce 报告和仪表板 API 彻底改变了您访问和 可视化您的数据。您可以:

  • 将报表数据集成到自定义对象中。
  • 将报表数据集成到丰富的可视化效果中,以动画化 数据。
  • 构建自定义仪表板。
  • 自动执行报告任务。

概括地说,API 资源使您能够查询和筛选 报告数据。您可以:

  • 同步或异步运行表格、摘要或矩阵报表。
  • 动态筛选特定数据。
  • 查询报表数据和元数据。
  • 要求和限制
    通过 Apex 的 Salesforce 报告和仪表板 API 适用于启用了 API 的组织。
  • 运行报告
    您可以通过 Apex 通过 Salesforce 报告和仪表板 API 同步或异步运行报告。
  • 列出报表
    的异步运行 您最多可以检索异步运行的报表的 2,000 个实例。
  • 获取报表元数据
    可以检索报表元数据以获取有关报表及其报表类型的信息。
  • 获取报表数据 可以使用该类获取事实映射,其中包含与报表关联的数据
    ReportResults
  • 筛选报告
    若要动态获取特定结果,可以通过 API 筛选报告。
  • 解码事实数据图
    事实数据地图包含报表的摘要和记录级数据值。
  • 测试报告
    与所有 Apex 代码一样,通过 Apex 代码的 Salesforce 报告和仪表板 API 需要测试覆盖率。

要求和限制

通过 Apex 的 Salesforce 报告和仪表板 API 适用于以下组织 已启用 API。

除了以下限制外,还适用于通过 Apex 的报表和仪表板 API 常规 API 限制。

  • 在以下情况下,交叉筛选器、标准报表筛选器和按行限制筛选不可用 筛选数据。
  • 只有矩阵报告才支持历史跟踪报告。
  • 历史跟踪报告不支持订阅。
  • API 只能处理包含最多 100 个选为列的字段的报表。
  • 最多可以返回 200 个最近查看的报告的列表。
  • 您的组织每小时最多可以请求 500 次同步报表运行。
  • API 一次最多支持 20 个同步报表运行请求。
  • 异步运行的报表的最多 2,000 个实例的列表可以是 返回。
  • 该 API 一次最多支持 200 个请求来获取异步报告的结果 运行。
  • 您的组织每小时最多可以请求 1,200 个异步请求。
  • 异步报告运行结果在 24 小时滚动周期内可用。
  • API 最多返回前 2,000 个报表行。您可以使用以下方法缩小结果范围 过滤 器。
  • 运行报表时,您最多可以添加 20 个自定义字段筛选器。
  • 如果报表以 Apex 中的自动化流程用户身份在标准或自定义对象上运行 test 类,则仅返回必填的自定义字段。非必填自定义字段不是 结果中显示。
    • 您的组织每小时最多可以请求 200 次仪表板刷新。
    • 您的组织每小时最多可以请求 5,000 个仪表板的结果。

此外,以下限制适用于通过 Apex 的 Reports and Dashboards API。

  • 批处理 Apex 中不允许异步报告调用。
  • Apex 触发器中不允许报告调用。
  • 没有 Apex 方法可以列出最近运行的报告。
  • 同步报表运行期间处理的报表行数计入 将 SOQL 查询检索的总行数限制为 50,000 行的调控器限制 每笔交易。异步运行报表时,不会施加此限制。
  • 在 Apex 测试中,报告运行始终忽略注释,无论注释是否设置为 或 。这 表示报告结果将包含测试未创建的预先存在的数据。 无法禁用注释 用于执行报表。若要限制结果,请对报表使用筛选器。SeeAllDatatruefalseSeeAllData
  • 在 Apex 测试中,异步报告运行仅在测试停止后执行 方法。Test.stopTest

注意

适用于在报表生成器中创建的报表的所有限制也适用于 API。为 有关详细信息,请参阅 Salesforce 联机帮助中的“分析限制”。

运行报表

您可以通过 Salesforce 报表和仪表板同步或异步运行报表 API 通过 Apex。

报表可以在有或没有详细信息的情况下运行,并且可以通过设置报表元数据进行筛选。什么时候 运行报告时,API 将返回相同数量的记录的数据,这些记录在 报告在 Salesforce 用户界面中运行。

如果希望报表快速完成运行,请同步运行报表。否则,我们建议 出于以下原因,您通过 Salesforce API 异步运行报告:

  • 长时间运行的报表在达到超时限制时的风险较低 异步运行。
  • 通过 Apex 的 Salesforce 报告和仪表板 API 可以处理更多数量的 一次异步运行请求。
  • 因为异步运行报表的结果将存储 24 小时 滚动期,它们可用于定期访问。

同步运行报表

运行报表 同步地,使用其中一种方法。例如:ReportManager.runReport()

// Get the report ID
List <Report> reportList = [SELECT Id,DeveloperName FROM Report where 
    DeveloperName = 'Closed_Sales_This_Quarter'];
String reportId = (String)reportList.get(0).get('Id');

// Run the report
Reports.ReportResults results = Reports.ReportManager.runReport(reportId, true);
System.debug('Synchronous results: ' + results);

异步运行报表

运行报表 异步使用其中一种方法。例如:ReportManager.runAsyncReport()

// Get the report ID
List <Report> reportList = [SELECT Id,DeveloperName FROM Report where 
    DeveloperName = 'Closed_Sales_This_Quarter'];
String reportId = (String)reportList.get(0).get('Id');

// Run the report
Reports.ReportInstance instance = Reports.ReportManager.runAsyncReport(reportId, true);
System.debug('Asynchronous instance: ' + instance);

列出报表的异步运行

您最多可以检索 2,000 个报表实例,这些实例 异步运行。

实例列表按日期和时间排序 报告已运行。报告结果滚动存储 24 小时 时期。在此期间,根据您的用户访问级别,您可以 访问已运行的报表的每个实例的结果。

您可以通过调用该方法获取实例列表。 例如:ReportManager.getReportInstances

// Get the report ID
List <Report> reportList = [SELECT Id,DeveloperName FROM Report where
    DeveloperName = 'Closed_Sales_This_Quarter'];
String reportId = (String)reportList.get(0).get('Id');

// Run a report asynchronously
Reports.ReportInstance instance = Reports.ReportManager.runAsyncReport(reportId, true);
System.debug('List of asynchronous runs: ' + 
    Reports.ReportManager.getReportInstances(reportId));

获取报表元数据

您可以检索报表元数据以获取有关以下内容的信息 报表及其报表类型。元数据包括有关 筛选器、分组、详细数据和摘要的报告。您可以 使用元数据执行以下几项操作:

  • 了解您可以在报告中筛选哪些字段和值 类型。
  • 使用元数据信息构建自定义图表可视化效果 关于字段、分组、详细数据和摘要。
  • 运行报表时更改报表元数据中的筛选器。

使用该方法检索报表元数据。然后,您可以使用“get” 类上的方法来访问元数据值。ReportResults.getReportMetadataReportMetadata

下面的示例检索报表的元数据。

// Get the report ID
List <Report> reportList = [SELECT Id,DeveloperName FROM Report where 
    DeveloperName = 'Closed_Sales_This_Quarter'];
String reportId = (String)reportList.get(0).get('Id');

// Run a report
Reports.ReportResults results = Reports.ReportManager.runReport(reportId);

// Get the report metadata
Reports.ReportMetadata rm = results.getReportMetadata();
System.debug('Name: ' + rm.getName());
System.debug('ID: ' + rm.getId());
System.debug('Currency code: ' + rm.getCurrencyCode());
System.debug('Developer name: ' + rm.getDeveloperName());

// Get grouping info for first grouping
Reports.GroupingInfo gInfo = rm.getGroupingsDown()[0];
System.debug('Grouping name: ' + gInfo.getName());
System.debug('Grouping sort order: ' + gInfo.getSortOrder());
System.debug('Grouping date granularity: ' + gInfo.getDateGranularity());

// Get aggregates
System.debug('First aggregate: ' + rm.getAggregates()[0]);
System.debug('Second aggregate: ' + rm.getAggregates()[1]);

// Get detail columns
System.debug('Detail columns: ' + rm.getDetailColumns());

// Get report format
System.debug('Report format: ' + rm.getReportFormat());

获取报表数据

可以使用该类获取事实映射,其中包含关联的数据 有一份报告。

ReportResults

要访问事实映射的数据值,您可以映射分组 value 键设置为相应的事实映射键。在以下示例中, 想象一下,您有一个已分组的商机报表 按收月,您已经汇总了金额字段。自 获取 报告:

  1. 使用以下方法获取报表中的第一个缩减分组 并访问第一个对象。ReportResults.getGroupingsDownGroupingValue
  2. 使用该方法从对象中获取分组键值。GroupingValuegetKey
  3. 通过追加到此键值来构造事实映射键。生成的事实图键表示 第一个缩减分组的汇总值。‘!T’
  4. 使用事实地图从报告结果中获取事实地图 钥匙。
  5. 使用方法获取第一个汇总金额值,并 访问第一个对象。ReportFact.getAggregatesSummaryValue
  6. 从第一行的第一个数据单元格中获取字段值 的报表。ReportFactWithDetails.getRows
// Get the report ID
List <Report> reportList = [SELECT Id,DeveloperName FROM Report where 
    DeveloperName = 'Closed_Sales_This_Quarter'];
String reportId = (String)reportList.get(0).get('Id');

// Run a report synchronously
Reports.reportResults results = Reports.ReportManager.runReport(reportId, true);

// Get the first down-grouping in the report
Reports.Dimension dim = results.getGroupingsDown();
Reports.GroupingValue groupingVal = dim.getGroupings()[0];
System.debug('Key: ' + groupingVal.getKey());
System.debug('Label: ' + groupingVal.getLabel());
System.debug('Value: ' + groupingVal.getValue());

// Construct a fact map key, using the grouping key value
String factMapKey = groupingVal.getKey() + '!T';

// Get the fact map from the report results
Reports.ReportFactWithDetails factDetails =
    (Reports.ReportFactWithDetails)results.getFactMap().get(factMapKey);

// Get the first summary amount from the fact map
Reports.SummaryValue sumVal = factDetails.getAggregates()[0];
System.debug('Summary Value: ' + sumVal.getLabel());

// Get the field value from the first data cell of the first row of the report
Reports.ReportDetailRow detailRow = factDetails.getRows()[0];
System.debug(detailRow.getDataCells()[0].getLabel());

筛选报表

要即时获得特定结果,您可以通过 应用程序接口。

通过 API 对筛选器所做的更改不会影响源 报告定义。使用 API,您可以使用最多 20 个自定义字段筛选器进行筛选 并添加筛选器逻辑(例如 AND 和 OR)。但是标准过滤器(如范围), 按行限制进行筛选,并且交叉筛选器不可用。

在筛选报表之前,检查以下筛选器值会很有帮助 在元数据中。

  • 该方法告诉您是否可以筛选字段。ReportTypeColumn.getFilterable
  • 方法 返回字段的所有筛选器值。ReportTypeColumn.filterValues
  • 该方法列出字段 可用于筛选报表的数据类型。ReportManager.dataTypeFilterOperatorMap
  • 该方法列出报表中存在的所有筛选器。ReportMetadata.getReportFilters

您可以在同步或异步报表运行期间筛选报表。

若要筛选报表,请在报表元数据中设置筛选器值,然后运行报表。 以下示例检索报表元数据,重写筛选器值,然后 运行报表。示例:

  1. 使用该方法从元数据中检索报表筛选器对象。ReportMetadata.getReportFilters
  2. 使用方法将筛选器中的值设置为特定日期,然后运行 报告。ReportFilter.setValue
  3. 将筛选器值覆盖到其他日期并运行报表 再。

该示例的输出显示了不同的总值,基于 已应用的日期筛选器。

// Get the report ID
List <Report> reportList = [SELECT Id,DeveloperName FROM Report where 
    DeveloperName = 'Closed_Sales_This_Quarter'];
String reportId = (String)reportList.get(0).get('Id');

// Get the report metadata
Reports.ReportDescribeResult describe = Reports.ReportManager.describeReport(reportId);
Reports.ReportMetadata reportMd = describe.getReportMetadata();

// Override filter and run report
Reports.ReportFilter filter = reportMd.getReportFilters()[0];
filter.setValue('2013-11-01');
Reports.ReportResults results = Reports.ReportManager.runReport(reportId, reportMd);
Reports.ReportFactWithSummaries factSum = 
    (Reports.ReportFactWithSummaries)results.getFactMap().get('T!T');
System.debug('Value for November: ' + factSum.getAggregates()[0].getLabel());

// Override filter and run report
filter = reportMd.getReportFilters()[0];
filter.setValue('2013-10-01');
results = Reports.ReportManager.runReport(reportId, reportMd);
factSum = (Reports.ReportFactWithSummaries)results.getFactMap().get('T!T');
System.debug('Value for October: ' + factSum.getAggregates()[0].getLabel());

解码事实地图

事实地图包含摘要和记录级别的数据 报表的值。

根据报表的运行方式,报表结果中的事实映射可以包含以下值 仅摘要或摘要和详细数据。事实映射值表示为键,其中 可以通过编程方式用于可视化报表数据。事实图键提供 事实地图的每个部分,您可以从中访问摘要和详细数据。

事实映射键的模式因报表格式而异,如下表所示。

报告格式事实地图键模式
表格式的T!T:报表的总计。两者都记录数据 值和总计由此键表示。
总结<First level row grouping_second level row grouping_third level row grouping>!T:T表示行大 总。
矩阵<First level row grouping_second level row grouping>!<First level column grouping_second level column grouping>.

行或列分组中的每个项目都以 开头编号。以下是事实地图键的一些示例:0

事实地图键描述
0!T第一级分组中的第一项。
1!T第一级分组中的第二项。
0_0!T第一级分组中的第一项和第二级分组中的第一项 分组。
0_1!T第一级分组中的第一项和第二级分组中的第二项 分组。

让我们看一下事实映射键如何表示数据的示例,因为它出现在 Salesforce 表格、摘要或矩阵报告。

表格报表事实地图

下面是表格格式的商机报告示例。由于表格报告 没有分组,所有记录级别的数据和摘要都由键表示,键指的是总计。T!T

表格报表事实数据映射键

摘要报告事实地图

此示例显示摘要报告中的值在事实映射中的表示方式。

摘要报告事实图键
事实地图键描述
0!T“勘探”阶段机会价值的摘要。
1_0!T需求分析中制造机会的概率摘要 阶段。

矩阵报告事实地图

下面是矩阵商机报告中数据的一些事实映射键的示例,其中 几个行和列分组。

矩阵事实图键
事实地图键描述
0!02010 年第 4 季度处于探矿阶段的总机会量。
0_0!0_0制造业勘探阶段的总机会量 2010 年 10 月。
2_1!1_1技术领域价值主张阶段的机会总价值 在2011年2月。
T!T报告的总摘要。

测试报告

与所有 Apex 代码一样,通过 Apex 代码的 Salesforce 报告和仪表板 API 需要测试 覆盖。

Reporting Apex 方法不在系统模式下运行,而是在 当前用户(也称为上下文用户或登录用户)。这 方法有权访问当前用户有权访问的任何内容。

在 Apex 测试中,无论报告运行如何,报告运行始终忽略注释 批注是否设置为 或 。这 意味着报告结果将包括预先存在的数据,这些数据 测试未创建。无法禁用报表的批注 执行。若要限制结果,请对报表使用筛选器。SeeAllDatatruefalseSeeAllData

创建报表测试类

以下 示例测试异步和同步报表。每种方法:

  • 创建一个新的 Opportunity 对象,并使用它来设置筛选器 报告。
  • 运行报表。
  • 调用断言来验证数据。

注意

在 Apex 测试中,异步报告仅在 使用该方法停止测试。Test.stopTest

@isTest
public class ReportsInApexTest{

    @isTest(SeeAllData='true')
    public static void testAsyncReportWithTestData() {

      List <Report> reportList = [SELECT Id,DeveloperName FROM Report where
          DeveloperName = 'Closed_Sales_This_Quarter'];
      String reportId = (String)reportList.get(0).get('Id');
      
      // Create an Opportunity object.
      Opportunity opp = new Opportunity(Name='ApexTestOpp', StageName='stage',
          Probability = 95, CloseDate=system.today());
      insert opp;
    
      Reports.ReportMetadata reportMetadata =
          Reports.ReportManager.describeReport(reportId).getReportMetadata();
      
      // Add a filter.
      List<Reports.ReportFilter> filters = new List<Reports.ReportFilter>(); 
      Reports.ReportFilter newFilter = new Reports.ReportFilter();
      newFilter.setColumn('OPPORTUNITY_NAME');
      newFilter.setOperator('equals');
      newFilter.setValue('ApexTestOpp');
      filters.add(newFilter);
      reportMetadata.setReportFilters(filters);
      
      Test.startTest();
       
      Reports.ReportInstance instanceObj =
          Reports.ReportManager.runAsyncReport(reportId,reportMetadata,false);
      String instanceId = instanceObj.getId();
      
      // Report instance is not available yet.
      Test.stopTest();
      // After the stopTest method, the report has finished executing
      // and the instance is available.
     
      instanceObj = Reports.ReportManager.getReportInstance(instanceId);
      System.assertEquals(instanceObj.getStatus(),'Success');
      Reports.ReportResults result = instanceObj.getReportResults();
      Reports.ReportFact grandTotal = (Reports.ReportFact)result.getFactMap().get('T!T');
      System.assertEquals(1,(Decimal)grandTotal.getAggregates().get(1).getValue());
    }
  
    @isTest(SeeAllData='true')
    public static void testSyncReportWithTestData() {
    
      // Create an Opportunity Object.
      Opportunity opp = new Opportunity(Name='ApexTestOpp', StageName='stage',
          Probability = 95, CloseDate=system.today());
      insert opp;
      
      List <Report> reportList = [SELECT Id,DeveloperName FROM Report where
          DeveloperName = 'Closed_Sales_This_Quarter'];
      String reportId = (String)reportList.get(0).get('Id');
      
      Reports.ReportMetadata reportMetadata =
          Reports.ReportManager.describeReport(reportId).getReportMetadata();
      
      // Add a filter.
      List<Reports.ReportFilter> filters = new List<Reports.ReportFilter>(); 
      Reports.ReportFilter newFilter = new Reports.ReportFilter();
      newFilter.setColumn('OPPORTUNITY_NAME');
      newFilter.setOperator('equals');
      newFilter.setValue('ApexTestOpp');
      filters.add(newFilter);
      reportMetadata.setReportFilters(filters);
      
      Reports.ReportResults result =
          Reports.ReportManager.runReport(reportId,reportMetadata,false); 
      Reports.ReportFact grandTotal = (Reports.ReportFact)result.getFactMap().get('T!T');
      System.assertEquals(1,(Decimal)grandTotal.getAggregates().get(1).getValue());
    }
}

Salesforce 连接

Apex 代码可以通过任何 Salesforce Connect 适配器访问外部对象数据。使用 Apex Connector Framework,用于为 Salesforce Connect 开发自定义适配器。自定义适配器可以从外部系统检索数据并在本地合成数据。 Salesforce Connect 在 Salesforce 外部对象中表示该数据,使用户和 Lightning Platform 可与存储在 Salesforce 外部的数据无缝交互 组织。

  • Salesforce Connect 外部对象的 Apex 注意事项 Apex 代码可以通过任何 Salesforce Connect 适配器访问外部对象
    数据,但存在一些要求和限制。
  • 可写的外部对象 默认情况下,外部对象
    是只读的,但您可以使它们可写。这样,Salesforce 用户和 API 就可以通过与组织内的外部对象进行交互来创建、更新和删除存储在组织外部的数据。例如,用户可以查看驻留在 SAP 系统中与 Salesforce 中的帐户关联的所有订单。然后,在不离开 Salesforce 用户界面的情况下,他们可以下新订单或路由现有订单。相关数据在SAP系统中自动创建或更新。
  • 外部变更数据捕获打包和测试 您可以在托管包中分发外部变更数据捕获组件,包括用于测试
    Apex 触发器的框架。特殊行为和限制适用于包装和包装安装。
  • Apex 连接器框架
    入门 要开始使用 Salesforce Connect 的第一个自定义适配器,请创建两个 Apex 类:一个用于扩展类,另一个用于扩展该类。DataSource.ConnectionDataSource.Provider
  • 关于 Apex 连接器框架的关键概念 命名空间提供 Apex 连接器框架
    的类。使用 Apex 连接器框架为 Salesforce Connect 开发自定义适配器。然后,通过 Salesforce Connect 自定义适配器将您的 Salesforce 组织连接到任何位置的任何数据。DataSource
  • Apex Connector 框架
    的注意事项 了解使用 Apex Connector Framework 创建 Salesforce Connect 自定义适配器的限制和注意事项。
  • Apex 连接器框架示例 这些示例
    说明如何使用 Apex 连接器框架为 Salesforce Connect 创建自定义适配器。

Salesforce Connect 外部的 Apex 注意事项 对象

Apex 代码可以通过任何 Salesforce Connect 适配器访问外部对象数据,但某些 适用要求和限制。

  • 这些功能不适用于外部对象。
    • Apex 托管共享
    • Apex 触发器(但是,您可以针对外部更改数据创建触发器 从 OData 4.0 连接捕获事件。
  • 当开发人员使用 Apex 操作外部对象记录时,异步 计时和活动后台队列可最大程度地减少潜在的保存冲突。一个 专门的 Apex 方法和关键字集可处理潜在的计时问题 具有写入执行功能。Apex 还允许您检索删除和 更新插入操作。使用 BackgroundOperation 对象监视作业进度 用于通过 API 或 SOQL 进行写入操作。
  • Database.insertAsync()方法不能在 门户用户的上下文,即使门户用户是社区成员也是如此。 要通过 Apex 添加外部对象记录,请使用方法。Database.insertImmediate()

重要

针对外部数据源运行可迭代的批处理 Apex 作业时, 作业运行时,外部记录存储在 Salesforce 中。数据将从 作业完成时的存储,无论作业是否成功。没有外部数据 在使用 .Database.QueryLocator

  • 如果使用批处理 Apex 来访问外部对象 通过适用于 Salesforce 的 OData 适配器:Database.QueryLocator

可写的外部对象

默认情况下,外部对象是只读的,但您可以使它们可写。这样做 允许 Salesforce 用户和 API 创建、更新和删除存储在组织外部的数据 通过与组织内的外部对象进行交互。例如,用户可以看到所有 驻留在 SAP 系统中且与 Salesforce 中的帐户关联的订单。然后 在不离开 Salesforce 用户界面的情况下,他们可以下新订单或路由 现有订单。相关数据在SAP中自动创建或更新 系统。

对外部数据的访问取决于 Salesforce 与外部数据之间的连接 存储数据的系统。网络延迟和外部可用性 系统可能会在外部执行 Apex 写入或删除操作时引入计时问题 对象。

由于这些连接的复杂性,Apex 无法执行标准、或操作 在外部对象上。相反,Apex 提供了一组专门的数据库方法和 关键字来解决写入执行的潜在问题。DML 插入、更新、 对外部对象的创建和删除操作是异步的或执行的 当满足特定标准时。insert()update()create()此示例使用该方法将新订单异步插入数据库表中。它返回一个包含唯一标识符的对象 用于插入物 工作。

Database.insertAsync()SaveResult

​public void createOrder () {​   
    SalesOrder__x order = new SalesOrder__x ();​   
    Database.SaveResult sr = Database.insertAsync (order);​   
    if (! sr.isSuccess ()) {
        String locator =  Database.getAsyncLocator ( sr );​     
        completeOrderCreation(locator);
    }
​}

注意

通过 Salesforce 用户界面或 API 对外部对象执行写入 是同步的,其工作方式与标准对象和自定义对象相同。

您可以异步对外部对象执行以下 DML 操作 或基于条件:插入记录、更新记录、更新插入记录或删除记录。 使用命名空间中的类获取 异步作业的唯一标识符,或检索 upsert 的结果列表, 删除或保存操作。DataSource

在外部对象上启动 Apex 方法时,将调度作业并将其放置在 后台作业队列。使用 BackgroundOperation 对象可以查看作业状态 用于通过 API 或 SOQL 进行写入操作。监视作业进度和相关错误 组织、提取统计信息、处理批处理作业,或查看指定中发生的错误数 时间段。

有关使用信息和示例,请参阅数据库命名空间和数据源命名空间。

外部变更数据捕获、打包和测试

您可以在托管包中分发外部变更数据捕获组件, 包括用于测试 Apex 触发器的框架。特殊行为和限制 适用于包装和包装安装。

  • 通过选择 将外部更改数据跟踪组件包含在托管包中 Apex 类组件类型列表中的测试。触发器、测试、外部数据 源、外部对象和其他相关资源被引入到包中 分配。
  • 证书不可打包。如果打包外部数据源 指定证书,请确保订阅者组织具有有效的证书 同名。

为了帮助您测试外部变更数据捕获触发的 Apex 类,下面是一个单元 测试触发器对模拟外部更改做出反应的代码示例。

例 触发

​trigger OnExternalProductChangeEventForAudit on Products__ChangeEvent (after insert) {
    if (Trigger.new.size() != 1) return;
    for (Products__ChangeEvent event: Trigger.new) {
         Product_Audit__c audit = new Product_Audit__c(); 
         audit.Name = 'ProductChangeOn' + event.ExternalId;
         audit.Change_Type__c = event.ChangeEventHeader.getChangeType();
         audit.Audit_Price__c = event.Price__c;
         audit.Product_Name__c = event.Name__c;
         insert(audit);
    }
}

顶点测试

​@isTest
public class testOnExternalProductChangeEventForAudit {
    static testMethod void testExternalProductChangeTrigger() { 
            // Create Change Event
           Products__ChangeEvent event = new Products__ChangeEvent();
            // Set Change Event Header Fields
           EventBus.ChangeEventHeader header = new EventBus.ChangeEventHeader();
           header.changeType='CREATE';
           header.entityName='Products__x';
           header.changeOrigin='here';
           header.transactionKey = 'some';
           header.commitUser = 'me';
           event.changeEventHeader = header;
           event.put('ExternalId', 'ParentExternalId');
           event.put('Price__c', 5500);
           event.put('Name__c', 'Coat');
            // Publish the event to the EventBus
           EventBus.publish(event);
           Test.getEventBus().deliver();
            // Perform assertion that the trigger was run
           Product_Audit__c audit = [SELECT name, Audit_Price__c, Product_Name__c FROM Product_Audit__c WHERE name = : 'ProductChangeOn'+ event.ExternalId LIMIT 1]; 
           System.assertEquals('ProductChangeOn'+ event.ExternalId, audit.Name); 
           System.assertEquals(5500, audit.Audit_Price__c); 
           System.assertEquals('Coat', audit.Product_Name__c); 
    }
}

Apex Connector 框架入门

要开始使用 Salesforce Connect 的第一个自定义适配器,请创建两个 Apex 类:一个用于扩展类,一个用于扩展类。

DataSource.ConnectionDataSource.Provider

让我们逐步完成示例自定义适配器的代码。

  1. 创建示例 DataSource.Connection 类 首先,创建一个类
    ,使 Salesforce 能够获取外部系统的架构,并处理外部数据的查询和搜索。DataSource.Connection
  2. 创建示例 DataSource.Provider 类
    现在,您需要一个类来扩展和覆盖 中的几个方法。DataSource.Provider
  3. 设置 Salesforce Connect 以使用您的自定义适配器 创建 和 类后,Salesforce Connect 自定义适配器
    将在“设置”中可用。DataSource.ConnectionDataSource.Provider

创建示例 DataSource.Connection 类

首先,创建一个类 使 Salesforce 能够获取外部系统的架构并处理查询,以及 搜索外部数据。

DataSource.Connection

global class SampleDataSourceConnection
    extends DataSource.Connection {
    global SampleDataSourceConnection(DataSource.ConnectionParams
        connectionParams) {
    }
// Add implementation of abstract methods
// ...

该类包含这些方法。

DataSource.Connection

  • 查询
  • 搜索
  • 同步
  • upsert行
  • deleteRows

同步

当管理员单击外部数据源上的“验证并同步”按钮时,将调用该方法 详情页面。它返回描述结构元数据的信息 外部系统。sync()

注意

更改类的方法不会 自动重新同步任何外部对象。syncDataSource.Connection

// ...
    override global List<DataSource.Table> sync() {
        List<DataSource.Table> tables =
            new List<DataSource.Table>();
        List<DataSource.Column> columns;
        columns = new List<DataSource.Column>();
        columns.add(DataSource.Column.text('Name', 255));
        columns.add(DataSource.Column.text('ExternalId', 255));
        columns.add(DataSource.Column.url('DisplayUrl'));
        tables.add(DataSource.Table.get('Sample', 'Title',
            columns));
        return tables;
    }
// ...

查询

当 SOQL 查询时调用该方法 在外部对象上执行。将自动生成 SOQL 查询,并 当用户在 Salesforce的。是 始终只针对单个表。queryDataSource.QueryContext

此示例自定义适配器使用类中的帮助程序方法对结果进行筛选和排序,并根据 SOQL 查询中的 and 子句。DataSource.QueryUtilsWHEREORDER BY

类及其 帮助程序方法可以在 Salesforce 组织内本地处理查询结果。这 提供课程是为了您的方便,以简化您的 用于初始测试的 Salesforce Connect 自定义适配器。但是,该类及其方法 不支持在使用标注进行检索的生产环境中使用 来自外部系统的数据。完成对外部的过滤和排序 系统,然后再将查询结果发送到 Salesforce。如果可能,请使用 服务器驱动的分页或其他技术,让外部系统确定 根据查询中的 limit 和 offset 子句设置适当的数据子集。DataSource.QueryUtilsDataSource.QueryUtils

// ...
    override global DataSource.TableResult query(
        DataSource.QueryContext context) {
        if (context.tableSelection.columnsSelected.size() == 1 &&
            context.tableSelection.columnsSelected.get(0).aggregation ==
                DataSource.QueryAggregation.COUNT) {
                List<Map<String,Object>> rows = getRows(context);
                List<Map<String,Object>> response =
                    DataSource.QueryUtils.filter(context, getRows(context));
                List<Map<String, Object>> countResponse =
                    new List<Map<String, Object>>();
                Map<String, Object> countRow =
                    new Map<String, Object>();
                countRow.put(
                    context.tableSelection.columnsSelected.get(0).columnName,
                    response.size());
                countResponse.add(countRow);
                return DataSource.TableResult.get(context,
                    countResponse);
        } else {
            List<Map<String,Object>> filteredRows =
                DataSource.QueryUtils.filter(context, getRows(context));
            List<Map<String,Object>> sortedRows =
                DataSource.QueryUtils.sort(context, filteredRows);
            List<Map<String,Object>> limitedRows =
                DataSource.QueryUtils.applyLimitAndOffset(context,
                    sortedRows);
            return DataSource.TableResult.get(context, limitedRows);
        }
    }
// ...

搜索

该方法由 SOSL 查询调用 外部对象或当用户执行 Salesforce 全局搜索时,该搜索还 搜索外部对象。由于搜索可以针对多个对象进行联合, 可以有 已选择多个表。但是,在此示例中,自定义适配器知道 只有一张表。searchDataSource.SearchContext

// ...
    override global List<DataSource.TableResult> search(
            DataSource.SearchContext context) {
        List<DataSource.TableResult> results =
            new List<DataSource.TableResult>();
        for (DataSource.TableSelection tableSelection :
            context.tableSelections) {
            results.add(DataSource.TableResult.get(tableSelection,
                getRows(context)));
        }
        return results;
    }
// ...

以下是 helper 方法 搜索示例调用以从外部系统获取行值。该方法利用其他帮助程序 方法:

getRowsgetRows

  • makeGetCallout向 外部系统。
  • foundRow根据 标注结果中的值。该方法用于对 返回的字段值,例如更改字段名称或修改字段 价值。foundRow

这些方法不包含在此代码片段中,但在完整示例中可用 包含在连接类中。通常,过滤器 from 或将用于减少结果 set,但为简单起见,此示例不使用 context 对象。SearchContextQueryContext

// ...
    // Helper method to get record values from the external system for the Sample table.
    private List<Map<String, Object>> getRows () {
        // Get row field values for the Sample table from the external system via a callout.
        HttpResponse response = makeGetCallout();
        // Parse the JSON response and populate the rows.
        Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped(
                response.getBody());
        Map<String, Object> error = (Map<String, Object>)m.get('error');
        if (error != null) {
            throwException(string.valueOf(error.get('message')));
        }
        List<Map<String,Object>> rows = new List<Map<String,Object>>();
        List<Object> jsonRows = (List<Object>)m.get('value');
        if (jsonRows == null) {
            rows.add(foundRow(m));
        } else {
            for (Object jsonRow : jsonRows) {
                Map<String,Object> row = (Map<String,Object>)jsonRow;
                rows.add(foundRow(row));
            }
        }
        return rows;
    }
// ...

upsert行

该方法在以下情况下调用 创建或更新外部对象记录。您可以创建或更新外部 通过 Salesforce 用户界面或 DML 进行对象记录。以下示例 提供该方法的示例实现。该示例使用传入的表来确定哪个表是 selected 并仅在所选表的名称为 时执行更新插入。upsert 操作分为 插入新记录或更新现有记录。这些 使用标注在外部系统中执行操作。数组从 从标注响应中获得的结果。请注意,因为标注是针对 每一行,此示例可能会达到 Apex 标注限制。upsertRowsupsertRowsUpsertContextSampleDataSource.UpsertResult

// ...
    global override List<DataSource.UpsertResult> upsertRows(DataSource.UpsertContext 
            context) {
       if (context.tableSelected == 'Sample') {
           List<DataSource.UpsertResult> results = new List<DataSource.UpsertResult>();
           List<Map<String, Object>> rows = context.rows;
           
           for (Map<String, Object> row : rows){
              // Make a callout to insert or update records in the external system.
              HttpResponse response;
              // Determine whether to insert or update a record.
              if (row.get('ExternalId') == null){
                 // Send a POST HTTP request to insert new external record.
                 // Make an Apex callout and get HttpResponse.
                 response = makePostCallout(
                     '{"name":"' + row.get('Name') + '","ExternalId":"' + 
                     row.get('ExternalId') + '"');
              }
              else {
                 // Send a PUT HTTP request to update an existing external record.
                 // Make an Apex callout and get HttpResponse.
                 response = makePutCallout(
                     '{"name":"' + row.get('Name') + '","ExternalId":"' + 
                     row.get('ExternalId') + '"',
                     String.valueOf(row.get('ExternalId')));
              }
         
              // Check the returned response.
              // Deserialize the response.
              Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped(
                      response.getBody());
              if (response.getStatusCode() == 200){
                  results.add(DataSource.UpsertResult.success(
                          String.valueOf(m.get('id'))));
              } 
              else {
                 results.add(DataSource.UpsertResult.failure(
                         String.valueOf(m.get('id')), 
                         'The callout resulted in an error: ' + 
                         response.getStatusCode()));
              }
           } 
           return results;
       } 
       return null;
    }
// ...

deleteRows

该方法在以下情况下调用 外部对象记录将被删除。您可以通过以下方式删除外部对象记录 Salesforce 用户界面或 DML。以下示例提供了一个示例 方法的实现。 该示例使用传入的表来确定选择了哪个表,并且仅当名称 所选表为 。删除是 在外部系统中使用每个外部 ID 的标注执行。填充 从标注响应获得的结果。请注意,因为标注是 针对每个 ID 创建,此示例可能会达到 Apex 标注限制。deleteRowsdeleteRowsDeleteContextSampleDataSource.DeleteResult

// ...
    global override List<DataSource.DeleteResult> deleteRows(DataSource.DeleteContext 
            context) {
       if (context.tableSelected == 'Sample'){
           List<DataSource.DeleteResult> results = new List<DataSource.DeleteResult>();
           for (String externalId : context.externalIds){
              HttpResponse response = makeDeleteCallout(externalId);
              if (response.getStatusCode() == 200){
                 results.add(DataSource.DeleteResult.success(externalId));
              } 
              else {
                 results.add(DataSource.DeleteResult.failure(externalId, 
                         'Callout delete error:' 
                         + response.getBody()));
              }
           }
           return results;
       }
       return null;
     }
// ...

创建示例 DataSource.Provider 类

现在,您需要一个类来扩展和覆盖 中的一些方法。

DataSource.Provider

您的班级通知 Salesforce 支持或需要的功能和身份验证功能 连接到外部系统。DataSource.Provider

global class SampleDataSourceProvider extends DataSource.Provider {

如果外部系统需要身份验证,Salesforce 可以提供身份验证 来自外部数据源定义或用户个人设置的凭据。为 但是,简单来说,此示例声明外部系统不需要 认证。为此,它将作为列表中的唯一条目返回 身份验证功能。AuthenticationCapability.ANONYMOUS

override global List<DataSource.AuthenticationCapability>
        getAuthenticationCapabilities() {
        List<DataSource.AuthenticationCapability> capabilities =
            new List<DataSource.AuthenticationCapability>();
        capabilities.add(
            DataSource.AuthenticationCapability.ANONYMOUS);
        return capabilities;
    }

此示例还声明外部系统允许 SOQL 查询、SOSL 查询、 Salesforce 搜索、更新插入数据和删除数据。

  • 为了允许 SOQL,该示例声明了该功能。DataSource.Capability.ROW_QUERY
  • 为了允许 SOSL 和 Salesforce 搜索,该示例声明了该功能。DataSource.Capability.SEARCH
  • 为了允许更新插入外部数据,该示例声明了 和 功能。DataSource.Capability.ROW_CREATEDataSource.Capability.ROW_UPDATE
  • 为了允许删除外部数据,该示例声明了该功能。DataSource.Capability.ROW_DELETE
override global List<DataSource.Capability> getCapabilities()
    {
        List<DataSource.Capability> capabilities = new
            List<DataSource.Capability>();
        capabilities.add(DataSource.Capability.ROW_QUERY);
        capabilities.add(DataSource.Capability.SEARCH);
        capabilities.add(DataSource.Capability.ROW_CREATE);
        capabilities.add(DataSource.Capability.ROW_UPDATE);
        capabilities.add(DataSource.Capability.ROW_DELETE);
        return capabilities;
    }

最后,该示例标识获取外部系统架构的类,以及 处理外部数据的查询和搜索。SampleDataSourceConnection

override global DataSource.Connection getConnection(
        DataSource.ConnectionParams connectionParams) {
        return new SampleDataSourceConnection(connectionParams);
    }
}

设置 Salesforce Connect 以使用您的自定义适配器

创建 和 类后,Salesforce Connect 自定义适配器在安装程序中可用。

DataSource.ConnectionDataSource.Provider

完成“设置 Salesforce Connect to Access”中描述的任务 Salesforce 帮助中的“使用自定义适配器的外部数据”。要将外部对象的写入功能添加到适配器,请执行以下操作:

  1. 使此适配器的外部数据源可写。请参阅“定义 Salesforce 帮助中的 Salesforce Connect – 自定义适配器”。
  2. 实现 和 方法 适配器。有关详细信息,请参阅连接类。DataSource.Connection.upsertRows()DataSource.Connection.deleteRows()

关于 Apex 连接器框架的关键概念

命名空间提供类 用于 Apex Connector 框架。使用 Apex 连接器框架开发自定义适配器 适用于 Salesforce Connect。然后,通过 Salesforce 将您的 Salesforce 组织连接到任何地方的任何数据 连接自定义适配器。

DataSource

我们建议您了解一些关键概念,以帮助您使用 Apex 连接器 有效的框架。

  • Salesforce Connect 外部对象的外部 ID 当您使用 Salesforce Connect 的自定义适配器访问外部数据时,外部对象
    上的外部 ID 标准字段的值来自命名的 .DataSource.ColumnExternalId
  • Salesforce Connect 自定义适配器的身份验证
    您的类声明哪些类型的凭据可用于向外部系统进行身份验证。DataSource.Provider
  • Salesforce Connect 自定义适配器的标注
    就像任何其他 Apex 代码一样,Salesforce Connect 自定义适配器可以进行标注。如果与外部系统的连接需要身份验证,请将身份验证参数合并到标注中。
  • 使用 Apex 连接器框架
    进行分页 在用户界面中显示大量记录时,Salesforce 会将记录集分成多个批次并显示一个批次。然后,您可以分页浏览这些批次。但是,Salesforce Connect 的自定义适配器不会自动支持任何类型的分页。若要支持通过自定义适配器获取的外部对象数据进行分页,请实现服务器驱动或客户端驱动的分页。
  • queryMore with the Apex Connector Framework
    适用于 Salesforce Connect 的自定义适配器不会自动支持 API 查询中的方法。但是,您的实现必须能够将大型结果集分解为批处理,并使用 SOAP API 中的方法循环访问它们。默认批处理大小为 500 条记录,但查询开发人员可以在查询调用中以编程方式调整该值。queryMorequeryMore
  • Salesforce Connect 自定义适配器的聚合
    如果收到查询,则所选列的属性中具有该值。所选列在 for 的属性中提供。COUNT()QueryAggregation.COUNTaggregationcolumnsSelectedtableSelectionDataSource.QueryContext
  • Apex 连接器框架
    中的过滤器包含一个 .可以有多个 .每个属性都有一个属性,该属性表示 SOQL 或 SOSL 查询中的子句。DataSource.QueryContextDataSource.TableSelectionDataSource.SearchContextTableSelectionTableSelectionfilterWHERE

Salesforce Connect 外部对象的外部 ID

当您使用 Salesforce Connect 的自定义适配器访问外部数据时, 外部对象上的“外部 ID”标准字段的值来自命名的 .

DataSource.ColumnExternalId

每个外部对象都有一个外部 ID 标准字段。它的价值观 唯一标识组织中的每个外部对象记录。当外部对象是 外部查找关系中的父项,外部 ID 标准字段用于 标识子记录。

重要

  • 自定义适配器的 Apex 代码必须声明命名并提供其值。DataSource.ColumnExternalId
  • 不要使用敏感数据作为 外部 ID 标准字段或指定为名称字段的字段,因为 Salesforce 有时会存储这些值。
  • 外部查找 子记录上的关系字段存储并显示外部 父记录的 ID 值。
  • 为 仅供内部使用,Salesforce 存储每个 ID 的外部 ID 值 从外部系统检索到的行。 此行为不会 应用于与高数据量关联的外部对象 外部数据源。

示例类的摘录显示了名为 .

DataSource.ConnectionDataSource.ColumnExternalId

override global List<DataSource.Table> sync() {
        List<DataSource.Table> tables =
        new List<DataSource.Table>();
    List<DataSource.Column> columns;
    columns = new List<DataSource.Column>();
    columns.add(DataSource.Column.text('title', 255));
    columns.add(DataSource.Column.text('description',255));
    columns.add(DataSource.Column.text('createdDate',255));
    columns.add(DataSource.Column.text('modifiedDate',255));
    columns.add(DataSource.Column.url('selfLink'));
    columns.add(DataSource.Column.url('DisplayUrl'));
    columns.add(DataSource.Column.text('ExternalId',255));
    tables.add(DataSource.Table.get('googleDrive','title',
        columns));
    return tables;
    }

Salesforce Connect 自定义适配器的身份验证

你的班级声明了什么 凭据类型可用于向外部系统进行身份验证。

DataSource.Provider

如果 DataSource.Provider 类的扩展返回 DataSource.AuthenticationCapability 值,这些值指示 支持身份验证,DataSource.Connection 类在构造函数中使用 DataSource.ConnectionParams 实例进行实例化。实例中的身份验证凭据取决于身份 Salesforce 中外部数据源定义的 Type 字段。

DataSource.ConnectionParams

  • 如果“身份类型”设置为 ,则凭据来自外部数据 源定义。Named Principal
  • 如果“身份类型”设置为:Per User
    • 对于查询和搜索, 凭据特定于调用查询的当前用户 或搜索。凭据来自用户的身份验证 外部系统的设置。
    • 对于管理连接, 例如同步外部系统的架构,凭据就来了 从外部数据源定义。
  • 适用于 Salesforce Connect 自定义适配器的
    OAuth 如果您使用 OAuth 2.0 访问外部数据,请了解如何避免访问令牌过期导致的访问中断。

Salesforce Connect 自定义适配器的 OAuth

如果您使用 OAuth 2.0 访问外部数据,请了解如何避免访问中断 由过期的访问令牌导致。某些外部系统使用过期且需要刷新的 OAuth 访问令牌。我们 在以下情况下,可以根据需要自动刷新访问令牌:

  • 用户或外部数据源具有来自先前 OAuth 的有效刷新令牌 流。
  • 类中的 sync、query 或 search 方法会引发 .DataSource.ConnectionDataSource.OAuthTokenExpiredException

我们使用用户或外部数据源的相关 OAuth 凭据进行协商 并刷新令牌。该类是使用 我们提供的 到构造函数。然后重新调用搜索或查询。DataSource.ConnectionDataSource.ConnectionParams

如果身份验证提供程序未提供刷新令牌,则访问外部 当当前访问令牌过期时,系统将丢失。如果出现警告消息 外部数据源详细信息页面,请咨询您的 OAuth 提供商以获取有关以下内容的信息 请求脱机访问或刷新令牌。

对于某些身份验证提供程序,请求脱机访问就像添加 范围。例如,要从 Salesforce 身份验证提供商请求脱机访问, 添加到 Salesforce 组织中身份验证提供商定义的默认范围字段。refresh_token

对于其他身份验证提供程序,必须在身份验证中请求脱机访问 URL 作为查询参数。例如,使用 Google 时,将 Append 附加到 Authorize Endpoint Salesforce 中身份验证提供商定义上的 URL 字段 组织。若要编辑授权终结点,请选择“打开 ID” 在 Provider Type 字段中连接 身份验证提供程序。有关详细信息,请参阅“配置 OpenID Connect 身份验证” 提供商”。?access_type=offline

Salesforce Connect 自定义适配器的标注

就像任何其他 Apex 代码一样,Salesforce Connect 自定义适配器可以进行标注。 如果与外部系统的连接需要身份验证,请合并 身份验证参数添加到标注中。

身份验证参数封装在对象中,并提供给类的构造函数。ConnectionParamsDataSource.Connection例如,如果您的连接需要 OAuth 访问令牌,请使用类似于 以后。

public HttpResponse getResponse(String url) {
    Http httpProtocol = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndPoint(url);
    request.setMethod('GET');
    request.setHeader('Authorization', 'Bearer ' + 
            this.connectionInfo.oauthToken);
    HttpResponse response = httpProtocol.send(request);
    return response;
}

如果您的连接需要基本密码身份验证,请使用类似于 以后。

public HttpResponse getResponse(String url) {
    Http httpProtocol = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndPoint(url);
    request.setMethod('GET');
    string encodedHeaderValue = EncodingUtil.base64Encode(Blob.valueOf(
            this.connectioninfo.username + ':' + 
            this.connectionInfo.password));
    request.setHeader('Authorization', 'Basic ' + encodedHeaderValue);
    HttpResponse response = httpProtocol.send(request);
    return response;
}

命名凭据作为 Salesforce Connect Custom 的标注端点 适配器

Salesforce Connect 自定义适配器获取存储的相关凭据 在 Salesforce 中,只要有需要。但是,您的 Apex 代码必须应用这些 所有标注的凭据,但将命名凭据指定为 标注端点。命名凭据允许 Salesforce 处理身份验证 逻辑,这样你的代码就不必这样做了。

如果所有自定义适配器的标注都使用命名凭据,则可以设置外部 数据源的“身份验证协议”字段设置为“否” 身份验证。命名凭据会添加相应的 证书,并且可以将标准授权标头添加到标注中。你也 不需要为定义为 命名凭据。

使用 Apex 连接器框架进行分页

在用户界面中显示大量记录时,Salesforce 会破坏 设置为批次并显示一个批次。然后,您可以分页浏览这些批次。然而 Salesforce Connect 的自定义适配器不会自动支持任何类型的分页。自 支持通过自定义适配器获取的外部对象数据进行分页,实现 服务器驱动或客户端驱动的分页。

使用服务器驱动的分页,外部系统控制分页并忽略任何批处理 在查询中指定的边界或页面大小。若要启用服务器驱动的分页,请执行以下操作: 在类中声明功能。 此外,您的 Apex 代码必须生成一个查询令牌,并使用它来确定和获取 下一批结果。QUERY_PAGINATION_SERVER_DRIVENDataSource.Provider

使用客户端驱动的分页,可以使用 and 子句对结果集进行分页。因素 和属性来确定要返回的行。例如 假设结果集有 20 行,数值介于 1 到 20 之间。如果我们要求 of 和 of ,我们希望得到带有 ID – 的行。我们建议您执行所有操作 在外部系统中,在 Apex 之外,使用外部方法进行过滤 系统支持。LIMITOFFSEToffsetmaxResultsDataSource.QueryContextExternalIDoffset5maxResults5610

queryMore 与 Apex 连接器框架

Salesforce Connect 的自定义适配器不会自动支持 API 查询中的方法。但是,您的 实现必须能够将大型结果集分解为多个批次并对其进行迭代 通过使用 SOAP API 中的方法。这 默认批大小为 500 条记录,但查询开发人员可以调整该值 在查询调用中以编程方式。

queryMorequeryMore要支持 ,您的实现必须 指示存在的数据是否多于当前批处理中的数据。当闪电 平台知道存在更多数据,您的API查询将返回一个类似于 以后。

queryMoreQueryResult

{
         "totalSize" => -1,
              "done" => false,
    "nextRecordsUrl" => "/services/data/v32.0/query/01gxx000000B5OgAAK-2000",
           "records" => [
        [   0] {
            "attributes" => {
                "type" => "Sample__x",
                 "url" => 
                     "/services/data/v32.0/sobjects/Sample__x/x06xx0000000001AAA"
            },
            "ExternalId" => "id0"
        },
        [   1] {
            "attributes" => {
                "type" => "Sample__x",
                 "url" => 
                     "/services/data/v32.0/sobjects/Sample__x/x06xx0000000002AAA"
            },
…
}
  • 使用服务器驱动的分页
    支持 queryMore 使用服务器驱动的分页时,外部系统控制分页并忽略查询中指定的任何批处理边界或页面大小。若要启用服务器驱动的分页,请在类中声明该功能。QUERY_PAGINATION_SERVER_DRIVENDataSource.Provider
  • 通过使用客户端驱动的分页
    支持 queryMore 在客户端驱动的分页中,可以使用 and 子句对结果集进行分页。LIMITOFFSET

支持查询More by Using 服务器驱动的寻呼

使用服务器驱动的分页时,外部系统控制分页并忽略任何 查询中指定的批处理边界或页面大小。启用服务器驱动 分页,在类中声明功能。

QUERY_PAGINATION_SERVER_DRIVENDataSource.Provider

当返回的没有 包含整个结果集,必须提供一个值。查询 token 是我们临时存储的任意字符串。当我们要求下一批时 中,我们将查询令牌传递回 .您的 Apex 代码必须使用 该查询令牌,用于确定哪些行属于下一批结果。DataSource.TableResultTableResultqueryMoreTokenDataSource.QueryContext

当自定义适配器返回最终批处理时,它不得返回 中的值。queryMoreTokenTableResult

使用客户端驱动支持 queryMore 寻呼

使用客户端驱动的分页,您可以使用 and 子句对结果进行分页 集。

LIMITOFFSET

如果外部系统可以返回每个查询的结果集的总大小, 在类中声明功能。确保 每个搜索或查询都返回值 在。如果总大小 大于批处理中返回的行数,我们生成一个链接并将标志设置为 .我们还将 到你的值 供应。QUERY_TOTAL_SIZEDataSource.ProvidertotalSizeDataSource.TableResultnextRecordsUrldonefalsetotalSizeTableResult

如果外部系统无法返回每个查询的总大小,请不要在类中声明该功能。每当我们进行查询时 通过您的自定义适配器,我们要求增加一行。例如,如果您运行查询,我们调用 具有 属性设置为 6 的对象上的方法。存在或 结果集中缺少第六行表示是否有更多数据可用。我们 但是,假设我们查询的数据集在查询之间不会更改。如果 数据集在查询之间发生变化,您可能会看到重复的行或无法获取所有行 结果。QUERY_TOTAL_SIZEDataSource.ProviderSELECT ExternalId FROM Sample LIMIT 5queryDataSource.ConnectionDataSource.QueryContextmaxResults

归根结底,当您检索小数据时,访问外部数据最有效 数据量和您查询的数据集很少更改。

Salesforce Connect 自定义适配器的聚合

如果您收到查询,则选中 列在其属性中具有值。所选列是 在 for 的属性中提供。

COUNT()QueryAggregation.COUNTaggregationcolumnsSelectedtableSelectionDataSource.QueryContext

下面的示例演示如何应用属性的值来处理查询。aggregationCOUNT()

// Handle COUNT() queries
if (context.tableSelection.columnsSelected.size() == 1 &&      
    context.tableSelection.columnsSelected.get(0).aggregation == 
        QueryAggregation.COUNT) {
    List<Map<String, Object>> countResponse = new List<Map<String, Object>>();
    Map<String, Object> countRow = new Map<String, Object>();
    countRow.put(context.tableSelection.columnsSelected.get(0).columnName, 
    response.size());
    countResponse.add(countRow);
    return countResponse;
}

聚合查询仍然可以具有筛选器,因此查询方法可以实现如下方式 以下示例支持基本查询,无论是否使用筛选器。aggregation

override global DataSource.TableResult query(DataSource.QueryContext context) {
    List<Map<String,Object>> rows = retrieveData(context);
    List<Map<String,Object>> response = postFilterRecords(
            context.tableSelection.filter, rows);
    if (context.tableSelection.columnsSelected.size() == 1 &&        
        context.tableSelection.columnsSelected.get(0).aggregation ==   
                DataSource.QueryAggregation.COUNT) {
        List<Map<String, Object>> countResponse = new List<Map<String, 
                Object>>();
        Map<String, Object> countRow = new Map<String, Object>();
        countRow.put(context.tableSelection.columnsSelected.get(0).columnName, 
                response.size());
        countResponse.add(countRow);
        return DataSource.TableResult.get(context, countResponse);
    }
    return DataSource.TableResult.get(context, response);
}

Apex 连接器框架中的过滤器

包含一个 .可以有多个 .每个都有一个属性 表示 SOQL 或 SOSL 中的子句 查询。

DataSource.QueryContextDataSource.TableSelectionDataSource.SearchContextTableSelectionTableSelectionfilterWHERE

例如,当用户转到外部对象的记录详细信息页面时,将执行 your 。背后 场景中,我们生成一个类似于以下内容的 SOQL 查询。DataSource.Connection

SELECT columnNames 
FROM externalObjectApiName 
WHERE ExternalId = 'selectedExternalObjectExternalId'

此 SOQL 查询会导致调用类上的方法。 以下代码可以检测到这种情况。queryDataSource.Connection

if (context.tableSelection.filter != null) {
    if (context.tableSelection.filter.type == DataSource.FilterType.EQUALS 
        && 'ExternalId' ==  context.tableSelection.filter.columnName 
        && context.tableSelection.filter.columnValue instanceOf String) {
        String selection = (String)context.tableSelection.filter.columnValue;
        return DataSource.TableResult.get(true, null, 
                tableSelection.tableSelected, findSingleResult(selection));
    }
}

此代码示例假定您实现了一个返回单个记录的方法,该方法给定选定的 .确保您的代码获取 与请求的 .findSingleResultExternalIdExternalId

  • 在 Apex 连接器框架
    中评估过滤器 如果某行与过滤器描述的条件匹配,则过滤器的计算结果为 true。
  • Apex 连接器框架
    筛选器中的复合筛选器可以具有子筛选器,这些子筛选器存储在属性中。subfilters

评估 Apex 连接器框架中的过滤器

如果某行与以下条件匹配,则筛选器将该行的计算结果为 true: 过滤器描述。

例如,假设 a 已设置为 、 和 设置为 。将返回远程表中列条目等于 42 的任何行。DataSource.FiltercolumnNamemeaningOfLifecolumnValue42typeEQUALSmeaningOfLife

相反,假设筛选器已设置为 、 和 设置为 。我们将构造一个对象,其中包含值小于 3 的所有行。typeLESS_THANcolumnValue3columnNamenumericColDataSource.TableResultnumericCol

若要提高性能,请在外部系统中执行所有筛选。你可以,为了 例如,将对象转换为 SQL 或 OData 查询,或将其映射到 SOAP 查询上的参数。如果外部系统返回 大量数据,您在 Apex 代码中进行过滤,您很快就会超过 调速器限制。Filter

如果无法在外部系统中执行所有过滤,请尽可能多地在那里执行 并返回尽可能少的数据。然后筛选较小的数据集合 您的 Apex 代码。

Apex 连接器框架中的复合过滤器

筛选器可以具有子筛选器,这些子筛选器存储在属性中。

subfilters

如果筛选器具有子项,则筛选器必须 以下其中一项。type

过滤器类型描述
AND_我们返回与所有子筛选器匹配的所有行。
OR_我们返回与任何子筛选器匹配的所有行。
NOT_筛选器反转其子筛选器计算行的方式。过滤器 此类型只能有一个子筛选器。

此代码示例演示如何处理复合筛选器。

override global DataSource.TableResult query(DataSource.QueryContext context) {
    // Call out to an external data source and retrieve a set of records.
    // We should attempt to get as much information as possible about the 
    // query from the QueryContext, to minimize the number of records 
    // that we return.
    List<Map<String,Object>> rows = retrieveData(context);
    
    // This only filters the results. Anything in the query that we don’t 
    // currently support, such as aggregation or sorting, is ignored.
    return DataSource.TableResult.get(context, postFilterRecords(
        context.tableSelection.filter, rows));
}

private List<Map<String,Object>> retrieveData(DataSource.QueryContext context) {
    // Call out to an external data source. Form the callout so that
    // it filters as much as possible on the remote site,
    // based on the parameters in the QueryContext.
    return ...;
}

private List<Map<String,Object>> postFilterRecords(
    DataSource.Filter filter, List<Map<String,Object>> rows) {
    if (filter == null) {
        return rows;
    }
    DataSource.FilterType type = filter.type;
    List<Map<String,Object>> retainedRows = new List<Map<String,Object>>();
    if (type == DataSource.FilterType.NOT_) {
        // We expect one Filter in the subfilters.
        DataSource.Filter subfilter = filter.subfilters.get(0);
        for (Map<String,Object> row : rows) {
            if (!evaluate(filter, row)) {
                retainedRows.add(row);
            }
        }
        return retainedRows;
    } else if (type == DataSource.FilterType.AND_) {
        // For each filter, find all matches; anything that matches ALL filters 
        // is returned.
        retainedRows = rows;
        for (DataSource.Filter subfilter : filter.subfilters) {
            retainedRows = postFilterRecords(subfilter, retainedRows);
        }
        return retainedRows;
    } else if (type == DataSource.FilterType.OR_) {
        // For each filter, find all matches. Anything that matches 
        // at least one filter is returned.
        for (DataSource.Filter subfilter : filter.subfilters) {
            List<Map<String,Object>> matchedRows = postFilterRecords(
                subfilter, rows);
            retainedRows.addAll(matchedRows);
        }
        return retainedRows;
    } else {
        // Find all matches for this filter in our collection of records.
        for (Map<String,Object> row : rows) {
            if (evaluate(filter, row)) {
                retainedRows.add(row);
            }
        }
        return retainedRows;
    }
}

private Boolean evaluate(DataSource.Filter filter, Map<String,Object> row) {
    if (filter.type == DataSource.FilterType.EQUALS) {
        String columnName = filter.columnName;
        Object expectedValue = filter.columnValue;
        Object foundValue = row.get(columnName);
        return expectedValue.equals(foundValue);
    } else {
        // Throw an exception; implementing other filter types is left
        // as an exercise for the reader.
        throwException('Unexpected filter type: ' + filter.type);
    }
    return false;
}

Apex 连接器框架的注意事项

了解创建 Salesforce Connect 自定义的限制和注意事项 具有 Apex 连接器框架的适配器。

  • 如果更改并保存类,请重新保存 相应的类。 否则,在定义外部数据源时,自定义适配器不会 显示为“类型”字段的选项。DataSource.ConnectionDataSource.Provider此外, 关联的外部对象的自定义选项卡不再显示在 Salesforce UI 中。
  • 包含自定义的 Apex 代码中不允许 DML 操作 适配器。
  • 确保您了解外部系统 API 的限制。例如 某些外部系统仅接受最多 40 行的请求。
  • Apex 数据类型限制:
    • 双精度 – 超过 18 位有效数字时,该值将失去精度。为 精度更高,使用小数而不是双精度。
    • 字符串 – 如果长度大于 255 个字符,则字符串为 映射到 Salesforce 中的长文本区域字段。
  • Salesforce Connect 的自定义适配器受到与任何适配器相同的限制 其他 Apex 代码。例如:
    • 所有 Apex 调速器限制均适用。
    • 测试方法不支持 Web 服务标注。执行 Web 的测试 服务标注失败。对于演示如何避免这些失败的示例 通过返回模拟响应进行测试,请参阅适用于 Salesforce 的 Google 云端硬盘™自定义适配器 连接。
  • 在 Apex 测试中,使用动态 SOQL 进行查询 外部对象。对外部对象执行静态 SOQL 查询的测试 失败。

Apex 连接器框架示例

这些示例说明了如何使用 Apex Connector Framework 创建自定义 Salesforce Connect 的适配器。

  • 适用于 Salesforce Connect 的 Google Drive 自定义适配器 此示例说明如何使用标注和 OAuth 连接到外部系统,在本例中为 Google Drive™™ 在线存储服务。
    该示例还演示了如何通过返回测试方法的模拟响应来避免 Web 服务标注的测试失败。
  • 适用于 Salesforce Connect 的 Google 图书自定义适配器 此示例说明了如何解决外部系统 API 的要求和限制:在本例中为 Google 图书™ API 系列。
  • Salesforce Connect 的环回自定义适配器 此示例说明如何处理查询中的筛选。
    为简单起见,此示例将 Salesforce 组织作为外部系统连接到自身。
  • 适用于 Salesforce Connect 的 GitHub 自定义适配器 此示例说明如何支持间接查找关系。
    间接查找关系将子外部对象链接到父标准对象或自定义对象。
  • 适用于 Salesforce Connect 的 Stack Overflow 自定义适配器 此示例说明如何支持外部查找关系和多个表。
    外部查找关系将子标准对象、自定义对象或外部对象链接到父外部对象。每个表都可以成为 Salesforce 组织中的外部对象。

适用于 Salesforce Connect 的 Google Drive™ 自定义适配器

此示例说明如何使用标注和 OAuth 连接到外部 系统,在本例中为 Google Drive™ 在线存储服务。该示例还 演示如何通过返回 Web 服务标注的模拟响应来避免测试失败 测试方法。

要使此示例可靠地工作,请在设置 OAuth 时请求脱机访问,以便 Salesforce 可以获取并维护连接的刷新令牌。

DriveDataSourceConnection 类

/**
 *   Extends the DataSource.Connection class to enable 
 *   Salesforce to sync the external system’s schema 
 *   and to handle queries and searches of the external data. 
 **/
global class DriveDataSourceConnection extends
    DataSource.Connection {
    private DataSource.ConnectionParams connectionInfo;
    
    /**
     *   Constructor for DriveDataSourceConnection.
     **/
    global DriveDataSourceConnection(
        DataSource.ConnectionParams connectionInfo) {
        this.connectionInfo = connectionInfo;
    }
    
    /**
     *   Called when an external object needs to get a list of 
     *   schema from the external data source, for example when 
     *   the administrator clicks “Validate and Sync” in the 
     *   user interface for the external data source.   
     **/
    override global List<DataSource.Table> sync() {
        List<DataSource.Table> tables =
            new List<DataSource.Table>();
        List<DataSource.Column> columns;
        columns = new List<DataSource.Column>();
        columns.add(DataSource.Column.text('title', 255));
        columns.add(DataSource.Column.text('description',255));
        columns.add(DataSource.Column.text('createdDate',255));
        columns.add(DataSource.Column.text('modifiedDate',255));
        columns.add(DataSource.Column.url('selfLink'));
        columns.add(DataSource.Column.url('DisplayUrl'));
        columns.add(DataSource.Column.text('ExternalId',255));
        tables.add(DataSource.Table.get('googleDrive','title',
            columns));
        return tables;
    }

    /**
     *   Called to query and get results from the external 
     *   system for SOQL queries, list views, and detail pages 
     *   for an external object that’s associated with the 
     *   external data source.
     *   
     *   The QueryContext argument represents the query to run 
     *   against a table in the external system.
     *   
     *   Returns a list of rows as the query results.
     **/
    override global DataSource.TableResult query(
        DataSource.QueryContext context) {
        DataSource.Filter filter = context.tableSelection.filter;
        String url;
        if (filter != null) {
            String thisColumnName = filter.columnName;
            if (thisColumnName != null && 
                    thisColumnName.equals('ExternalId'))
                url = 'https://www.googleapis.com/drive/v2/'
                + 'files/' + filter.columnValue;
            else
                url = 'https://www.googleapis.com/drive/v2/'
                + 'files';
        } else {
            url = 'https://www.googleapis.com/drive/v2/' 
            + 'files';
        }

        /**
         * Filters, sorts, and applies limit and offset clauses.
         **/
        List<Map<String, Object>> rows = 
            DataSource.QueryUtils.process(context, getData(url));
        return DataSource.TableResult.get(true, null,
            context.tableSelection.tableSelected, rows);
    }

    /**
     *   Called to do a full text search and get results from
     *   the external system for SOSL queries and Salesforce
     *   global searches.
     *   
     *   The SearchContext argument represents the query to run 
     *   against a table in the external system.
     *   
     *   Returns results for each table that the SearchContext 
     *   requested to be searched.
     **/
    override global List<DataSource.TableResult> search(
        DataSource.SearchContext context) {
        List<DataSource.TableResult> results =
            new List<DataSource.TableResult>();

        for (Integer i =0;i< context.tableSelections.size();i++) {
            String entity = context.tableSelections[i].tableSelected;
            String url = 
                'https://www.googleapis.com/drive/v2/files'+
                '?q=fullText+contains+\''+context.searchPhrase+'\'';
            results.add(DataSource.TableResult.get(
                true, null, entity, getData(url)));
        }

        return results;
    }

    /**
     *   Helper method to parse the data.
     *   The url argument is the URL of the external system.
     *   Returns a list of rows from the external system.
     **/
    public List<Map<String, Object>> getData(String url) {
        String response = getResponse(url);

        List<Map<String, Object>> rows =
            new List<Map<String, Object>>();

        Map<String, Object> responseBodyMap = (Map<String, Object>)
            JSON.deserializeUntyped(response);

        /**
         *   Checks errors.
         **/
        Map<String, Object> error =
            (Map<String, Object>)responseBodyMap.get('error');
        if (error!=null) {
            List<Object> errorsList =
                (List<Object>)error.get('errors');
            Map<String, Object> errors =
                (Map<String, Object>)errorsList[0];
            String errorMessage = (String)errors.get('message');
            throw new DataSource.OAuthTokenExpiredException(errorMessage);
        }

        List<Object> fileItems=(List<Object>)responseBodyMap.get('items');
        if (fileItems != null) {
            for (Integer i=0; i < fileItems.size(); i++) {
                Map<String, Object> item = 
                    (Map<String, Object>)fileItems[i];
                rows.add(createRow(item));  
            }
        } else {
            rows.add(createRow(responseBodyMap));
        }

        return rows;
    }

    /**
     *   Helper method to populate the External ID and Display 
     *   URL fields on external object records based on the 'id' 
     *   value that’s sent by the external system.
     *   
     *   The Map<String, Object> item parameter maps to the data 
     *   that represents a row.
     *   
     *   Returns an updated map with the External ID and 
     *   Display URL values.
     **/
    public Map<String, Object> createRow(
        Map<String, Object> item){
        Map<String, Object> row = new Map<String, Object>();
        for ( String key : item.keySet() ) {
            if (key == 'id') {
                row.put('ExternalId', item.get(key));
            } else if (key=='selfLink') {
                row.put(key, item.get(key));
                row.put('DisplayUrl', item.get(key));
            } else {
                row.put(key, item.get(key));
            }
        }
        return row;
    }
    
    static String mockResponse = '{' +
      '  "kind": "drive#file",' +
      '  "id": "12345",' +
      '  "selfLink": "files/12345",' +
      '  "title": "Mock File",' +
      '  "mimeType": "application/text",' +
      '  "description": "Mock response that’s used during tests",' +
      '  "createdDate": "2016-04-20",' +
      '  "modifiedDate": "2016-04-20",' +
      '  "version": 1' +
      '}';
    
    /**
     *   Helper method to make the HTTP GET call.
     *   The url argument is the URL of the external system.
     *   Returns the response from the external system.
     **/
    public String getResponse(String url) {
        if (System.Test.isRunningTest()) {
          // Avoid callouts during tests. Return mock data instead.
          return mockResponse;
        } else {
          // Perform callouts for production (non-test) results.
          Http httpProtocol = new Http();
          HttpRequest request = new HttpRequest();
          request.setEndPoint(url);
          request.setMethod('GET');
          request.setHeader('Authorization', 'Bearer '+
              this.connectionInfo.oauthToken);
          HttpResponse response = httpProtocol.send(request);
          return response.getBody();
        }
    }
}

DriveDataSourceProvider Class

/**
 *   Extends the DataSource.Provider base class to create a 
 *   custom adapter for Salesforce Connect. The class informs 
 *   Salesforce of the functional and authentication 
 *   capabilities that are supported by or required to connect 
 *   to an external system.
 **/
global class DriveDataSourceProvider
    extends DataSource.Provider {
 
    /**
     *   Declares the types of authentication that can be used 
     *   to access the external system.
     **/
    override global List<DataSource.AuthenticationCapability>
        getAuthenticationCapabilities() {
        List<DataSource.AuthenticationCapability> capabilities =
            new List<DataSource.AuthenticationCapability>();
        capabilities.add(
            DataSource.AuthenticationCapability.OAUTH);
        capabilities.add(
            DataSource.AuthenticationCapability.ANONYMOUS);
        return capabilities;
    }
 
    /**
     *   Declares the functional capabilities that the 
     *   external system supports.
     **/
    override global List<DataSource.Capability>
        getCapabilities() {
        List<DataSource.Capability> capabilities =
            new List<DataSource.Capability>();
        capabilities.add(DataSource.Capability.ROW_QUERY);
        capabilities.add(DataSource.Capability.SEARCH);
        return capabilities;
    }

    /**
     *   Declares the associated DataSource.Connection class.
     **/
    override global DataSource.Connection getConnection(
        DataSource.ConnectionParams connectionParams) {
        return new DriveDataSourceConnection(connectionParams);
    }
}

适用于 Salesforce Connect 的 Google 图书™自定义适配器

此示例说明如何解决 外部系统的 API:在本例中为 Google 图书 API 系列。为了与 Google 图书™服务集成,我们按如下方式设置 Salesforce Connect。

  • Google Books API 最多允许返回 40 个结果,因此我们开发了 用于处理超过 40 行的结果集的自定义适配器。
  • Google Books API 只能按搜索相关性和发布日期进行排序,因此我们 开发我们的自定义适配器以禁用列排序。
  • 为了支持 OAuth,我们在 Salesforce 中设置了身份验证设置,以便 请求的访问令牌权限范围包括 。https://www.googleapis.com/auth/books
  • 为了允许 Apex 标注,我们在 Salesforce 中定义了以下远程站点:
    • https://www.googleapis.com
    • https://books.google.com

BooksDataSourceConnection 类

/**
 *   Extends the DataSource.Connection class to enable
 *   Salesforce to sync the external system metadata
 *   schema and to handle queries and searches of the external
 *   data.
 **/
global class BooksDataSourceConnection extends
    DataSource.Connection {
 
    private DataSource.ConnectionParams connectionInfo;

    // Constructor for BooksDataSourceConnection.
    global BooksDataSourceConnection(DataSource.ConnectionParams
                                    connectionInfo) {
        this.connectionInfo = connectionInfo;
    }

    /**
     *   Called when an external object needs to get a list of 
     *   schema from the external data source, for example when 
     *   the administrator clicks “Validate and Sync” in the 
     *   user interface for the external data source.   
     **/
    override global List<DataSource.Table> sync() {
        List<DataSource.Table> tables =
            new List<DataSource.Table>();
        List<DataSource.Column> columns;
        columns = new List<DataSource.Column>();
        columns.add(getColumn('title'));
        columns.add(getColumn('description'));
        columns.add(getColumn('publishedDate'));
        columns.add(getColumn('publisher'));
        columns.add(DataSource.Column.url('DisplayUrl'));
        columns.add(DataSource.Column.text('ExternalId', 255));
        tables.add(DataSource.Table.get('googleBooks', 'title',
                                        columns));
        return tables;
    }

    /**
     *   Google Books API v1 doesn't support sorting,
     *   so we create a column with sortable = false.
     **/
    private DataSource.Column getColumn(String columnName) {
        DataSource.Column column = DataSource.Column.text(columnName,
                                                        255);
        column.sortable = false;
        return column;
    }

    /**
     *   Called to query and get results from the external
     *   system for SOQL queries, list views, and detail pages
     *   for an external object that's associated with the
     *   external data source.
     *
     *   The QueryContext argument represents the query to run
     *   against a table in the external system.
     *
     *   Returns a list of rows as the query results.
     **/
    override global DataSource.TableResult query(
                    DataSource.QueryContext contexts) {
        DataSource.Filter filter = contexts.tableSelection.filter;
        String url;
        if (contexts.tableSelection.columnsSelected.size() == 1 &&
        contexts.tableSelection.columnsSelected.get(0).aggregation ==
            DataSource.QueryAggregation.COUNT) {
            return getCount(contexts);
        }

        if (filter != null) {
            String thisColumnName = filter.columnName;
            if (thisColumnName != null &&
                thisColumnName.equals('ExternalId')) {
                url = 'https://www.googleapis.com/books/v1/' +
                    'volumes?q=' + filter.columnValue +
                    '&maxResults=1&id=' + filter.columnValue;
                return DataSource.TableResult.get(true, null,
                            contexts.tableSelection.tableSelected,
                            getData(url));
            }
            else {
                url = 'https://www.googleapis.com/books/' +
                    'v1/volumes?q=' + filter.columnValue +
                    '&id=' + filter.columnValue +
                    '&maxResults=40' + '&startIndex=';
            }
        } else {
            url = 'https://www.googleapis.com/books/v1/' +
                'volumes?q=america&' + '&maxResults=40' +
                '&startIndex=';
        }
        /**
         *   Google Books API v1 supports maxResults of 40
         *   so we handle pagination explicitly in the else statement
         *   when we handle more than 40 records per query.
         **/
        if (contexts.maxResults < 40) {
            return DataSource.TableResult.get(true, null,
                    contexts.tableSelection.tableSelected,
                    getData(url + contexts.offset));
        }
        else {
            return fetchData(contexts, url);
        }
     }

    /**
     *   Helper method to fetch results when maxResults is 
     *   greater than 40 (the max value for maxResults supported 
     *   by Google Books API v1).
     **/
    private DataSource.TableResult fetchData(
        DataSource.QueryContext contexts, String url) {
        Integer fetchSlot = (contexts.maxResults / 40) + 1;
        List<Map<String, Object>> data =
            new List<Map<String, Object>>();
        Integer startIndex = contexts.offset;
        for(Integer count = 0; count < fetchSlot; count++) {
            data.addAll(getData(url + startIndex));
            if(count == 0)
                contexts.offset = 41;
            else
                contexts.offset += 40;
        }
 
        return DataSource.TableResult.get(true, null,
                        contexts.tableSelection.tableSelected, data);
    }

    /**
     *   Helper method to execute count() query.
     **/
    private DataSource.TableResult getCount(
        DataSource.QueryContext contexts) {
        String url = 'https://www.googleapis.com/books/v1/' +
                    'volumes?q=america&projection=full';
        List<Map<String,Object>> response =
            DataSource.QueryUtils.filter(contexts, getData(url));
        List<Map<String, Object>> countResponse =
            new List<Map<String, Object>>();
        Map<String, Object> countRow =
            new Map<String, Object>();
        countRow.put(
            contexts.tableSelection.columnsSelected.get(0).columnName,
            response.size());
        countResponse.add(countRow);
        return DataSource.TableResult.get(contexts, countResponse);
    }

    /**
     *   Called to do a full text search and get results from
     *   the external system for SOSL queries and Salesforce
     *   global searches.
     *
     *   The SearchContext argument represents the query to run
     *   against a table in the external system.
     *
     *   Returns results for each table that the SearchContext
     *   requested to be searched.
     **/
    override global List<DataSource.TableResult> search(
        DataSource.SearchContext contexts) {
        List<DataSource.TableResult> results =
            new List<DataSource.TableResult>();

        for (Integer i =0; i< contexts.tableSelections.size();i++) {
            String entity = contexts.tableSelections[i].tableSelected;
            String url = 'https://www.googleapis.com/books/v1' +
                        '/volumes?q=' + contexts.searchPhrase;
            results.add(DataSource.TableResult.get(true, null,
                                                entity,
                                                getData(url)));
        }

        return results;
    }

    /**
     *   Helper method to parse the data.
     *   Returns a list of rows from the external system.
     **/
    public List<Map<String, Object>> getData(String url) {
        HttpResponse response = getResponse(url);
        String body = response.getBody();

        List<Map<String, Object>> rows =
            new List<Map<String, Object>>();

        Map<String, Object> responseBodyMap =
            (Map<String, Object>)JSON.deserializeUntyped(body);

    /**
     *   Checks errors.
     **/        
        Map<String, Object> error =
            (Map<String, Object>)responseBodyMap.get('error');
        if (error!=null) {
            List<Object> errorsList =
                (List<Object>)error.get('errors');
            Map<String, Object> errors =
                (Map<String, Object>)errorsList[0];
            String messages = (String)errors.get('message');
            throw new DataSource.OAuthTokenExpiredException(messages);
        }

        List<Object> sItems = (List<Object>)responseBodyMap.get('items');
        if (sItems != null) {
            for (Integer i=0; i< sItems.size(); i++) {
                Map<String, Object> item =
                    (Map<String, Object>)sItems[i];
                rows.add(createRow(item));
            }
        } else {
            rows.add(createRow(responseBodyMap));
        }
 
        return rows;
    }

    /**
     *   Helper method to populate a row based on source data.
     *
     *   The item argument maps to the data that
     *   represents a row.
     *
     *   Returns an updated map with the External ID and
     *   Display URL values.
     **/
    public Map<String, Object> createRow(
        Map<String, Object> item) {
        Map<String, Object> row = new Map<String, Object>();
        for ( String key : item.keySet() ){
            if (key == 'id') {
                row.put('ExternalId', item.get(key));
            } else if (key == 'volumeInfo') {
                Map<String, Object> volumeInfoMap =
                    (Map<String, Object>)item.get(key);
                row.put('title', volumeInfoMap.get('title'));
                row.put('description',
                        volumeInfoMap.get('description'));
                row.put('DisplayUrl',
                        volumeInfoMap.get('infoLink'));
                row.put('publishedDate',
                        volumeInfoMap.get('publishedDate'));
                row.put('publisher',
                        volumeInfoMap.get('publisher'));
            }
        }
        return row;
    }

    /**
     *   Helper method to make the HTTP GET call.
     *   The url argument is the URL of the external system.
     *   Returns the response from the external system.
     **/
    public HttpResponse getResponse(String url) {
        Http httpProtocol = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndPoint(url);
        request.setMethod('GET');
        request.setHeader('Authorization', 'Bearer '+
                        this.connectionInfo.oauthToken);
        HttpResponse response = httpProtocol.send(request);
        return response;
    }
}

BooksDataSourceProvider Class

/**
 *   Extends the DataSource.Provider base class to create a
 *   custom adapter for Salesforce Connect. The class informs
 *   Salesforce of the functional and authentication
 *   capabilities that are supported by or required to connect
 *   to an external system.
 **/
global class BooksDataSourceProvider extends
    DataSource.Provider {
    /**
     *   Declares the types of authentication that can be used
     *   to access the external system.
     **/   
    override global List<DataSource.AuthenticationCapability>
        getAuthenticationCapabilities() {
        List<DataSource.AuthenticationCapability> capabilities =
            new List<DataSource.AuthenticationCapability>();
        capabilities.add(
            DataSource.AuthenticationCapability.OAUTH);
        capabilities.add(
            DataSource.AuthenticationCapability.ANONYMOUS);
        return capabilities;
    }

    /**
     *   Declares the functional capabilities that the
     *   external system supports.
     **/
    override global List<DataSource.Capability>
        getCapabilities() {
        List<DataSource.Capability> capabilities = new
            List<DataSource.Capability>();
        capabilities.add(DataSource.Capability.ROW_QUERY);
        capabilities.add(DataSource.Capability.SEARCH);
        return capabilities;
    }

    /**
     *   Declares the associated DataSource.Connection class.
     **/
    override global DataSource.Connection getConnection(
        DataSource.ConnectionParams connectionParams) {
        return new BooksDataSourceConnection(connectionParams);
    }
}

适用于 Salesforce Connect 的环回自定义适配器

此示例说明如何处理查询中的筛选。为简单起见,这里 示例将 Salesforce 组织作为外部系统连接到自身。

LoopbackDataSourceConnection 类

/**
 *   Extends the DataSource.Connection class to enable 
 *   Salesforce to sync the external system’s schema 
 *   and to handle queries and searches of the external data. 
 **/
global class LoopbackDataSourceConnection 
    extends DataSource.Connection {

    /**
     *   Constructors.
     **/
    global LoopbackDataSourceConnection(
        DataSource.ConnectionParams connectionParams) {
    }
    global LoopbackDataSourceConnection() {}
    
    /**
     *   Called when an external object needs to get a list of 
     *   schema from the external data source, for example when 
     *   the administrator clicks “Validate and Sync” in the 
     *   user interface for the external data source.   
     **/
    override global List<DataSource.Table> sync() {
        List<DataSource.Table> tables = 
            new List<DataSource.Table>();        
        List<DataSource.Column> columns;
        columns = new List<DataSource.Column>();
        columns.add(DataSource.Column.text('ExternalId', 255));
        columns.add(DataSource.Column.url('DisplayUrl'));
        columns.add(DataSource.Column.text('Name', 255));
        columns.add(
            DataSource.Column.number('NumberOfEmployees', 18, 0));
        tables.add(
            DataSource.Table.get('Looper', 'Name', columns));
        return tables;
    }
    
    /**
     *   Called to query and get results from the external 
     *   system for SOQL queries, list views, and detail pages 
     *   for an external object that’s associated with the 
     *   external data source.
     *   
     *   The QueryContext argument represents the query to run 
     *   against a table in the external system.
     *   
     *   Returns a list of rows as the query results.
     **/
    override global DataSource.TableResult 
        query(DataSource.QueryContext context) {
        if (context.tableSelection.columnsSelected.size() == 1 &&
            context.tableSelection.columnsSelected.get(0).aggregation ==
                DataSource.QueryAggregation.COUNT) {
            integer count = execCount(getCountQuery(context));
            List<Map<String, Object>> countResponse =
                new List<Map<String, Object>>();
            Map<String, Object> countRow =
                new Map<String, Object>();
            countRow.put(
                context.tableSelection.columnsSelected.get(0).columnName,
                count);
            countResponse.add(countRow);
            return DataSource.TableResult.get(context,countResponse);
        } else {
            List<Map<String,Object>> rows = execQuery(
                getSoqlQuery(context));
            return DataSource.TableResult.get(context,rows);
        }
    }
    
    /**
     *   Called to do a full text search and get results from
     *   the external system for SOSL queries and Salesforce
     *   global searches.
     *   
     *   The SearchContext argument represents the query to run 
     *   against a table in the external system.
     *   
     *   Returns results for each table that the SearchContext 
     *   requested to be searched.
     **/
    override global List<DataSource.TableResult> 
        search(DataSource.SearchContext context) {        
        return DataSource.SearchUtils.searchByName(context, this);
    }

    /**
     *   Helper method to execute the SOQL query and 
     *   return the results.
     **/
    private List<Map<String,Object>> 
        execQuery(String soqlQuery) {
        List<Account> objs = Database.query(soqlQuery);
        List<Map<String,Object>> rows = 
            new List<Map<String,Object>>();
        for (Account obj : objs) {
            Map<String,Object> row = new Map<String,Object>();
            row.put('Name', obj.Name);
            row.put('NumberOfEmployees', obj.NumberOfEmployees);
            row.put('ExternalId', obj.Id);
            row.put('DisplayUrl', 
                URL.getOrgDomainUrl().toExternalForm() + 
                    obj.Id);
            rows.add(row);
        }
        return rows;
    }

    /**
     *   Helper method to get aggregate count.
     **/
    private integer execCount(String soqlQuery) {
        integer count = Database.countQuery(soqlQuery);
        return count;
    }

    /**
     *   Helper method to create default aggregate query.
     **/
    private String getCountQuery(DataSource.QueryContext context) {
        String baseQuery = 'SELECT COUNT() FROM Account';
        String filter = getSoqlFilter('', 
            context.tableSelection.filter);
        if (filter.length() > 0)
            return baseQuery + ' WHERE ' + filter;
        return baseQuery;
    }

    /**
     *   Helper method to create default query.
     **/
    private String getSoqlQuery(DataSource.QueryContext context) {
        String baseQuery = 
            'SELECT Id,Name,NumberOfEmployees FROM Account';
        String filter = getSoqlFilter('', 
            context.tableSelection.filter);
        if (filter.length() > 0)
            return baseQuery + ' WHERE ' + filter;
        return baseQuery;
    }

    /**
     *   Helper method to handle query filter.
     **/
    private String getSoqlFilter(String query, 
        DataSource.Filter filter) {
        if (filter == null) {
            return query;
        }
        String append;
        DataSource.FilterType type = filter.type;
        List<Map<String,Object>> retainedRows = 
            new List<Map<String,Object>>();
        if (type == DataSource.FilterType.NOT_) {
            DataSource.Filter subfilter = filter.subfilters.get(0);
            append = getSoqlFilter('NOT', subfilter);
        } else if (type == DataSource.FilterType.AND_) {
            append =  
                getSoqlFilterCompound('AND', filter.subfilters);
        } else if (type == DataSource.FilterType.OR_) {
            append = 
                getSoqlFilterCompound('OR', filter.subfilters);
        } else {
            append = getSoqlFilterExpression(filter);
        }
        return query + ' ' + append;
    }
    
    /**
     *   Helper method to handle query subfilters.
     **/
    private String getSoqlFilterCompound(String operator, 
        List<DataSource.Filter> subfilters) {
        String expression = ' (';
        boolean first = true;
        for (DataSource.Filter subfilter : subfilters) {
            if (first)
                first = false;
            else
                expression += ' ' + operator + ' ';
            expression += getSoqlFilter('', subfilter);
        }
        expression += ') ';
        return expression;
    }
    
    /**
     *   Helper method to handle query filter expressions.
     **/
    private String getSoqlFilterExpression(
        DataSource.Filter filter) {
        String columnName = filter.columnName;
        String operator;
        Object expectedValue = filter.columnValue;
        if (filter.type == DataSource.FilterType.EQUALS) {
            operator = '=';
        } else if (filter.type == 
            DataSource.FilterType.NOT_EQUALS) {
            operator = '<>';
        } else if (filter.type == 
            DataSource.FilterType.LESS_THAN) {
            operator = '<';
        } else if (filter.type == 
            DataSource.FilterType.GREATER_THAN) {
            operator = '>';
        } else if (filter.type == 
            DataSource.FilterType.LESS_THAN_OR_EQUAL_TO) {
            operator = '<=';
        } else if (filter.type == 
            DataSource.FilterType.GREATER_THAN_OR_EQUAL_TO) {
            operator = '>=';
        } else if (filter.type == 
            DataSource.FilterType.STARTS_WITH) {
            return mapColumnName(columnName) + 
            ' LIKE \'' + String.valueOf(expectedValue) + '%\'';
        } else if (filter.type == 
            DataSource.FilterType.ENDS_WITH) {
            return mapColumnName(columnName) + 
            ' LIKE \'%' + String.valueOf(expectedValue) + '\'';
        } else if (filter.type == 
            DataSource.FilterType.LIKE_) {
            return mapColumnName(columnName) + 
            ' LIKE \'' + String.valueOf(expectedValue) + '\'';
        } else {
            throwException(
            'Implementing other filter types is left as an exercise for the reader: ' 
            + filter.type);
        }
        return mapColumnName(columnName) + 
            ' ' + operator + ' ' + wrapValue(expectedValue);
    }
    
    /**
     *   Helper method to map column names.
     **/
    private String mapColumnName(String apexName) {
        if (apexName.equalsIgnoreCase('ExternalId'))
            return 'Id';
        if (apexName.equalsIgnoreCase('DisplayUrl'))
            return 'Id';
        return apexName;
    }

    /**
    *   Helper method to wrap expression Strings with quotes.
    **/
    private String wrapValue(Object foundValue) {
        if (foundValue instanceof String)
            return '\'' + String.valueOf(foundValue) + '\'';
        return String.valueOf(foundValue);
    }
}

LoopbackDataSourceProvider Class

/**
 *   Extends the DataSource.Provider base class to create a 
 *   custom adapter for Salesforce Connect. The class informs 
 *   Salesforce of the functional and authentication 
 *   capabilities that are supported by or required to connect 
 *   to an external system.
 **/
global class LoopbackDataSourceProvider 
    extends DataSource.Provider {
    
    /**
     *   Declares the types of authentication that can be used 
     *   to access the external system.
     **/
    override global List<DataSource.AuthenticationCapability> 
        getAuthenticationCapabilities() {
        List<DataSource.AuthenticationCapability> capabilities = 
            new List<DataSource.AuthenticationCapability>();
        capabilities.add(
            DataSource.AuthenticationCapability.ANONYMOUS);
        capabilities.add(
            DataSource.AuthenticationCapability.BASIC);
        return capabilities;
    }

    /**
     *   Declares the functional capabilities that the 
     *   external system supports.
     **/
    override global List<DataSource.Capability> 
        getCapabilities() {
        List<DataSource.Capability> capabilities = 
            new List<DataSource.Capability>();
        capabilities.add(DataSource.Capability.ROW_QUERY);
        capabilities.add(DataSource.Capability.SEARCH);
        return capabilities;
    }

    /**
     *   Declares the associated DataSource.Connection class.
     **/
    override global DataSource.Connection 
        getConnection(DataSource.ConnectionParams connectionParams) {
        return new LoopbackDataSourceConnection();
    }
}

适用于 Salesforce Connect 的 GitHub 自定义适配器

此示例演示如何支持间接查找关系。间接 查找关系将子外部对象链接到父标准对象或自定义对象 对象。

若要使此示例正常工作,请在“联系人”标准对象上创建一个自定义字段。命名自定义项 字段,使其成为长度为 39 的文本字段,并且 选择 和 属性。另外,将 https://api.github.com 添加到 您的远程站点设置。github_usernameExternal IDUnique

GitHubDataSourceConnection 类

/**
 *   Defines the connection to GitHub REST API v3 to support
 *   querying of GitHub profiles.
 *   Extends the DataSource.Connection class to enable
 *   Salesforce to sync the external system’s schema
 *   and to handle queries and searches of the external data.
 **/
global class GitHubDataSourceConnection extends
        DataSource.Connection {
    private DataSource.ConnectionParams connectionInfo;

    /**
     *   Constructor for GitHubDataSourceConnection
     **/
    global GitHubDataSourceConnection(
            DataSource.ConnectionParams connectionInfo) {
        this.connectionInfo = connectionInfo;
    }

    /**
     *   Called to query and get results from the external 
     *   system for SOQL queries, list views, and detail pages 
     *   for an external object that’s associated with the 
     *   external data source.
     *   
     *   The queryContext argument represents the query to run 
     *   against a table in the external system.
     *   
     *   Returns a list of rows as the query results.
     **/
    override global DataSource.TableResult query(
            DataSource.QueryContext context) {
        DataSource.Filter filter = context.tableSelection.filter;
        String url;
        if (filter != null) {
            String thisColumnName = filter.columnName;
            if (thisColumnName != null &&
               (thisColumnName.equals('ExternalId') ||
                thisColumnName.equals('login')))
                url = 'https://api.github.com/users/'
                        + filter.columnValue;
            else
                    url = 'https://api.github.com/users';
        } else {
            url = 'https://api.github.com/users';
        }

        /**
         * Filters, sorts, and applies limit and offset clauses.
         **/
        List<Map<String, Object>> rows =
                DataSource.QueryUtils.process(context, getData(url));
        return DataSource.TableResult.get(true, null,
                context.tableSelection.tableSelected, rows);
    }

    /**
     *   Defines the schema for the external system. 
     *   Called when the administrator clicks “Validate and Sync”
     *   in the user interface for the external data source.
     **/
    override global List<DataSource.Table> sync() {
        List<DataSource.Table> tables =
                new List<DataSource.Table>();
        List<DataSource.Column> columns;
        columns = new List<DataSource.Column>();

        // Defines the indirect lookup field. (For this to work,
        // make sure your Contact standard object has a
        // custom unique, external ID field called github_username.)
        columns.add(DataSource.Column.indirectLookup(
                'login', 'Contact', 'github_username__c'));

        columns.add(DataSource.Column.text('id', 255));
        columns.add(DataSource.Column.text('name',255));
        columns.add(DataSource.Column.text('company',255));
        columns.add(DataSource.Column.text('bio',255));
        columns.add(DataSource.Column.text('followers',255));
        columns.add(DataSource.Column.text('following',255));
        columns.add(DataSource.Column.url('html_url'));
        columns.add(DataSource.Column.url('DisplayUrl'));
        columns.add(DataSource.Column.text('ExternalId',255));
        tables.add(DataSource.Table.get('githubProfile','login',
                columns));
        return tables;
    }

    /**
     *   Called to do a full text search and get results from
     *   the external system for SOSL queries and Salesforce
     *   global searches.
     *
     *   The SearchContext argument represents the query to run
     *   against a table in the external system.
     *
     *   Returns results for each table that the SearchContext
     *   requested to be searched.
     **/
    override global List<DataSource.TableResult> search(
            DataSource.SearchContext context) {
        List<DataSource.TableResult> results =
                new List<DataSource.TableResult>();

        for (Integer i =0;i< context.tableSelections.size();i++) {
            String entity = context.tableSelections[i].tableSelected;

            // Search usernames
            String url = 'https://api.github.com/users/'
                            + context.searchPhrase;
            results.add(DataSource.TableResult.get(
                    true, null, entity, getData(url)));
        }

        return results;
    }

    /**
     *   Helper method to parse the data.
     *   The url argument is the URL of the external system.
     *   Returns a list of rows from the external system.
     **/
    public List<Map<String, Object>> getData(String url) {
        String response = getResponse(url);

        // Standardize response string
        if (!response.contains('"items":')) {
            if (response.substring(0,1).equals('{')) {
                response = '[' + response  + ']';
            }
            response = '{"items": ' + response + '}';
        }

        List<Map<String, Object>> rows =
                new List<Map<String, Object>>();

        Map<String, Object> responseBodyMap = (Map<String, Object>)
                JSON.deserializeUntyped(response);

        /**
         *   Checks errors.
         **/
        Map<String, Object> error =
                (Map<String, Object>)responseBodyMap.get('error');
        if (error!=null) {
            List<Object> errorsList =
                    (List<Object>)error.get('errors');
            Map<String, Object> errors =
                    (Map<String, Object>)errorsList[0];
            String errorMessage = (String)errors.get('message');
            throw new 
                    DataSource.OAuthTokenExpiredException(errorMessage);
        }

        List<Object> fileItems = 
            (List<Object>)responseBodyMap.get('items');
        if (fileItems != null) {
            for (Integer i=0; i < fileItems.size(); i++) {
                Map<String, Object> item =
                        (Map<String, Object>)fileItems[i];
                rows.add(createRow(item));
            }
        } else {
            rows.add(createRow(responseBodyMap));
        }

        return rows;
    }

    /**
     *   Helper method to populate the External ID and Display
     *   URL fields on external object records based on the 'id'
     *   value that’s sent by the external system.
     *
     *   The Map<String, Object> item parameter maps to the data
     *   that represents a row.
     *
     *   Returns an updated map with the External ID and
     *   Display URL values.
     **/
    public Map<String, Object> createRow(
            Map<String, Object> item){
        Map<String, Object> row = new Map<String, Object>();
        for ( String key : item.keySet() ) {
            if (key == 'login') {
                row.put('ExternalId', item.get(key));
            } else if (key=='html_url') {
                row.put('DisplayUrl', item.get(key));
            }

            row.put(key, item.get(key));
        }
        return row;
    }

    /**
     *   Helper method to make the HTTP GET call.
     *   The url argument is the URL of the external system.
     *   Returns the response from the external system.
     **/
    public String getResponse(String url) {
        // Perform callouts for production (non-test) results.
        Http httpProtocol = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndPoint(url);
        request.setMethod('GET');
        HttpResponse response = httpProtocol.send(request);
        return response.getBody();
    }
}

GitHubDataSourceProvider 类

/**
 *   Extends the DataSource.Provider base class to create a
 *   custom adapter for Salesforce Connect. The class informs
 *   Salesforce of the functional and authentication
 *   capabilities that are supported by or required to connect
 *   to an external system.
 **/
global class GitHubDataSourceProvider
        extends DataSource.Provider {

    /**
     *   For simplicity, this example declares that the external 
     *   system doesn’t require authentication by returning
     *   AuthenticationCapability.ANONYMOUS as the sole entry 
     *   in the list of authentication capabilities.
     **/
    override global List<DataSource.AuthenticationCapability>
    getAuthenticationCapabilities() {
        List<DataSource.AuthenticationCapability> capabilities =
                new List<DataSource.AuthenticationCapability>();
        capabilities.add(
                DataSource.AuthenticationCapability.ANONYMOUS);
        return capabilities;
    }

    /**
     *   Declares the functional capabilities that the
     *   external system supports, in this case
     *   only SOQL queries.
     **/
    override global List<DataSource.Capability>
    getCapabilities() {
        List<DataSource.Capability> capabilities =
                new List<DataSource.Capability>();
        capabilities.add(DataSource.Capability.ROW_QUERY);
        return capabilities;
    }

    /**
     *   Declares the associated DataSource.Connection class.
     **/
    override global DataSource.Connection getConnection(
            DataSource.ConnectionParams connectionParams) {
        return new GitHubDataSourceConnection(connectionParams);
    }
}

适用于 Salesforce Connect 的 Stack Overflow 自定义适配器

此示例说明如何支持外部查找关系和多个 表。外部查找关系链接子标准对象、自定义对象或外部对象 添加到父外部对象。每个表都可以成为 Salesforce 中的外部对象 组织。

若要使此示例正常工作,请在“联系人”标准对象上创建一个自定义字段。将 自定义字段“github_username”,然后选择 和 属性。External IDUnique

StackOverflowDataSourceConnection 类

/**
 *   Defines the connection to Stack Exchange API v2.2 to support
 *   querying of Stack Overflow users (stackoverflowUser)
 *   and posts (stackoverflowPost).
 *   Extends the DataSource.Connection class to enable
 *   Salesforce to sync the external system’s schema
 *   and to handle queries of the external data.
 **/
global class StackOverflowDataSourceConnection extends
        DataSource.Connection {
    private DataSource.ConnectionParams connectionInfo;

    /**
     *   Constructor for StackOverflowDataSourceConnection
     **/
    global StackOverflowDataSourceConnection(
            DataSource.ConnectionParams connectionInfo) {
        this.connectionInfo = connectionInfo;
    }

    /**
     *   Defines the schema for the external system. 
     *   Called when the administrator clicks “Validate and Sync”
     *   in the user interface for the external data source.
     **/
    override global List<DataSource.Table> sync() {
        List<DataSource.Table> tables =
                new List<DataSource.Table>();

        // Defines columns for the table of Stack OverFlow posts
        List<DataSource.Column> postColumns =
          new List<DataSource.Column>();

        // Defines the external lookup field.
        postColumns.add(DataSource.Column.externalLookup(
          'owner_id', 'stackoverflowUser__x'));
        postColumns.add(DataSource.Column.text('title', 255));
        postColumns.add(DataSource.Column.text('view_count', 255));
        postColumns.add(DataSource.Column.text('question_id',255));
        postColumns.add(DataSource.Column.text('creation_date',255));
        postColumns.add(DataSource.Column.text('score',255));
        postColumns.add(DataSource.Column.url('link'));
        postColumns.add(DataSource.Column.url('DisplayUrl'));
        postColumns.add(DataSource.Column.text('ExternalId',255));

        tables.add(DataSource.Table.get('stackoverflowPost','title',
          postColumns));

        // Defines columns for the table of Stack OverFlow users
        List<DataSource.Column> userColumns =
          new List<DataSource.Column>();
        userColumns.add(DataSource.Column.text('user_id', 255));
        userColumns.add(DataSource.Column.text('display_name', 255));
        userColumns.add(DataSource.Column.text('location',255));
        userColumns.add(DataSource.Column.text('creation_date',255));
        userColumns.add(DataSource.Column.url('website_url',255));
        userColumns.add(DataSource.Column.text('reputation',255));
        userColumns.add(DataSource.Column.url('link'));
        userColumns.add(DataSource.Column.url('DisplayUrl'));
        userColumns.add(DataSource.Column.text('ExternalId',255));

        tables.add(DataSource.Table.get('stackoverflowUser',
                'Display_name', userColumns));

        return tables;
    }

    /**
     *   Called to query and get results from the external
     *   system for SOQL queries, list views, and detail pages
     *   for an external object that’s associated with the
     *   external data source.
     *
     *   The QueryContext argument represents the query to run
     *   against a table in the external system.
     *
     *   Returns a list of rows as the query results.
     **/
    override global DataSource.TableResult query(
            DataSource.QueryContext context) {
        DataSource.Filter filter = context.tableSelection.filter;
        String url;

        // Sets the URL to query Stack Overflow posts
        if (context.tableSelection.tableSelected
.equals('stackoverflowPost')) {
            if (filter != null) {
                String thisColumnName = filter.columnName;
                if (thisColumnName != null &&
                        thisColumnName.equals('ExternalId'))
                    url = 'https://api.stackexchange.com/2.2/'
                            + 'questions/' + filter.columnValue
                            + '?order=desc&sort=activity'
                            + '&site=stackoverflow';
                else
                        url = 'https://api.stackexchange.com/2.2/'
                                + 'questions'
                                + '?order=desc&sort=activity'
                                + '&site=stackoverflow';
            } else {
                url = 'https://api.stackexchange.com/2.2/'
                        + 'questions'
                        + '?order=desc&sort=activity'
                        + '&site=stackoverflow';
            }
        // Sets the URL to query Stack Overflow users
        } else if (context.tableSelection.tableSelected
.equals('stackoverflowUser')) {
            if (filter != null) {
                String thisColumnName = filter.columnName;
                if (thisColumnName != null &&
                        thisColumnName.equals('ExternalId'))
                    url = 'https://api.stackexchange.com/2.2/'
                            + 'users/' + filter.columnValue
                            + '?order=desc&sort=reputation'
                            + '&site=stackoverflow';
                else
                    url = 'https://api.stackexchange.com/2.2/'
                            + 'users' + 
'?order=desc&sort=reputation&site=stackoverflow';
            } else {
                url = 'https://api.stackexchange.com/2.2/'
                        + 'users' + '?order=desc&sort=reputation'
                        + '&site=stackoverflow';
            }
        }

        /**
         * Filters, sorts, and applies limit and offset clauses.
         **/
        List<Map<String, Object>> rows =
                DataSource.QueryUtils.process(context, getData(url));
        return DataSource.TableResult.get(true, null,
                context.tableSelection.tableSelected, rows);
    }

    /**
     *   Helper method to parse the data.
     *   The url argument is the URL of the external system.
     *   Returns a list of rows from the external system.
     **/
    public List<Map<String, Object>> getData(String url) {
        String response = getResponse(url);

        List<Map<String, Object>> rows =
                new List<Map<String, Object>>();

        Map<String, Object> responseBodyMap = (Map<String, Object>)
                JSON.deserializeUntyped(response);

        /**
         *   Checks errors.
         **/
        Map<String, Object> error =
                (Map<String, Object>)responseBodyMap.get('error');
        if (error!=null) {
            List<Object> errorsList =
                    (List<Object>)error.get('errors');
            Map<String, Object> errors =
                    (Map<String, Object>)errorsList[0];
            String errorMessage = (String)errors.get('message');
            throw new 
                    DataSource.OAuthTokenExpiredException(errorMessage);
        }

        List<Object> fileItems=
            (List<Object>)responseBodyMap.get('items');
        if (fileItems != null) {
            for (Integer i=0; i < fileItems.size(); i++) {
                Map<String, Object> item =
                        (Map<String, Object>)fileItems[i];
                rows.add(createRow(item));
            }
        } else {
            rows.add(createRow(responseBodyMap));
        }

        return rows;
    }

    /**
     *   Helper method to populate the External ID and Display
     *   URL fields on external object records based on the 'id'
     *   value that’s sent by the external system.
     *
     *   The Map<String, Object> item parameter maps to the data
     *   that represents a row.
     *
     *   Returns an updated map with the External ID and
     *   Display URL values.
     **/
    public Map<String, Object> createRow(
            Map<String, Object> item) {
        Map<String, Object> row = new Map<String, Object>();
        for ( String key : item.keySet() ) {
            if (key.equals('question_id') || key.equals('user_id')) {
                row.put('ExternalId', item.get(key));
            } else if (key.equals('link')) {
                row.put('DisplayUrl', item.get(key));
            } else if (key.equals('owner')) {
                Map<String, Object> ownerMap =
                (Map<String, Object>)item.get(key);
                row.put('owner_id', ownerMap.get('user_id'));
            }

            row.put(key, item.get(key));
        }
        return row;
    }

    /**
     *   Helper method to make the HTTP GET call.
     *   The url argument is the URL of the external system.
     *   Returns the response from the external system.
     **/
    public String getResponse(String url) {
        // Perform callouts for production (non-test) results.
        Http httpProtocol = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndPoint(url);
        request.setMethod('GET');
        HttpResponse response = httpProtocol.send(request);
        return response.getBody();
    }
}

StackOverflowPostDataSourceProvider Class

/**
 *   Extends the DataSource.Provider base class to create a
 *   custom adapter for Salesforce Connect. The class informs
 *   Salesforce of the functional and authentication
 *   capabilities that are supported by or required to connect
 *   to an external system.
 **/
global class StackOverflowPostDataSourceProvider
        extends DataSource.Provider {

    /**
     *   For simplicity, this example declares that the external 
     *   system doesn’t require authentication by returning
     *   AuthenticationCapability.ANONYMOUS as the sole entry 
     *   in the list of authentication capabilities.
     **/
    override global List<DataSource.AuthenticationCapability>
    getAuthenticationCapabilities() {
        List<DataSource.AuthenticationCapability> capabilities =
                new List<DataSource.AuthenticationCapability>();
        capabilities.add(
                DataSource.AuthenticationCapability.ANONYMOUS);
        return capabilities;
    }

    /**
     *   Declares the functional capabilities that the
     *   external system supports, in this case
     *   only SOQL queries.
     **/
    override global List<DataSource.Capability>
    getCapabilities() {
        List<DataSource.Capability> capabilities =
                new List<DataSource.Capability>();
        capabilities.add(DataSource.Capability.ROW_QUERY);
        return capabilities;
    }

    /**
     *   Declares the associated DataSource.Connection class.
     **/
    override global DataSource.Connection getConnection(
            DataSource.ConnectionParams connectionParams) {
        return new 
            StackOverflowDataSourceConnection(connectionParams);
    }
}

Salesforce 知识

Salesforce Knowledge 是一个知识库,用户可以在其中轻松创建和管理 内容,称为文章,并快速查找和查看他们需要的文章。

使用 Apex 访问以下 Salesforce Knowledge 功能:

  • 知识管理
    除了 Salesforce 用户界面外,用户还可以使用 Apex 编写、发布、存档和管理文章。
  • 升级的搜索词 升级的搜索词
    对于推广 Salesforce Knowledge 文章非常有用,您知道该文章通常用于解决最终用户搜索包含某些关键字时的支持问题。除了 Salesforce 用户界面之外,用户还可以通过将关键字与 Apex 中的文章相关联(通过使用 SearchPromotionRule sObject)来在搜索结果中推广文章。
  • 建议 Salesforce 知识文章 为用户提供快捷方式,以便在用户执行搜索之前导航到相关文章
    。调用以返回标题与用户的搜索查询字符串匹配的 Salesforce Knowledge 文章列表。Search.suggest(searchText, objectType, options)

知识管理

用户可以使用 Apex 编写、发布、存档和管理文章,此外 Salesforce 用户界面。使用类中的方法管理文章及其翻译生命周期的以下部分:

KbManagement.PublishingService

  • 出版
  • 更新
  • 检索
  • 删除
  • 提交翻译
  • 将翻译设置为完成或未完成状态
  • 归档
  • 为文章草稿或翻译分配审校任务

注意

日期值基于 GMT。若要使用此类中的方法,必须启用 Salesforce 知识。请参阅 Salesforce 知识实施 有关设置 Salesforce Knowledge 的更多信息的指南。

推广的搜索词

推广的搜索词对于推广 您知道的 Salesforce 知识文章通常用于解决以下情况下的支持问题 最终用户的搜索包含某些关键字。 用户可以通过以下方式在搜索结果中推广文章 将关键字与 Apex 中的文章相关联(通过使用 SearchPromotionRule sObject) 添加到 Salesforce 用户界面。

文章必须处于已发布状态(PublishSatus 字段值为 Online),以便您管理其推广的字词。

此代码示例演示如何添加搜索 晋升规则。此示例执行查询以获取类型为 MyArticle__kav 的已发布文章。 接下来,该示例创建一个 SearchPromotionRule sObject 来升级包含 单词“Salesforce”,并将第一个返回的文章分配给它。最后,样品插入 这个新的 s对象。

// Identify the article to promote in search results
List<MyArticle__kav> articles = [SELECT Id FROM MyArticle__kav WHERE PublishStatus='Online' AND Language='en_US' AND Id='Article Id'];

// Define the promotion rule
SearchPromotionRule s = new SearchPromotionRule(
    Query='Salesforce',
    PromotedEntity=articles[0]);

// Save the new rule
insert s;

要在 SearchPromotionRule sObject,则必须启用 Salesforce Knowledge。

推荐 Salesforce 知识文章

为用户提供快捷方式,以便在用户执行 搜索。调用以返回标题与 用户的搜索查询字符串。

Search.suggest(searchText, objectType, options)

要返回建议,请启用 Salesforce Knowledge。请参阅 Salesforce 知识实施 有关设置 Salesforce Knowledge 的更多信息的指南。此 Visualforce 页面有一个用于搜索文章或帐户的输入字段。当用户 按“建议”按钮,显示建议的记录。如果超过五个 结果,则显示“更多结果”按钮。要显示更多结果,请单击该按钮。

<apex:page controller="SuggestionDemoController">
    <apex:form >
        <apex:pageBlock mode="edit" id="block">
            <h1>Article and Record Suggestions</h1>
            <apex:pageBlockSection >
                <apex:pageBlockSectionItem >
                    <apex:outputPanel >
                        <apex:panelGroup >
                            <apex:selectList value="{!objectType}" size="1">
                                <apex:selectOption itemLabel="Account" itemValue="Account" />
                                <apex:selectOption itemLabel="Article" itemValue="KnowledgeArticleVersion" />
                                <apex:actionSupport event="onchange" rerender="block"/>
                            </apex:selectList>
                        </apex:panelGroup>
                        <apex:panelGroup >
                            <apex:inputHidden id="nbResult" value="{!nbResult}" />
                            <apex:outputLabel for="searchText">Search Text</apex:outputLabel>
                            &nbsp;
                            <apex:inputText id="searchText" value="{!searchText}"/>
                            <apex:commandButton id="suggestButton" value="Suggest" action="{!doSuggest}" 
                                                rerender="block"/>
                            <apex:commandButton id="suggestMoreButton" value="More results..." action="{!doSuggestMore}" 
                                                rerender="block" style="{!IF(hasMoreResults, '', 'display: none;')}"/>
                        </apex:panelGroup>
                    </apex:outputPanel>
                </apex:pageBlockSectionItem>
            </apex:pageBlockSection>
            <apex:pageBlockSection title="Results" id="results" columns="1" rendered="{!results.size>0}">
                <apex:dataList value="{!results}" var="w" type="1">
                    Id: {!w.SObject['Id']}
                    <br />
                    <apex:panelGroup rendered="{!objectType=='KnowledgeArticleVersion'}">
                        Title: {!w.SObject['Title']}
                    </apex:panelGroup>
                    <apex:panelGroup rendered="{!objectType!='KnowledgeArticleVersion'}">
                        Name: {!w.SObject['Name']}
                    </apex:panelGroup>
                    <hr />
                </apex:dataList>
            </apex:pageBlockSection>
            <apex:pageBlockSection id="noresults" rendered="{!results.size==0}">
                No results
            </apex:pageBlockSection>
            <apex:pageBlockSection rendered="{!LEN(searchText)>0}">
                Search text: {!searchText}
            </apex:pageBlockSection>
        </apex:pageBlock>
    </apex:form>
</apex:page>

此代码是 页:

public class SuggestionDemoController {
    
    public String searchText;
    public String language = 'en_US';
    public String objectType = 'Account';
    public Integer nbResult = 5;
    public Transient Search.SuggestionResults suggestionResults;

    public String getSearchText() {
        return searchText;
    }

    public void setSearchText(String s) {
        searchText = s;
    }
    
    public Integer getNbResult() {
        return nbResult;
    }

    public void setNbResult(Integer n) {
        nbResult = n;
    }
    
    public String getLanguage() {
        return language;
    }
    
    public void setLanguage(String language) {
        this.language = language;
    }
            
    public String getObjectType() {
        return objectType;
    }
    
    public void setObjectType(String objectType) {
        this.objectType = objectType;
    }

    public List<Search.SuggestionResult> getResults() {
        if (suggestionResults == null) {
            return new List<Search.SuggestionResult>();
        }
        
        return suggestionResults.getSuggestionResults();
    }
    
    public Boolean getHasMoreResults() {
        if (suggestionResults == null) {
            return false;
        }
        return suggestionResults.hasMoreResults();
    }
    
    public PageReference doSuggest() {
        nbResult = 5;
        suggestAccounts();
        return null;
    }
    
    public PageReference doSuggestMore() {
        nbResult += 5;
        suggestAccounts();
        return null;
    }
    
    private void suggestAccounts() {
        Search.SuggestionOption options = new Search.SuggestionOption();
        Search.KnowledgeSuggestionFilter filters = new Search.KnowledgeSuggestionFilter();
        if (objectType=='KnowledgeArticleVersion') {
            filters.setLanguage(language);
            filters.setPublishStatus('Online');
        }
        options.setFilter(filters);
        options.setLimit(nbResult);
        suggestionResults = Search.suggest(searchText, objectType, options);
    }
}

Salesforce文件

使用 Apex 自定义 Salesforce Files 的行为。

  • 自定义文件下载 当用户尝试使用 Apex 回调下载文件时,
    您可以自定义文件的行为。ContentVersion 支持在下载操作后修改文件行为,例如防病毒扫描和信息权限管理 (IRM)。API 版本 39.0 及更高版本中提供了文件下载自定义。
  • 自定义文件下载示例
    您可以使用 Apex 在尝试下载时自定义文件的行为。这些示例假定只下载一个文件。API 版本 39.0 及更高版本中提供了文件下载自定义。

自定义文件下载

当用户尝试使用 Apex 回调。ContentVersion 支持修改后的文件行为,例如防病毒扫描和 信息权限管理 (IRM),在下载操作之后。文件下载定制是 在 API 版本 39.0 及更高版本中可用。

自定义代码在下载之前运行,并确定下载是否可以继续。

命名空间包含用于自定义的 Apex 对象 Salesforce 文件在下载之前的行为。 提供用于自定义文件下载的界面。 该类定义相关的值 是否允许下载,以及否则该怎么做。枚举是进行下载的上下文。SfcContentDownloadHandlerFactoryContentDownloadHandlerContentDownloadContext

您可以使用 Apex 从 Salesforce 中的“内容”选项卡自定义多文件下载 经典。Apex 函数参数 List<ID> 处理 ContentVersion ID 列表。

自定义也适用于内容包和内容交付。List<ID> 是 ContentPack 中的版本 ID。在多文件或 ContentPack 下载上进行设置会导致整个下载失败。你 可以通过 中的 URL 参数将问题文件的列表传递回错误页面。isDownloadAllowed = falseredirectUrl

  • 根据用户配置文件、正在使用的设备或文件类型阻止下载文件 和大小。
  • 应用 IRM 控件来跟踪信息,例如文件被 下载。
  • 在下载之前标记可疑文件,并重定向它们以进行防病毒扫描。

流程执行

当从 UI、Connect API 或检索 sObject 调用触发下载时,将查找 的实现。如果没有 找到实现,继续下载。否则,用户将被重定向到已 在属性中定义。如果找到多个实现,则对它们进行级联处理(按名称排序)和 考虑第一个不允许下载的。ContentVersion.VersionDataSfc.ContentDownloadHandlerFactoryContentDownloadHandler#redirectUrl

注意

如果 SOAP API 操作触发下载,它将通过 Apex 类来检查 是否允许下载。如果不允许下载,则无法处理重定向, 并返回包含错误消息的异常。

自定义文件下载示例

您可以使用 Apex 在尝试下载时自定义文件的行为。这些 示例假定只下载一个文件。文件下载定制是 在 API 版本 39.0 及更高版本中可用。

此示例演示了一个系统,该系统需要下载才能通过 IRM 进行 对某些用户进行控制。对于允许下载的修改所有数据 (MAD) 用户 文件,其用户 ID 为 :

005xx

// Allow customization of the content Download experience
public class ContentDownloadHandlerFactoryImpl implements Sfc.ContentDownloadHandlerFactory {

public Sfc.ContentDownloadHandler getContentDownloadHandler(List<ID> ids, Sfc.ContentDownloadContext context) {
    Sfc.ContentDownloadHandler contentDownloadHandler = new Sfc.ContentDownloadHandler();

    if(UserInfo.getUserId() == '005xx') {
        contentDownloadHandler.isDownloadAllowed = true;
        return contentDownloadHandler;
    }
    
    contentDownloadHandler.isDownloadAllowed = false;
    contentDownloadHandler.downloadErrorMessage = 'This file needs to be IRM controlled. You're not allowed to download it';
    contentDownloadHandler.redirectUrl ='/apex/IRMControl?Id='+ids.get(0);
    return contentDownloadHandler;
}
}

注意

自 请参阅 MAD 用户配置文件,您可以使用 代替 .UserInfo.getProfileId()UserInfo.getUserId()

在此示例中,是为 显示用于从 IRM 系统下载文件的链接。您需要一个控制器 调用 IRM 系统的此页面。在处理文件时,它会给出一个 端点,以便在文件受到控制时下载文件。IRM 系统使用 sObject API 来获取这个 .因此,IRM 系统需要 VersionID,并且必须使用 MAD 用户检索 VersionData。IRMControlVersionDataContentVersion

您的 IRM 系统处于 http://irmsystem,并且期望 VersionID 为 查询参数。IRM 系统返回包含下载终结点的 JSON 响应 在值中。downloadEndpoint

public class IRMController {
    
private String downloadEndpoint;
    
public IRMController() {
    downloadEndpoint = '';
}
    
public void applyIrmControl() {
    String versionId = ApexPages.currentPage().getParameters().get('id');
    Http h = new Http();

    //Instantiate a new HTTP request, specify the method (GET) as well as the endpoint
    HttpRequest req = new HttpRequest();
    req.setEndpoint('http://irmsystem?versionId=' + versionId);
    req.setMethod('GET');

    // Send the request, and retrieve a response
    HttpResponse r = h.send(req);
    JSONParser parser = JSON.createParser(r.getBody());
      while (parser.nextToken() != null) {
        if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&
            (parser.getText() == 'downloadEndpoint')) {
                parser.nextToken();
                downloadEndpoint = parser.getText();
                break;
        }
    }
}
    
public String getDownloadEndpoint() {
    return downloadEndpoint;
}
    
}

下面的示例创建一个实现接口的类,并返回 阻止将文件下载到移动设备的下载处理程序 装置。

ContentDownloadHandlerFactory

// Allow customization of the content Download experience
public class ContentDownloadHandlerFactoryImpl implements Sfc.ContentDownloadHandlerFactory {

public Sfc.ContentDownloadHandler getContentDownloadHandler(List<ID> ids, Sfc.ContentDownloadContext context) {
    Sfc.ContentDownloadHandler contentDownloadHandler = new Sfc.ContentDownloadHandler();
    
    if(context == Sfc.ContentDownloadContext.MOBILE) {
        contentDownloadHandler.isDownloadAllowed = false;
        contentDownloadHandler.downloadErrorMessage = 'Downloading a file from a mobile device isn't allowed.';
        return contentDownloadHandler;
    }
    contentDownloadHandler.isDownloadAllowed = true;
    return contentDownloadHandler;
}

您还可以阻止从移动设备下载文件,并要求 文件必须通过 IRM 控制。

// Allow customization of the content Download experience
public class ContentDownloadHandlerFactoryImpl implements Sfc.ContentDownloadHandlerFactory {

public Sfc.ContentDownloadHandler getContentDownloadHandler(List<ID> ids, Sfc.ContentDownloadContext context) {
    Sfc.ContentDownloadHandler contentDownloadHandler = new Sfc.ContentDownloadHandler();

    if(UserInfo.getUserId() == '005xx000001SvogAAC') {
        contentDownloadHandler.isDownloadAllowed = true;
        return contentDownloadHandler;
    }
    if(context == Sfc.ContentDownloadContext.MOBILE) {
        contentDownloadHandler.isDownloadAllowed = false;
        contentDownloadHandler.downloadErrorMessage = 'Downloading a file from a mobile device isn't allowed.';
        return contentDownloadHandler;
    }
    
    contentDownloadHandler.isDownloadAllowed = false;
    contentDownloadHandler.downloadErrorMessage = 'This file needs to be IRM controlled. You're not allowed to download it';
    contentDownloadHandler.redirectUrl ='/apex/IRMControl?Id='+id.get(0);
    return contentDownloadHandler;
}
}

平台缓存

Lightning Platform 平台缓存层提供更快的性能和更好的可靠性 缓存 Salesforce 会话和组织数据时。指定要缓存的内容以及不缓存多长时间 使用自定义对象和设置或重载 Visualforce 视图状态。平台缓存 通过分配缓存空间来提高性能,使某些应用程序或操作不会 从他人那里窃取容量。

因为 Apex 在多租户环境中运行,缓存数据在内部并存 缓存数据,缓存对核心 Salesforce 流程的干扰最小。

  • 平台缓存功能平台缓存 API 允许您存储和检索与 Salesforce 会话绑定或在整个组织中共享的数据。使用 Cache 命名空间中的 、 、 和 类放置、检索或删除缓存值。使用“设置”中的“平台缓存分区”工具创建或删除组织分区,并分配其缓存容量,以平衡跨应用的性能。SessionOrgSessionPartitionOrgPartition
  • 平台缓存注意事项 在使用平台缓存
    ,请查看这些注意事项。
  • 平台缓存限制 使用平台缓存时,这些限制
    适用。
  • 平台缓存分区
    使用平台缓存分区可以提高应用程序的性能。分区允许您以最适合您的应用程序的方式分配缓存空间。将数据缓存到指定的分区可确保它不会被其他应用程序或不太重要的数据覆盖。
  • 平台缓存内部
    平台缓存使用本地缓存和最少最近使用的 (LRU) 算法来提高性能。
  • 存储和检索会话平台缓存
    中的值 使用 和 类管理会话缓存中的值。若要管理任何分区中的值,请使用类中的方法。如果要管理一个分区中的缓存值,请改用这些方法。Cache.SessionCache.SessionPartitionCache.SessionCache.SessionPartition
  • 从组织平台缓存
    中存储和检索值 使用 和 类来管理组织缓存中的值。若要管理任何分区中的值,请使用类中的方法。如果要管理一个分区中的缓存值,请改用这些方法。Cache.OrgCache.OrgPartitionCache.OrgCache.OrgPartition
  • 将 Visualforce 全局变量用于平台缓存 您可以从具有全局变量的 Visualforce 页面访问存储在会话或组织缓存中的缓存
    值。
  • 使用 CacheBuilder 接口
    安全缓存值 平台缓存最佳实践是通过测试返回 null 的缓存请求来确保您的 Apex 代码处理缓存未命中。您可以自己编写此代码。或者,您可以使用该接口,该接口可以轻松地安全地将值存储和检索到会话或组织缓存中。Cache.CacheBuilder
  • 平台缓存最佳实践
    平台缓存可以大大提高应用程序的性能。但是,请务必遵循这些准则以获得最佳缓存性能。通常,缓存几个大项目比单独缓存多个小项目更有效。还要注意缓存限制,以防止意外的缓存逐出。

平台缓存功能

Platform Cache API 允许您存储和检索绑定的数据 到 Salesforce 会话或在整个组织中共享。放置、检索或删除缓存值 使用 Cache 命名空间中的 、 、 和 类。使用 安装程序中的平台缓存分区工具,用于创建或删除组织分区并分配其 缓存容量,用于平衡跨应用的性能。SessionOrgSessionPartitionOrgPartition有两种类型的缓存:

  • 会话缓存 – 存储单个用户会话的数据。例如,在应用中 在指定区域内查找客户,在用户运行时运行的计算 在地图上浏览不同的位置被重复使用。会话缓存与用户并存 会期。会话的最长寿命为 8 小时。会话缓存在其 达到指定的生存时间(值)或会话时 八小时后过期,以先到者为准。ttlsecs
  • 组织缓存 – 存储组织中任何用户重复使用的数据。例如,内容 基于用户配置文件动态显示菜单项的导航栏是 重用。与会话缓存不同,组织缓存可以跨会话、请求和 组织用户和配置文件。组织缓存在其指定的生存时间时过期 ( 值)。ttlsecs

此外,Salesforce 还提供 3 MB 的免费平台缓存容量,用于安全审查 通过称为“无提供程序容量”的容量类型托管包。您可以分配 会话缓存和组织缓存的容量来自提供程序免费容量。要缓存的最佳数据是:

  • 在整个会话中重复使用
  • 静态(不快速变化)
  • 否则检索成本高昂

对于会话缓存和组织缓存,您可以构造调用,以便缓存的数据位于一个命名空间中 不会被另一个中的类似数据覆盖。(可选)使用枚举指定 Apex 代码是否可以访问 在调用命名空间之外的命名空间中缓存数据。Cache.Visibility

每个缓存操作都取决于运行该操作的 Apex 事务。如果整个 事务失败,则该事务中的所有缓存操作都将回滚。

试用平台缓存

要在您自己的组织中使用平台缓存来测试性能改进,您可以请求 生产组织的试用缓存。 企业版、无限制版和性能版现已推出 使用一些缓存,但添加更多缓存通常可提供更高的性能。当您的试用期 请求获得批准时,您可以为分区分配容量,并尝试使用 缓存用于不同的场景。通过试用测试缓存,您可以做出明智的决定 决定是否购买缓存。

有关试用缓存的详细信息,请参阅“请求平台缓存试用” 在 Salesforce 帮助中。

您可以请求额外的缓存空间来提高应用程序的性能。为 有关请求其他缓存的详细信息,请参阅“请求其他平台缓存” 在 Salesforce 帮助中。

有关提供程序免费容量缓存的详细信息,请参阅“设置平台” Salesforce 帮助中的“使用提供程序可用容量缓存分区”。

注意

专业版不支持平台缓存。

平台缓存注意事项

使用平台缓存时,请查看这些注意事项。

  • 缓存不会持久化。无法保证不会丢失数据。
  • 当您修改组织中的 Apex 类时,部分或全部缓存将失效。
  • 缓存中的数据未加密。
  • 组织缓存支持跨多个同时 Apex 的并发读取和写入 交易。例如,事务使用 值 Fido。同时,另一个事务更新 与值 Felix 相同的键。两次写入都成功,但其中一个 任意选择两个值作为获胜者,然后交易读取该值 价值。但是,这种任意选择是按键而不是按事务进行的。例如 假设一个事务写入 PetType=“Cat” 和 PetName=“Felix”。然后,在同一时刻,另一笔交易 写入 PetType=“Dog” 和 PetName=“Fido”。在这个 情况下,PetType 获胜值可能来自第一笔交易, PetName 获胜值可能来自第二笔交易。 对这些键的后续调用将返回 PetType=“Cat” 和 PetName=“Fido”。get()
  • 可能会发生缓存未命中。我们建议在构造代码时考虑以下情况: 找不到以前缓存的项目。或者,使用 CacheBuilder 接口,该接口检查缓存 错过。
  • 所有平台缓存统计方法:、、、 从缓存服务器重新启动时开始的数据,不包括之前的数据 重新启动。getAvgGetSize()getAvgGetTime()getMaxGetSize()getMaxGetTime()getMissRate()
  • 分区必须遵守 Salesforce 中的限制。
  • 会话缓存最多可以存储 8 小时的值。组织缓存可以存储值 至 48 小时。
  • 对于使用 Salesforce Flow 的组织:
    • 当进程包含计划操作时,请确保 进程不会调用用于存储或检索会话缓存中值的 Apex 代码。 会话缓存限制适用于 Apex 操作和进程的更改 make 到导致 Apex 触发器触发的数据库。
    • 当流程包含 Pause 元素时,请确保流程中的后续元素 不要调用用于存储或检索会话缓存中的值的 Apex 代码。这 session-cache 限制适用于 Apex 操作和流所做的更改 添加到导致 Apex 触发器触发的数据库。

平台缓存限制

使用平台缓存时,这些限制适用。

平台缓存限制

特定于版本的限制

下表显示了可用于不同类型组织的平台缓存量。自 购买更多缓存,请联系您的 Salesforce 代表。

缓存大小
企业10兆字节
无限和性能30兆字节
所有其他0 兆字节

分区大小限制

限制价值
最小分区大小1 兆字节

会话缓存限制

限制价值
单个缓存项的最大大小(用于方法)put()100 知识库
分区的最大本地缓存大小, 每个请求1500 知识库
开发人员分配的最短生存时间300 秒(5 分钟)
开发人员分配的最长生存时间28,800 秒(8 小时)
最大会话缓存生存时间28,800 秒(8 小时)

组织缓存限制

限制价值
单个缓存项的最大大小(用于方法)put()100 KB
分区的最大本地缓存大小, 每个请求11,000 知识库
开发人员分配的最短生存时间300 秒(5 分钟)
开发人员分配的最长生存时间172,800 秒 (48 小时)
默认组织缓存生存时间86,400 秒(24 小时)

1本地缓存是应用程序服务器的内存中容器 客户端在请求期间与之交互。

平台缓存分区

使用平台缓存分区可提高应用程序的性能。 分区允许您以最适合您的应用程序的方式分配缓存空间。 将数据缓存到指定的分区可确保它不会被其他应用程序覆盖,或者 不太重要的数据。

要使用平台缓存,请首先使用平台缓存分区工具设置分区 设置。设置分区后,可以使用Platform Cache Apex API。

要访问“设置”中的“分区”工具,请输入“快速查找”框,然后选择Platform Cache使用分区工具可以:

  • 设置具有无提供程序容量的平台缓存分区。
  • 请求试用缓存。
  • 创建、编辑或删除缓存分区。
  • 分配每个分区的会话缓存和组织缓存容量以平衡 跨应用的性能。
  • 查看组织的当前缓存容量、细分和分区的快照 分配(以 KB 或 MB 为单位)。
  • 查看每个分区的详细信息。
  • 将任何分区设置为默认分区。

要使用平台缓存,请至少创建一个分区。每个分区都有一个会话缓存 和一个组织缓存段,您可以为每个段分配单独的容量。会期 缓存可用于存储单个用户会话的数据,而组织缓存用于存储以下数据 组织中的任何用户都可以访问。您可以将组织的缓存空间分布在任意数量的 分区。会话和组织缓存分配可以为零,也可以是 5 或更大,并且它们必须 是整数。所有分区分配的总和,包括默认分区, 等于平台缓存总分配。所有缓存段的总分配容量 必须小于或等于组织的整体容量。

您可以将任何分区定义为默认分区,但只能有一个默认分区 分区。当分区没有分配时,缓存操作(如 get 和 put)不会 调用,并且不会返回任何错误。

在默认分区内执行缓存操作时,可以省略该分区 密钥中的 name。

设置分区后,您可以使用 Apex 代码对 分区。例如,使用 和 类来放置、检索或 删除特定分区缓存上的值。使用 和 获取 使用完全限定的密钥对缓存操作进行分区或执行缓存操作。Cache.SessionPartitionCache.OrgPartitionCache.SessionCache.Org

打包平台缓存分区

打包使用平台缓存的应用程序时,请将任何引用的分区添加到 您的包。分区不会像其他分区那样自动拉入包中 依赖关系是。分区验证在运行时进行,而不是在编译时进行。 因此,如果包中缺少分区,则不会在 编译时。

注意

如果平台缓存代码用于包,请不要使用默认值 分区。相反,显式引用并打包非默认值 分区。无法部署任何包含默认分区的包。

平台缓存内部结构

平台缓存使用本地缓存和最少最近使用的 (LRU) 算法来改进 性能。

本地缓存

平台缓存使用本地缓存来提高性能,确保高效使用 网络,并支持原子事务。本地缓存是应用程序服务器的内存 客户端在请求期间与之交互的容器。缓存操作不交互 直接与缓存层,而是与本地缓存交互。

对于会话缓存,所有缓存项在首次请求时都会加载到本地缓存中。都 后续交互使用本地缓存。同样,组织缓存获取操作检索 缓存层中的值,并将其存储在本地缓存中。后续请求 此值是从本地缓存中检索的。所有可变操作,例如 put 和 remove,也对本地缓存执行。成功完成 request,可变操作被提交。

注意

本地缓存不支持并发操作。可变操作,例如 put 和 remove,针对本地缓存执行,并且仅在整个 Apex 时提交 请求成功。因此,其他并发请求看不到 可变操作。

原子事务

每个缓存操作都取决于它运行的 Apex 请求。如果整个请求 失败时,将回滚该请求中的所有缓存操作。在幕后,使用 本地缓存支持这些原子事务。

逐出算法

如果可能,平台缓存会使用 LRU 算法从缓存中逐出密钥。什么时候 达到缓存限制,密钥将被逐出,直到缓存减少到 100% 能力。如果使用会话缓存,系统将从所有现有缓存中均匀删除缓存 会话缓存实例。本地缓存还使用 LRU 算法。当最大本地 达到分区的缓存大小时,将从 本地缓存。

从会话缓存中存储和检索值

使用 和 类来管理会话缓存中的值。要管理任何分区中的值,请使用 类中的方法。如果您正在管理 将值缓存在一个分区中,请改用这些方法。

Cache.SessionCache.SessionPartitionCache.SessionCache.SessionPartition

Cache.Session 方法

若要在会话缓存中存储值,请调用该方法并提供键和值。密钥名称位于 格式。例如,对于 命名空间 ns1、分区 partition1 和键 orderDate,完全 限定的密钥名称为 。Cache.Session.put()namespace.partition.keyns1.partition1.orderDate

此示例存储一个缓存值,其中包含 钥匙。接下来,代码段检查密钥是否在缓存中,如果是,则检索值 从缓存中。DateTimeorderDateorderDate

// Add a value to the cache
DateTime dt = DateTime.parse('06/16/2015 11:46 AM');
Cache.Session.put('ns1.partition1.orderDate', dt);
if (Cache.Session.contains('ns1.partition1.orderDate')) {
    DateTime cachedDt = (DateTime)Cache.Session.get('ns1.partition1.orderDate');
}

若要引用调用类的默认分区和命名空间,请省略前缀并指定键名称。namespace.partition

Cache.Session.put('orderDate', dt);
if (Cache.Session.contains('orderDate')) {
    DateTime cachedDt = (DateTime)Cache.Session.get('orderDate');
}

前缀是指 运行代码的当前组织,无论组织是否具有命名空间 定义。如果组织的命名空间定义为 ns1,则以下两个语句是 等效。local

Cache.Session.put('local.myPartition.orderDate', dt);
Cache.Session.put('ns1.myPartition.orderDate', dt);

注意

中的前缀 已安装的托管软件包是指订阅者组织的命名空间,而不是 包的命名空间。缓存调用不是 允许在调用类不拥有的分区中。localput

该方法有多个版本(或 重载),每个版本采用不同的参数。例如,要指定 缓存的值不能被其他命名空间覆盖,设置这个的最后一个参数 方法设置为。以下示例还将 缓存值的生存期(3600 秒或 1 小时),并使该值可用于任何 命名空间。put()true

// Add a value to the cache with options
Cache.Session.put('ns1.partition1.totalSum', '500', 3600, Cache.Visibility.ALL, true);

若要从会话缓存中检索缓存的值,请调用该方法。由于返回一个对象,因此建议您强制转换返回的对象 值设置为特定类型。Cache.Session.get()Cache.Session.get()

// Get a cached value
Object obj = Cache.Session.get('ns1.partition1.orderDate');
// Cast return value to a specific data type
DateTime dt2 = (DateTime)obj;

Cache.SessionPartition 方法

如果要管理一个分区中的缓存值,请改用这些方法。获取分区对象后, 添加和检索缓存值的过程与使用这些方法类似。这些方法更易于使用,因为您只指定了 不带命名空间和分区前缀的键名称。Cache.SessionPartitionCache.SessionCache.SessionPartition

首先,获取会话分区并指定所需的分区。分区名称 包括命名空间前缀:。你 可以通过在 获取的 Partition 对象。以下示例获取 myNs 命名空间。接下来,如果缓存包含带有 key 的值,则检索此缓存值。一个新值是 添加了密钥和今天的日期。namespace.partitionBookTitleorderDate

// Get partition
Cache.SessionPartition sessionPart = Cache.Session.getPartition('myNs.myPartition');
// Retrieve cache value from the partition
if (sessionPart.contains('BookTitle')) {
    String cachedTitle = (String)sessionPart.get('BookTitle');
}
// Add cache value to the partition
sessionPart.put('OrderDate', Date.today());

此示例在分区上调用该方法 一个表达式,而不将分区实例分配给变量。get

// Or use dot notation to call partition methods
String cachedAuthor = (String)Cache.Session.getPartition('myNs.myPartition').get('BookAuthor');

从组织缓存中存储和检索值

使用 和 类管理组织缓存中的值。若要管理任何分区中的值,请使用 类。如果要管理 一个分区,使用方法 相反。

Cache.OrgCache.OrgPartitionCache.OrgCache.OrgPartition

Cache.Org方法

若要在组织缓存中存储值,请调用该方法并提供键和值。密钥名称的格式为 。例如,对于命名空间 ns1、partition partition1 和 key orderDate,完全限定键 名称为 。Cache.Org.put()namespace.partition.keyns1.partition1.orderDate

此示例存储一个缓存值,其中包含 钥匙。接下来,代码段检查密钥是否在缓存中,如果是,则检索值 从缓存中。DateTimeorderDateorderDate

// Add a value to the cache
DateTime dt = DateTime.parse('06/16/2015 11:46 AM');
Cache.Org.put('ns1.partition1.orderDate', dt);
if (Cache.Org.contains('ns1.partition1.orderDate')) {
    DateTime cachedDt = (DateTime)Cache.Org.get('ns1.partition1.orderDate');
}

若要引用调用类的默认分区和命名空间,请省略前缀并指定键名称。namespace.partition

Cache.Org.put('orderDate', dt);
if (Cache.Org.contains('orderDate')) {
    DateTime cachedDt = (DateTime)Cache.Org.get('orderDate');
}

前缀是指 运行代码的当前组织。前缀 指运行代码的当前组织的命名空间,无论是否 组织定义了一个命名空间。如果组织的命名空间定义为 ns1,则以下内容 两个语句是等效的。locallocal

Cache.Org.put('local.myPartition.orderDate', dt);
Cache.Org.put('ns1.myPartition.orderDate', dt);

注意

中的前缀 已安装的托管软件包是指订阅者组织的命名空间,而不是 包的命名空间。缓存调用不是 允许在调用类不拥有的分区中。localput

该方法有多个版本(或 重载),每个版本采用不同的参数。例如,要指定 缓存的值不能被其他命名空间覆盖,设置这个的最后一个参数 方法设置为。以下示例还将 缓存值的生存期(3600 秒或 1 小时),并使该值可用于任何 命名空间。put()true

// Add a value to the cache with options
Cache.Org.put('ns1.partition1.totalSum', '500', 3600, Cache.Visibility.ALL, true);

若要从组织缓存中检索缓存的值,请调用该方法。由于返回一个对象,因此建议您强制转换返回的值 更改为特定类型。Cache.Org.get()Cache.Org.get()

// Get a cached value
Object obj = Cache.Org.get('ns1.partition1.orderDate');
// Cast return value to a specific data type
DateTime dt2 = (DateTime)obj;

Cache.OrgPartition 方法

如果要管理一个分区中的缓存值,请改用这些方法。获取分区对象后, 添加和检索缓存值的过程与使用这些方法类似。这些方法更易于使用,因为您只指定密钥 不带命名空间和分区前缀的名称。Cache.OrgPartitionCache.OrgCache.OrgPartition

首先,获取组织分区并指定所需的分区。分区名称包括 命名空间前缀:。您可以 通过在 获取的 Partition 对象。以下示例获取 myNs 命名空间。如果缓存包含带有 key 的值,则检索此缓存值。使用键和今天的日期添加一个新值。namespace.partitionBookTitleorderDate

// Get partition
Cache.OrgPartition orgPart = Cache.Org.getPartition('myNs.myPartition');
// Retrieve cache value from the partition
if (orgPart.contains('BookTitle')) {
    String cachedTitle = (String)orgPart.get('BookTitle');
}
// Add cache value to the partition
orgPart.put('OrderDate', Date.today());

此示例在分区上调用该方法 一个表达式,而不将分区实例分配给变量。get

// Or use dot notation to call partition methods
String cachedAuthor = (String)Cache.Org.getPartition('myNs.myPartition').get('BookAuthor');

将 Visualforce 全局变量用于平台缓存

您可以从 Visualforce 页面访问存储在会话或组织缓存中的缓存值 替换为全局变量。

可以使用 或 全局变量。包括全局变量的 具有命名空间和分区名称的完全限定键名称。$Cache.Session$Cache.Org

此输出文本组件使用全局变量的 命名空间、分区和键。

<apex:outputText value="{!$Cache.Session.myNamespace.myPartition.key1}"/>

此示例与此类似,但使用全局变量从组织缓存中检索值。$Cache.Org

<apex:outputText value="{!$Cache.Org.myNamespace.myPartition.key1}"/>

注意

这 其余示例显示如何使用全局变量访问会话缓存。等效的组织缓存 示例是相同的,只是您改用全局变量。$Cache.Session$Cache.Org与 Apex 方法不同,您不能 省略要引用的前缀 组织中的默认分区。

myNamespace.myPartition

如果未为组织定义命名空间,则用于引用组织的命名空间。local

<apex:outputText value="{!$Cache.Session.local.myPartition.key1}"/>

缓存的值有时是具有属性或方法的数据结构,如 Apex list 或自定义类。在这种情况下,可以使用点表示法访问 or 表达式中的属性。例如,如果 的值声明为 .$Cache.Session$Cache.OrgList.size()numbersListList

<apex:outputText value="{!$Cache.Session.local.myPartition.numbersList.size}"/>

此示例访问声明为 自定义类。

<apex:outputText value="{!$Cache.Session.local.myPartition.myData.value}"/>

如果使用 ,请限定密钥名称 与实现接口的类和文字字符串,以及命名空间和 分区名称。在此示例中,实现的类称为 。CacheBuilderCacheBuilder_B_CacheBuilderCacheBuilderImpl

<apex:outputText value="{!$Cache.Session.myNamespace.myPartition.CacheBuilderImpl_B_key1}"/>

使用 CacheBuilder 接口安全地缓存值

平台缓存最佳实践是确保您的 Apex 代码处理缓存未命中 通过测试返回 null 的缓存请求。您可以自己编写此代码。或者,您可以 使用界面,使它 易于安全地将值存储和检索到会话或组织缓存中。

Cache.CacheBuilder

与其仅仅声明要在 Apex 类中缓存的内容,不如创建一个内部 实现接口的类。这 接口只有一个方法, 通过对基于方法的参数生成缓存值的逻辑进行编码来重写它。CacheBuilderdoLoad(String var)doLoad(String var)

若要检索已缓存的值,请不要直接调用该方法。相反,它第一次由 Salesforce 间接调用 引用实现 的类。 只要该值存在,后续调用就会从缓存中获取该值。如果值 不存在,则该方法被调用 再次生成值,然后返回它。因此,在使用接口时不会执行方法。由于该方法会检查缓存未命中,因此您不必编写 代码自行检查空值。CacheBuilderdoLoad(String var)CacheBuilderdoLoad(String var)put()CacheBuilderdoLoad(String var)

让我们看一个例子。假设您正在为 Visualforce 编写 Apex 控制器类 页。在 Apex 类中,您经常运行 SOQL 查询,该查询根据 用户 ID。SOQL 查询可能很昂贵,而且 Salesforce 用户记录通常不会更改 很多,所以用户信息是 的一个很好的候选者。CacheBuilder

在控制器类中,创建一个实现接口并重写该方法的内部类。然后将 SOQL 代码添加到方法中,并将用户 ID 作为其 参数。CacheBuilderdoLoad(String var)doLoad(String var)

class UserInfoCache implements Cache.CacheBuilder {
    public Object doLoad(String userid) {
        User u = (User)[SELECT Id, IsActive, username FROM User WHERE id =: userid];
        return u;
    }
}

若要从组织缓存中检索用户记录,请执行该方法,并向其传递类和用户 ID。同样,使用 和 从会话或 分区缓存。Org.get(cacheBuilder, key)UserInfoCacheSession.get(cacheBuilder, key)Partition.get(cacheBuilder, key)

User batman = (User) Cache.Org.get(UserInfoCache.class, ‘00541000000ek4c');

运行该方法时,Salesforce 会搜索 使用由字符串 00541000000ek4c 和 UserInfoCache 组成的唯一键的缓存。 如果 Salesforce 找到缓存的值,则返回该值。在此示例中,缓存的值是 与 ID 00541000000ek4c 关联的用户记录。如果 Salesforce 找不到值,则 再次执行 UserInfoCache 的方法(并重新运行 SOQL 查询),缓存用户 记录,然后返回它。get()doLoad(String var)

CacheBuilder 编码要求

在编写实现接口的类时,请遵循这些要求。CacheBuilder

  • 该方法必须采用参数,即使您不使用 参数。Salesforce 使用字符串和类名来 为缓存的值生成唯一键。doLoad(String var)String
  • 该方法可以返回任何 值,包括 null。如果返回 null 值,则将其直接传递给 CacheBuilder 使用者,而不是缓存。CacheBuilder 使用者应处理 正常使用 null 值。建议使用 null 值来反映临时失败 重新生成缓存键。doLoad(String var)
  • 实现的类必须是 非静态,因为 Salesforce 实例化了类的新实例并运行该方法以创建缓存的 价值。CacheBuilderdoLoad(String var)

平台缓存最佳实践

平台缓存可以大大提高应用程序的性能。 但是,请务必遵循这些准则以获得最佳缓存性能。在 通常,缓存几个大项目比缓存许多小项目更有效 分别。还要注意缓存限制,以防止意外缓存 拆迁。

评估性能影响

要测试平台缓存是否提高了应用程序的性能,请计算 使用和不使用缓存时经过的时间。不要依赖 Apex 调试日志时间戳 用于执行时间。请改用该方法。例如,首先调用以获取开始时间。执行 应用程序逻辑,从缓存或其他数据源获取数据。然后 计算经过的时间。System.currentTimeMillis()System.currentTimeMillis()

long startTime = System.currentTimeMillis();
// Your code here
long elapsedTime = System.currentTimeMillis() - startTime;
System.debug(elapsedTime);

正常处理缓存未命中

通过测试返回 null 的缓存请求,确保代码处理缓存未命中。自 帮助调试,添加缓存操作的日志记录信息。

或者,使用检查缓存未命中的接口。Cache.CacheBuilder

public class CacheManager {
    private Boolean cacheEnabled;
        
    public void CacheManager() {
        cacheEnabled = true;
    }
    
    public Boolean toggleEnabled() { // Use for testing misses
        cacheEnabled = !cacheEnabled;
        return cacheEnabled;
    }

    public Object get(String key) {
        if (!cacheEnabled) return null;
        Object value = Cache.Session.get(key);
        if (value != null) System.debug(LoggingLevel.DEBUG, 'Hit for key ' + key);
        return value;
    }

    public void put(String key, Object value, Integer ttl) {
        if (!cacheEnabled) return;
        Cache.Session.put(key, value, ttl);
        // for redundancy, save to DB
        System.debug(LoggingLevel.DEBUG, 'put() for key ' + key);
    }

    public Boolean remove(String key) {
        if (!cacheEnabled) return false;
        Boolean removed = Cache.Session.remove(key);
        if (removed) { 
            System.debug(LoggingLevel.DEBUG, 'Removed key ' + key);
            return true;
        } else return false;
    }

}

组缓存请求

如果可能,请对缓存请求进行分组,但请注意缓存限制。帮助改善 性能,则对键列表执行缓存操作,而不是对单个键执行缓存操作。为 例如,如果您知道调用 Visualforce 页面或执行任务需要哪些键 在 Apex 中,一次检索所有密钥。若要检索多个密钥,请调用初始化方法。get(keys)

缓存较大的项目

缓存几个大项目比单独缓存许多小项目更有效。 缓存许多小项目会降低性能并增加开销,包括总 序列化大小、序列化时间、缓存提交时间和缓存容量使用情况。

不要在一个请求中向平台缓存添加许多小项目。相反,将数据包装在 较大的项目,例如列表。如果列表很大,请考虑将其分解为多个项目。 下面是一个要避免的示例。

// Don't do this!

public class MyController {

    public void initCache() {
        List<Account> accts = [SELECT Id, Name, Phone, Industry, Description FROM 
            Account limit 1000];
        for (Integer i=0; i<accts.size(); i++) {
            Cache.Org.put('acct' + i, accts.get(i));    
        }
    }
}

相反,将数据包装在几个相当大的项目中,而不会超过 单个缓存项的大小。

// Do this instead.
        
public class MyController {

public void initCache() {
    List<Account> accts = [SELECT Id, Name, Phone, Industry, Description FROM 
        Account limit 1000];
    Cache.Org.put('accts', accts);    
    }
}

缓存较大项的另一个很好的例子是将数据封装在 Apex 类中。为 例如,您可以创建一个包装会话数据的类,并缓存该类的实例 而不是单个数据项。缓存类实例可提高整体性能 序列化大小和性能。

注意缓存限制

将项目添加到缓存时,请注意以下限制。缓存分区大小限制当达到缓存分区限制时,将逐出密钥,直到缓存减少 达到 100% 容量。平台缓存使用最近最少使用 (LRU) 算法进行逐出 缓存中的密钥。本地缓存大小限制

将项目添加到缓存时,请确保不超过本地缓存 请求中的限制。会话缓存的本地缓存限制为 500 KB,并且 1,000 KB 用于组织缓存。如果超出本地缓存限制,则可以逐出项目 在提交请求之前从本地缓存中获取。这种驱逐可能会导致 意外失误和序列化时间长,可能会浪费资源。单个缓存项大小限制单个缓存项的大小限制为 100 KB。如果序列化大小 项目超出此限制,则引发异常。这是一个很好的 练习捕获此异常并减小缓存项的大小。Cache.ItemSizeLimitExceededException

使用“缓存诊断”页(谨慎)

要确定使用了多少缓存,请查看“平台缓存诊断”页面。自 到达“诊断”页面:

  1. 确保为用户启用了缓存诊断(在“用户详细信息”上) 页面)。
  2. 在“平台缓存分区”页面上,单击分区名称。
  3. 单击该分区的“诊断”页面的链接。

“诊断”页面提供有价值的信息,包括容量使用情况、密钥、 以及缓存项的序列化和压缩大小。会话缓存和组织缓存 具有单独的诊断页面。会话缓存诊断是按会话进行的,它们 不要在所有活动会话中提供见解。

注意

生成诊断页面 收集所有与分区相关的信息,这是一项成本高昂的操作。使用它 谨慎。

最大限度减少成本高昂的操作

请考虑以下准则,以最大程度地减少成本高昂的操作。

  • 谨慎使用。这两种方法都是 昂贵,因为它们遍历所有与分区相关的信息,寻找或制作 给定分区的计算。Cache.Org.getKeys()Cache.Org.getCapacity()注意Cache.Session使用成本不高。
  • 避免调用该方法 其次是方法。如果您打算 若要使用键值,只需调用该方法并确保该值不等于 null。contains(key)get(key)get(key)
  • 仅在必要时清除缓存。清除缓存会遍历所有 与分区相关的缓存空间,这很昂贵。清除缓存后,您的 应用程序可能会通过调用数据库查询来重新生成缓存,并且 计算。这种再生可能是复杂而广泛的,并会影响您的 应用程序的性能。

权限集组

若要为权限集组提供 Apex 测试覆盖率,请使用类中的方法编写测试。

System.Test.calculatePermissionSetGroup()

该方法强制立即计算 聚合指定权限集组的权限。由于强制计算很重要 针对 Apex CPU 限制,并且可能需要复杂的数据设置,最佳做法是尽量减少 执行此操作的次数。calculatePermissionSetGroup()

将此测试设置为在“测试设置方法”中运行一次,然后在后续测试中重复使用数据。

@isTest public class PSGTest {
  @isTest static void testPSG() {
    // get the PSG by name (may have been modified in deployment)
    PermissionSetGroup psg = [select Id, Status from PermissionSetGroup where DeveloperName='MyPSG'];
    
    // force calculation of the PSG if it is not already Updated
    if (psg.Status != 'Updated') {
      Test.calculatePermissionSetGroup(psg.Id);
    }
    
    // assign PSG to current user (this fails if PSG is Outdated)
    insert new PermissionSetAssignment(PermissionSetGroupId = psg.Id, AssigneeId = UserInfo.getUserId());

    // additional tests to validate permissions granted by PSG
  }
}

Flow

Flow Builder 允许管理员构建应用程序(称为),这些应用程序通过收集来自动执行业务流程 数据并在您的 Salesforce 组织或外部系统中执行某些操作。

例如,您可以创建一个流来编写客户支持中心或 为销售团队生成实时报价。您可以在 Visualforce 页面或 Aura 组件中嵌入流程,然后 在 Apex 控制器中访问它。

  • 获取流变量
    您可以在 Apex 中检索特定流的流变量。
  • 从可调用操作向外部系统发出标注 当您定义在屏幕流中作为可调用操作
    运行并向外部系统发出标注的方法时,请使用修饰符。callout
  • 使用 Process.Plugin 接口将数据传递到流是一个内置接口
    ,可用于处理组织内的数据并将其传递给指定的流。该接口将 Apex 公开为服务,该服务接受输入值并将输出返回给流。Process.Plugin

获取流变量

您可以在 Apex 中检索特定流的流变量。

Apex 类提供用于检索流变量的方法, 它可以位于 Visualforce 页面中嵌入的流程中,也可以位于 由 subflow 元素调用。此示例演示如何使用此方法获取痕迹导航 (导航)来自 Visualforce 页面中嵌入的流程的信息。如果那流动 包含子流元素,并且每个引用的流还包含一个变量,Visualforce 页面可以为用户提供 面包屑,无论面试运行哪个流程。Flow.InterviewgetVariableValuevaBreadCrumb

public class SampleContoller {

   // Instance of the flow
   public Flow.Interview.Flow_Template_Gallery myFlow {get; set;}

   public String getBreadCrumb() {
      String aBreadCrumb;
      if (myFlow==null) { return 'Home';}
      else aBreadCrumb = (String) myFlow.getVariableValue('vaBreadCrumb');

      return(aBreadCrumb==null ? 'Home': aBreadCrumb);

   }
}

从可调用操作向外部系统发出标注

当您定义一个方法,该方法在屏幕流中作为可调用操作运行,并使 标注到外部系统时,请使用修饰符。

callout

当该方法作为可调用操作执行时,屏幕流使用此修饰符来确定 该操作是否可以在当前事务中安全执行。流管理员可以配置 让 Flow 决定是在新事务中执行该操作还是在当前事务中执行该操作的操作 一。当满足以下所有条件时,流将提交当前事务,开始 一个新事务,并安全地调用外部系统:

  • 该方法的标注修饰符是 。true
  • 屏幕流中操作的“事务控制”设置配置为允许流 决定。
  • 当前事务具有未提交的工作。

如果满足以下任一条件,则流将在当前事务中执行操作:

  • 标注修饰符为 。false
  • 该操作由非屏幕流执行。
  • 当前事务没有未提交的工作。

使用 Process.Plugin 接口将数据传递到流

Process.Plugin 是一个内置接口,可让您在 您的组织并将其传递给指定的流.该接口将 Apex 公开为服务,该服务接受 输入值并将输出返回给流。

提示

我们建议使用注解而不是接口。@InvocableMethodProcess.Plugin

  • 该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
  • 注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。

当您定义在组织中实现接口的 Apex 类时,它将在 Flow Builder 中作为旧版 Apex 操作提供。Process.Plugin

Process.Plugin具有这些顶级类。

  • Process.PluginRequest 从以下类传递输入参数: 实现流的接口。
  • Process.PluginResult 从以下类返回输出参数: 实现流的接口。
  • Process.PluginDescribeResult 将输入参数从流传递给 实现接口的类。此类确定输入参数和 插件所需的输出参数。Process.PluginResult

编写 Apex 单元测试时,实例化一个类并将其传递到接口方法中。传入系统参数 需要,创建一个地图并在构造函数中使用它。有关更多信息,请参见使用 Process.PluginRequest 类。invoke

  • 实现 Process.Plugin 接口是一个内置接口
    ,允许您在组织和指定流之间传递数据。Process.Plugin
  • 使用 Process.PluginRequest 类 该类将实现接口的类
    中的输入参数传递到流。Process.PluginRequest
  • 使用 Process.PluginResult 类 该类从实现流接口的类
    返回输出参数。Process.PluginResult
  • 使用 Process.PluginDescribeResult 类
    使用接口方法动态提供流的输入和输出参数。此方法返回类。Process.PlugindescribeProcess.PluginDescribeResult
  • Process.Plugin 数据类型转换
    了解如何在 Apex 和返回给 .例如,流中的文本数据将转换为 Apex 中的字符串数据。Process.Plugin
  • 潜在客户转换
    的示例 Process.Plugin 实现 在此示例中,Apex 类实现接口并将潜在顾客转换为客户、联系人和(可选)商机。还包括插件的测试方法。可以通过旧版 Apex 操作从流中调用此实现。Process.Plugin

实现 Process.Plugin接口

Process.Plugin是一个内置接口, 允许您在组织和指定流之间传递数据。

提示

我们建议使用注解而不是接口。@InvocableMethodProcess.Plugin

  • 该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
  • 注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。

实现接口的类必须调用这些方法。Process.Plugin

名字参数返回类型描述
describeProcess.PluginDescribeResult返回描述这一点的对象 方法调用。Process.PluginDescribeResult
invokeProcess.PluginRequestProcess.PluginResult当实现 接口被实例化。

示例实现

global class flowChat implements Process.Plugin { 

// The main method to be implemented. The Flow calls this at runtime.
global Process.PluginResult invoke(Process.PluginRequest request) { 
        // Get the subject of the Chatter post from the flow
        String subject = (String) request.inputParameters.get('subject');
        
        // Use the Chatter APIs to post it to the current user's feed
        FeedItem fItem = new FeedItem(); 
        fItem.ParentId = UserInfo.getUserId(); 
        fItem.Body = 'Flow Update: ' + subject; 
        insert fItem; 

        // return to Flow
        Map<String,Object> result = new Map<String,Object>(); 
        return new Process.PluginResult(result); 
    } 

    // Returns the describe information for the interface
    global Process.PluginDescribeResult describe() { 
        Process.PluginDescribeResult result = new Process.PluginDescribeResult(); 
        result.Name = 'flowchatplugin';
        result.Tag = 'chat';
        result.inputParameters = new 
           List<Process.PluginDescribeResult.InputParameter>{ 
               new Process.PluginDescribeResult.InputParameter('subject', 
               Process.PluginDescribeResult.ParameterType.STRING, true) 
            }; 
        result.outputParameters = new 
           List<Process.PluginDescribeResult.OutputParameter>{ }; 
        return result; 
    }
}

测试类

以下是上述类的测试类。

@isTest
private class flowChatTest {

    static testmethod void flowChatTests() {
      
        flowChat plugin = new flowChat();
        Map<String,Object> inputParams = new Map<String,Object>();

        string feedSubject = 'Flow is alive';
        InputParams.put('subject', feedSubject);

        Process.PluginRequest request = new Process.PluginRequest(inputParams);           
        
        plugin.invoke(request);
    } 
}

使用 Process.PluginRequest 类

该类传递输入 实现流接口的类中的参数。

Process.PluginRequest

提示

我们建议使用注解而不是接口。@InvocableMethodProcess.Plugin

  • 该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
  • 注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。

此类没有方法。构造 函数 签名:

Process.PluginRequest (Map<String,Object>)

下面是使用一个输入参数实例化类的示例。Process.PluginRequest

Map<String,Object> inputParams = new Map<String,Object>();
        string feedSubject = 'Flow is alive';
        InputParams.put('subject', feedSubject);
        Process.PluginRequest request = new Process.PluginRequest(inputParams);

代码示例

在此示例中,代码从流程中返回 Chatter 帖子的主题,并发布 它到当前用户的 饲料。

global Process.PluginResult invoke(Process.PluginRequest request) { 
        // Get the subject of the Chatter post from the flow
        String subject = (String) request.inputParameters.get('subject');
        
        // Use the Chatter APIs to post it to the current user's feed
        FeedPost fpost = new FeedPost(); 
        fpost.ParentId = UserInfo.getUserId(); 
        fpost.Body = 'Flow Update: ' + subject; 
        insert fpost; 

        // return to Flow
        Map<String,Object> result = new Map<String,Object>(); 
        return new Process.PluginResult(result); 
    } 

    // describes the interface 
    global Process.PluginDescribeResult describe() { 
        Process.PluginDescribeResult result = new Process.PluginDescribeResult(); 
        result.inputParameters = new List<Process.PluginDescribeResult.InputParameter>{ 
            new Process.PluginDescribeResult.InputParameter('subject', 
            Process.PluginDescribeResult.ParameterType.STRING, true) 
            }; 
        result.outputParameters = new List<Process.PluginDescribeResult.OutputParameter>{ }; 
        return result; 
    }
}

使用 Process.PluginResult 类

该类返回 从实现接口的类输出参数到 流。

Process.PluginResult

提示

我们建议使用注解而不是接口。@InvocableMethodProcess.Plugin

  • 该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
  • 注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。

您可以使用以下格式之一实例化类:

Process.PluginResult

  • Process.PluginResult (Map<String,Object>)
  • Process.PluginResult (String, Object)

当您有多个结果或不知道有多少个结果时,请使用地图 将返回结果。下面是实例化类的示例。

Process.PluginResult

string url = 'https://docs.google.com/document/edit?id=abc';
        String status = 'Success';
        Map<String,Object> result = new Map<String,Object>();
        result.put('url', url);
        result.put('status',status);
        new Process.PluginResult(result);

使用 Process.PluginDescribeResult 类

使用 interface 方法动态提供输入和 流的输出参数。此方法返回类。

Process.PlugindescribeProcess.PluginDescribeResult

提示

我们建议使用注解而不是接口。@InvocableMethodProcess.Plugin

  • 该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
  • 注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。

该类没有 支持以下功能。

Process.PluginDescribeResult

  • 查询
  • 数据修改
  • 电子邮件
  • 顶点嵌套标注

Process.PluginDescribeResult 类和 子类属性

下面是该类的构造函数。

Process.PluginDescribeResult

Process.PluginDescribeResult classname = new Process.PluginDescribeResult();
  • PluginDescribeResult 类属性
  • PluginDescribeResult.InputParameter 类属性
  • PluginDescribeResult.OutputParameter 类属性

下面是该类的构造函数。

Process.PluginDescribeResult.InputParameter

Process.PluginDescribeResult.InputParameter ip = new 
    Process.PluginDescribeResult.InputParameter(Name,Optional_description_string, 
      Process.PluginDescribeResult.ParameterType.Enum, Boolean_required);

下面是该类的构造函数。

Process.PluginDescribeResult.OutputParameter

Process.PluginDescribeResult.OutputParameter op = new 
    new Process.PluginDescribeResult.OutputParameter(Name,Optional description string, 
       Process.PluginDescribeResult.ParameterType.Enum);

要使用该类, 创建这些子类的实例。

Process.PluginDescribeResult

  • Process.PluginDescribeResult.InputParameter
  • Process.PluginDescribeResult.OutputParameter

Process.PluginDescribeResult.InputParameter是一个 输入参数列表,并具有以下内容 格式。

Process.PluginDescribeResult.inputParameters = 
      new List<Process.PluginDescribeResult.InputParameter>{ 
         new Process.PluginDescribeResult.InputParameter(Name,Optional_description_string, 
      Process.PluginDescribeResult.ParameterType.Enum, Boolean_required)

为 例:

Process.PluginDescribeResult result = new Process.PluginDescribeResult(); 
result.setDescription('this plugin gets the name of a user');
result.setTag ('userinfo');
result.inputParameters = new List<Process.PluginDescribeResult.InputParameter>{ 
    new Process.PluginDescribeResult.InputParameter('FullName', 
       Process.PluginDescribeResult.ParameterType.STRING, true),
    new Process.PluginDescribeResult.InputParameter('DOB', 
       Process.PluginDescribeResult.ParameterType.DATE, true),
    }; 

Process.PluginDescribeResult.OutputParameter是一个 输出参数列表,并具有以下内容 格式。

Process.PluginDescribeResult.outputParameters = new List<Process.PluginDescribeResult.OutputParameter>{ 
    new Process.PluginDescribeResult.OutputParameter(Name,Optional description string, 
       Process.PluginDescribeResult.ParameterType.Enum)

为 例:

Process.PluginDescribeResult result = new Process.PluginDescribeResult(); 
result.setDescription('this plugin gets the name of a user');
result.setTag ('userinfo');
result.outputParameters = new List<Process.PluginDescribeResult.OutputParameter>{
    new Process.PluginDescribeResult.OutputParameter('URL', 
        Process.PluginDescribeResult.ParameterType.STRING),

两个类都采用枚举。有效值为:

Process.PluginDescribeResult.ParameterType

  • 布尔
  • 日期
  • 日期时间
  • 十进制
  • 编号
  • 整数
  • 字符串

为 例:

Process.PluginDescribeResult result = new Process.PluginDescribeResult(); 
        result.outputParameters = new List<Process.PluginDescribeResult.OutputParameter>{
            new Process.PluginDescribeResult.OutputParameter('URL', 
            Process.PluginDescribeResult.ParameterType.STRING, true),
            new Process.PluginDescribeResult.OutputParameter('STATUS', 
            Process.PluginDescribeResult.ParameterType.STRING),
            };

Process.Plugin 数据类型 转换

了解如何在 Apex 和返回给 .例如,流中的文本数据会转换 在 Apex 中字符串数据。

Process.Plugin

提示

我们建议使用注解而不是接口。@InvocableMethodProcess.Plugin

  • 该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
  • 注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。
流数据类型数据类型
十进制
日期日期时间/日期
日期时间日期时间/日期
布尔仅具有 1 或 0 值的布尔值和数字值
发短信字符串

Lead 的示例 Process.Plugin 实现 转换

在此示例中,Apex 类实现 接口并转换潜在客户 进入客户、联系人和(可选)商机。插件的测试方法如下 也包括在内。可以通过旧版 Apex 操作从流中调用此实现。

Process.Plugin

提示

我们建议使用注解而不是接口。@InvocableMethodProcess.Plugin

  • 该接口不支持 Blob、Collection、sObject 和 Time 数据类型,并且它 不支持批量操作。在类上实现接口后,类 只能从流中引用。
  • 注释支持所有数据类型和批量操作。一旦您实现了 注解,可以从流、流程和自定义中引用该类 可调用操作 REST API 端点。
// Converts a lead as an action in a flow.
global class VWFConvertLead implements Process.Plugin {
    // This method runs when called by a flow's legacy Apex action.
    global Process.PluginResult invoke(
        Process.PluginRequest request) {
            
        // Set up variables to store input parameters from 
        // the flow.
        String leadID = (String) request.inputParameters.get(
            'LeadID');
        String contactID = (String) 
            request.inputParameters.get('ContactID');
        String accountID = (String) 
            request.inputParameters.get('AccountID');
        String convertedStatus = (String) 
            request.inputParameters.get('ConvertedStatus');
        Boolean overWriteLeadSource = (Boolean) 
            request.inputParameters.get('OverwriteLeadSource');
        Boolean createOpportunity = (Boolean) 
            request.inputParameters.get('CreateOpportunity');
        String opportunityName = (String) 
            request.inputParameters.get('ContactID');
        Boolean sendEmailToOwner = (Boolean) 
            request.inputParameters.get('SendEmailToOwner');   
        
        // Set the default handling for booleans. 
        if (overWriteLeadSource == null) 
            overWriteLeadSource = false;
        if (createOpportunity == null) 
            createOpportunity = true;
        if (sendEmailToOwner == null) 
            sendEmailToOwner = false;
        
        // Convert the lead by passing it to a helper method.
        Map<String,Object> result = new Map<String,Object>();
        result = convertLead(leadID, contactID, accountID, 
            convertedStatus, overWriteLeadSource, 
            createOpportunity, opportunityName, 
            sendEmailToOwner);
 
        return new Process.PluginResult(result); 
    }
    
    // This method describes the plug-in and its inputs from
    // and outputs to the flow.
    // Implementing this method makes the class available 
    // in Flow Builder as a legacy Apex action.
    global Process.PluginDescribeResult describe() {
        // Set up plugin metadata
        Process.PluginDescribeResult result = new 
            Process.PluginDescribeResult();
        result.description = 
            'The LeadConvert Flow Plug-in converts a lead into ' + 
            'an account, a contact, and ' + 
            '(optionally)an opportunity.';
        result.tag = 'Lead Management';
        
        // Create a list that stores both mandatory and optional 
        // input parameters from the flow.
        // NOTE: Only primitive types (STRING, NUMBER, etc.) are 
        // supported. Collections aren't supported.
        result.inputParameters = new 
            List<Process.PluginDescribeResult.InputParameter>{
            // Lead ID (mandatory)
            new Process.PluginDescribeResult.InputParameter(
                'LeadID', 
                Process.PluginDescribeResult.ParameterType.STRING, 
                true),
            // Account Id (optional)
            new Process.PluginDescribeResult.InputParameter(
                'AccountID', 
                Process.PluginDescribeResult.ParameterType.STRING, 
                false),
            // Contact ID (optional)
            new Process.PluginDescribeResult.InputParameter(
                'ContactID', 
                Process.PluginDescribeResult.ParameterType.STRING, 
                false),            
            // Status to use once converted
            new Process.PluginDescribeResult.InputParameter(
                'ConvertedStatus', 
                Process.PluginDescribeResult.ParameterType.STRING, 
                true),
            new Process.PluginDescribeResult.InputParameter(
                'OpportunityName', 
                Process.PluginDescribeResult.ParameterType.STRING, 
                false),
            new Process.PluginDescribeResult.InputParameter(
                'OverwriteLeadSource', 
                Process.PluginDescribeResult.ParameterType.BOOLEAN, 
                false),
            new Process.PluginDescribeResult.InputParameter(
                'CreateOpportunity', 
                Process.PluginDescribeResult.ParameterType.BOOLEAN, 
                false),
            new Process.PluginDescribeResult.InputParameter(
                'SendEmailToOwner', 
                Process.PluginDescribeResult.ParameterType.BOOLEAN, 
                false)                                                   
        };

        // Create a list that stores output parameters sent 
        // to the flow.
        result.outputParameters = new List<
            Process.PluginDescribeResult.OutputParameter>{
            // Account ID of the converted lead
            new Process.PluginDescribeResult.OutputParameter(
                'AccountID', 
                Process.PluginDescribeResult.ParameterType.STRING),
            // Contact ID of the converted lead
            new Process.PluginDescribeResult.OutputParameter(
                'ContactID', 
                Process.PluginDescribeResult.ParameterType.STRING),
            // Opportunity ID of the converted lead
            new Process.PluginDescribeResult.OutputParameter(
                'OpportunityID', 
                Process.PluginDescribeResult.ParameterType.STRING)                
        };

        return result;
    }
        
    /**
     * Implementation of the LeadConvert plug-in.
     * Converts a given lead with several options:
     * leadID - ID of the lead to convert
     * contactID - 
     * accountID - ID of the Account to attach the converted 
     *  Lead/Contact/Opportunity to.
     * convertedStatus - 
     * overWriteLeadSource - 
     * createOpportunity - true if you want to create a new 
     *  Opportunity upon conversion
     * opportunityName - Name of the new Opportunity.
     * sendEmailtoOwner - true if you are changing owners upon 
     *  conversion and want to notify the new Opportunity owner.
     *
     * returns: a Map with the following output:
     * AccountID - ID of the Account created or attached 
     *  to upon conversion.
     * ContactID - ID of the Contact created or attached 
     *  to upon conversion.
     * OpportunityID - ID of the Opportunity created 
     *  upon conversion.
     */
    public Map<String,String> convertLead (
                               String leadID,
                               String contactID,
                               String accountID,
                               String convertedStatus,
                               Boolean overWriteLeadSource,
                               Boolean createOpportunity,
                               String opportunityName,
                               Boolean sendEmailToOwner
        ) {
        Map<String,String> result = new Map<String,String>();
                                
        if (leadId == null) throw new ConvertLeadPluginException(
            'Lead Id cannot be null');
        
        // check for multiple leads with the same ID
        Lead[] leads = [Select Id, FirstName, LastName, Company 
            From Lead where Id = :leadID];
        if (leads.size() > 0) {
            Lead l = leads[0];
            // CheckAccount = true, checkContact = false
            if (accountID == null && l.Company != null) {
                Account[] accounts = [Select Id, Name FROM Account 
                    where Name = :l.Company LIMIT 1];
                if (accounts.size() > 0) {
                    accountId = accounts[0].id;
                }
            }
            
            // Perform the lead conversion.
            Database.LeadConvert lc = new Database.LeadConvert();
            lc.setLeadId(leadID);
            lc.setOverwriteLeadSource(overWriteLeadSource);
            lc.setDoNotCreateOpportunity(!createOpportunity);
            lc.setConvertedStatus(convertedStatus);
            if (sendEmailToOwner != null) lc.setSendNotificationEmail(
                sendEmailToOwner);
            if (accountId != null && accountId.length() > 0) 
                lc.setAccountId(accountId);
            if (contactId != null && contactId.length() > 0) 
                lc.setContactId(contactId);
            if (createOpportunity) {
                lc.setOpportunityName(opportunityName);
            }
            
            Database.LeadConvertResult lcr = Database.convertLead(
                lc, true);
            if (lcr.isSuccess()) {
                result.put('AccountID', lcr.getAccountId());
                result.put('ContactID', lcr.getContactId());
                if (createOpportunity) {
                    result.put('OpportunityID', 
                        lcr.getOpportunityId());
                }
            } else {
                String error = lcr.getErrors()[0].getMessage();
                throw new ConvertLeadPluginException(error);
            }
        } else { 
            throw new ConvertLeadPluginException(
                'No leads found with Id : "' + leadId + '"');
        }
        return result;
    }
        
    // Utility exception class
    class ConvertLeadPluginException extends Exception {}
}
// Test class for the lead convert Apex plug-in.
@isTest
private class VWFConvertLeadTest {
    static testMethod void basicTest() {
        // Create test lead
        Lead testLead = new Lead(
           Company='Test Lead',FirstName='John',LastName='Doe');
        insert testLead;
    
        LeadStatus convertStatus = 
           [Select Id, MasterLabel from LeadStatus 
           where IsConverted=true limit 1]; 
        
        // Create test conversion
        VWFConvertLead aLeadPlugin = new VWFConvertLead();
        Map<String,Object> inputParams = new Map<String,Object>();
        Map<String,Object> outputParams = new Map<String,Object>();

        inputParams.put('LeadID',testLead.ID);
        inputParams.put('ConvertedStatus', 
           convertStatus.MasterLabel);

        Process.PluginRequest request = new 
           Process.PluginRequest(inputParams);
        Process.PluginResult result;
        result = aLeadPlugin.invoke(request);
        
        Lead aLead = [select name, id, isConverted 
                       from Lead where id = :testLead.ID];
        System.Assert(aLead.isConverted);
        
    }

     /*
      * This tests lead conversion with 
      * the Account ID specified.
      */
    static testMethod void basicTestwithAccount() {

        // Create test lead
        Lead testLead = new Lead(
            Company='Test Lead',FirstName='John',LastName='Doe');
        insert testLead;
        
        Account testAccount = new Account(name='Test Account');
        insert testAccount;
    
           // System.debug('ACCOUNT BEFORE' + testAccount.ID);

        LeadStatus convertStatus = [Select Id, MasterLabel 
                    from LeadStatus where IsConverted=true limit 1]; 
        
        // Create test conversion
        VWFConvertLead aLeadPlugin = new VWFConvertLead();
        Map<String,Object> inputParams = new Map<String,Object>();
        Map<String,Object> outputParams = new Map<String,Object>();

        inputParams.put('LeadID',testLead.ID);
        inputParams.put('AccountID',testAccount.ID);
        inputParams.put('ConvertedStatus',
            convertStatus.MasterLabel);

        Process.PluginRequest request = new 
            Process.PluginRequest(inputParams);
        Process.PluginResult result;
        result = aLeadPlugin.invoke(request);
        
        Lead aLead = 
            [select name, id, isConverted, convertedAccountID 
             from Lead where id = :testLead.ID];
        System.Assert(aLead.isConverted);
        //System.debug('ACCOUNT AFTER' + aLead.convertedAccountID);
        System.AssertEquals(testAccount.ID, aLead.convertedAccountID);
    }

    /*
     * This tests lead conversion with the Account ID specified.
    */
    static testMethod void basicTestwithAccounts() {

        // Create test lead
        Lead testLead = new Lead(
            Company='Test Lead',FirstName='John',LastName='Doe');
        insert testLead;
        
        Account testAccount1 = new Account(name='Test Lead');
        insert testAccount1;
        Account testAccount2 = new Account(name='Test Lead');
        insert testAccount2;

           // System.debug('ACCOUNT BEFORE' + testAccount.ID);

        LeadStatus convertStatus = [Select Id, MasterLabel 
            from LeadStatus where IsConverted=true limit 1]; 
        
        // Create test conversion
        VWFConvertLead aLeadPlugin = new VWFConvertLead();
        Map<String,Object> inputParams = new Map<String,Object>();
        Map<String,Object> outputParams = new Map<String,Object>();

        inputParams.put('LeadID',testLead.ID);
        inputParams.put('ConvertedStatus',
            convertStatus.MasterLabel);

        Process.PluginRequest request = new 
            Process.PluginRequest(inputParams);
        Process.PluginResult result;
        result = aLeadPlugin.invoke(request);
        
        Lead aLead = 
            [select name, id, isConverted, convertedAccountID 
            from Lead where id = :testLead.ID];
        System.Assert(aLead.isConverted);
    }


     /*
      * -ve Test
      */    
    static testMethod void errorTest() {

        // Create test lead
        // Lead testLead = new Lead(Company='Test Lead',
        //   FirstName='John',LastName='Doe');
        LeadStatus convertStatus = [Select Id, MasterLabel 
            from LeadStatus where IsConverted=true limit 1]; 
        
        // Create test conversion
        VWFConvertLead aLeadPlugin = new VWFConvertLead();
        Map<String,Object> inputParams = new Map<String,Object>();
        Map<String,Object> outputParams = new Map<String,Object>();
        inputParams.put('LeadID','00Q7XXXXxxxxxxx');
        inputParams.put('ConvertedStatus',convertStatus.MasterLabel);

        Process.PluginRequest request = new 
            Process.PluginRequest(inputParams);
        Process.PluginResult result;
        try {
            result = aLeadPlugin.invoke(request);    
        }
        catch (Exception e) {
          System.debug('EXCEPTION' + e);
          System.AssertEquals(1,1);
        }
        
    }
    
    
     /*
      * This tests the describe() method
      */ 
    static testMethod void describeTest() {

        VWFConvertLead aLeadPlugin = 
            new VWFConvertLead();
        Process.PluginDescribeResult result = 
            aLeadPlugin.describe();
        
        System.AssertEquals(
            result.inputParameters.size(), 8);
        System.AssertEquals(
            result.OutputParameters.size(), 3);
        
     }

}