通过 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);
        
     }

}

Apex电子邮件

您可以使用 Apex 处理入站和出站电子邮件。

将 Apex 与以下电子邮件功能结合使用:

  • 入站电子邮件
    使用 Apex 处理发送到 Salesforce 的电子邮件。
  • 出站电子邮件
    使用 Apex 处理从 Salesforce 发送的电子邮件。

入站电子邮件

使用 Apex 处理发送到 Salesforce 的电子邮件。

您可以使用 Apex 接收和处理电子邮件和附件。Apex 收到电子邮件 电子邮件服务,并由利用 InboundEmail 对象的 Apex 类进行处理。

注意

Apex 电子邮件服务仅在 Developer、Enterprise、Unlimited 和 Performance 中可用 版本组织。

请参阅 Apex 电子邮件服务。

出站电子邮件

使用 Apex 处理从 Salesforce 发送的电子邮件。您可以使用 Apex 发送个人电子邮件和群发电子邮件。电子邮件可以包含所有标准电子邮件 属性(例如主题行和密件抄送地址),使用 Salesforce 电子邮件模板, 并且是纯文本或 HTML 格式,或由 Visualforce 生成的格式。

注意

Visualforce 电子邮件 模板不能用于群发电子邮件。

您可以使用 Salesforce 以 HTML 格式跟踪电子邮件的状态,包括 电子邮件已发送、首次打开和上次打开,以及打开的总次数。要使用 Apex 发送个人和群发电子邮件,请使用以下类:SingleEmail消息实例化用于发送单个电子邮件的电子邮件对象。语法 是:

Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();

MassEmailMessage (群发电子邮件)实例化用于发送群发电子邮件的电子邮件对象。语法 是:

Messaging.MassEmailMessage mail = new Messaging.MassEmailMessage();

消息包括 static 方法,该方法将 使用 OR 类实例化的电子邮件对象,并返回一个 SendEmailResult 对象。sendEmailSingleEmailMessageMassEmailMessage

发送电子邮件的语法 是:

Messaging.sendEmail(new Messaging.Email[] { mail } , opt_allOrNone);

其中 是 或 。EmailMessaging.SingleEmailMessageMessaging.MassEmailMessage

可选参数 指定是否阻止传递所有 当任何消息因错误而失败时的其他消息 (),或者它是否允许传递没有错误的消息 ().缺省值为 。opt_allOrNonesendEmailtruefalsetrue包括 static 和 方法,它们可以 在发送任何电子邮件之前被调用,以确保发送组织不会 在提交交易和发送电子邮件时超出其每日电子邮件限制。这 语法 是:reserveMassEmailCapacityreserveSingleEmailCapacity

Messaging.reserveMassEmailCapacity(count);

Messaging.reserveSingleEmailCapacity(count);

其中表示总数 电子邮件将发送到的地址数。count请注意以下事项:

  • 在提交 Apex 事务之前,不会发送电子邮件。
  • 调用该方法的用户的电子邮件地址将插入到电子邮件标头的“发件人地址”字段中。都 返回、退回或收到的外出回复的电子邮件将发送给调用 方法。sendEmail
  • 每个事务最多 10 种方法。 使用 Limits 方法可以验证事务中的方法数。sendEmailsendEmail
  • 使用该方法发送的单封电子邮件计入发送组织的每日单封电子邮件限制。当此限制为 到达时,对方法 using 的调用将被拒绝,并且用户会收到错误代码。然而 允许通过应用程序发送单封电子邮件。sendEmailsendEmailSingleEmailMessageSINGLE_EMAIL_LIMIT_EXCEEDED
  • 使用该方法发送的大量电子邮件 计入发送组织的每日群发电子邮件限制。当达到此限制时, 对方法 using 的调用被拒绝,并且用户会收到错误代码。sendEmailsendEmailMassEmailMessageMASS_MAIL_LIMIT_EXCEEDED
  • SendEmailResult 对象中返回的任何错误都指示未发送电子邮件。

Messaging.SingleEmailMessage有一个名为 setOrgWideEmailAddressId 的方法。它接受对象的对象 ID。如果向 setOrgWideEmailAddressId 传递了有效的 ID,则会在电子邮件中使用该字段 标头,而不是登录用户的显示名称。发送电子邮件 标头中的 address 也设置为 中定义的字段。OrgWideEmailAddressOrgWideEmailAddress.DisplayNameOrgWideEmailAddress.Address

注意

如果同时定义了 setSenderDisplayName,则用户会收到错误。OrgWideEmailAddress.DisplayNameDUPLICATE_SENDER_DISPLAY_NAME

有关更多信息,请参阅 Salesforce 帮助中的组织范围的电子邮件地址 .

// First, reserve email capacity for the current Apex transaction to ensure
// that we won't exceed our daily email limits when sending email after
// the current transaction is committed.
Messaging.reserveSingleEmailCapacity(2);

// Processes and actions involved in the Apex transaction occur next,
// which conclude with sending a single email.

// Now create a new single email message object
// that will send out a single email to the addresses in the To, CC & BCC list.
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();

// Strings to hold the email addresses to which you are sending the email.
String[] toAddresses = new String[] {'user@acme.com'}; 
String[] ccAddresses = new String[] {'smith@gmail.com'};
  

// Assign the addresses for the To and CC lists to the mail object.
mail.setToAddresses(toAddresses);
mail.setCcAddresses(ccAddresses);

// Specify the address used when the recipients reply to the email. 
mail.setReplyTo('support@acme.com');

// Specify the name used as the display name.
mail.setSenderDisplayName('Salesforce Support');

// Specify the subject line for your email address.
mail.setSubject('New Case Created : ' + case.Id);

// Set to True if you want to BCC yourself on the email.
mail.setBccSender(false);

// Optionally append the Salesforce email signature to the email.
// The email address of the user executing the Apex Code will be used.
mail.setUseSignature(false);

// Specify the text content of the email.
mail.setPlainTextBody('Your Case: ' + case.Id +' has been created.');

mail.setHtmlBody('Your case:<b> ' + case.Id +' </b>has been created.<p>'+
     'To view your case <a href=https://MyDomainName.my.salesforce.com/'+case.Id+'>click here.</a>');

// Send the email you have created.
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });

外部服务

外部服务将您的 Salesforce 组织连接到 Salesforce 外部的服务,例如 员工银行服务。注册外部服务后,可以在 您的 Apex 代码。在外部服务的已注册 API 中定义的对象和操作 specification 成为命名空间中的 Apex 类和方法。已注册服务的架构类型映射到 Apex 类型,并且 是强类型的,使 Apex 编译器为您完成繁重的工作。例如,您可以 从 Apex 对外部服务进行类型安全标注,而无需使用该类或对 JSON 字符串执行转换。

ExternalServiceHttp

Apex 中的 DataWeave

Apex 中的 DataWeave 使用 Mulesoft DataWeave 库从一个库读取和解析数据 格式化、转换并以其他格式导出。您可以创建 DataWeave 脚本 作为元数据,并直接从 Apex 调用它们。与 Apex 一样,DataWeave 脚本在 Salesforce 应用程序服务器,在执行时强制执行相同的堆和 CPU 限制 法典。

企业应用程序通常需要在 CSV 等格式之间转换数据、 JSON、XML 和 Apex 对象。Apex 中的 DataWeave 补充了 Apex 对 JSON 的原生支持 和 XML 处理,使数据转换更易于编码、更具可扩展性,以及 有效。Apex 开发人员可以更专注于解决业务问题,而不是 解决文件格式的细节。

DataWeave 是用于访问、解析和转换的 MuleSoft 表达式语言 通过 Mule 应用程序传输的数据。有关详细信息,请参阅 DataWeave 概述。

注意

您不必是 MuleSoft 客户或拥有任何特定的 Salesforce 许可证即可 在 Apex 中使用 DataWeave。

以下是 Apex 中 DataWeave 的一些用例。

  • 使用自定义日期格式序列化 Apex 对象
  • 使用 Apex 保留关键字序列化和反序列化 JSON
  • 执行自定义转换,例如删除或添加命名空间或删除后缀__c
  • 解析和转换符合 RFC 4180 的 CSV(逗号分隔值)数据

您可以为组织中的 DataWeave 资源创建列表视图,并查看已部署的 DataWeave 命名空间中的脚本。在“设置”的“快速查找”框中,输入 ,然后选择“DataWeave” 资源。选择要监视的字段,例如 DataWeave 资源 ID、名称、命名空间前缀和 API 版本。DataWeave

  • 在 Apex 中实现 DataWeave 将 DataWeave 脚本创建为元数据,并直接从 Apex
    调用它们。使用 DataWeave 命名空间中的类方法和异常来加载和执行脚本。
  • Apex
    中的 DataWeave 示例 以下是演示 Apex 中 DataWeave 的代码示例。
  • Apex
    中 DataWeave 的局限性 Apex 中的 DataWeave 具有这些局限性。

在 Apex 中实施 DataWeave

将 DataWeave 脚本创建为元数据,并直接从 Apex 调用它们。使用类 DataWeave 命名空间中的方法和异常来加载和执行脚本。

DataWeave 命名空间

DataWeave 命名空间提供类和 支持从 Apex 调用 DataWeave 脚本的方法。该类包含从已部署到 组织。然后,可以使用该方法与有效负载一起运行生成的脚本,以获取对象中的脚本输出。该类包含用于检索脚本的方法 使用类方法输出。查看更多 有关这些类和方法的信息,请参阅 DataWeave 命名空间。ScriptcreateScript().dwlexecute()DataWeave.ResultResultScript

对于每个 DataWeave 脚本,都会生成一个内部类型的类。内部类 扩展类。你可以使用 生成的类,而不是通过方法使用实际的脚本名称。当前正在使用的 DataWeave 脚本 无法删除通过此内部类引用的内容。要使生成的 DataWeaveScriptResource 类全局,在元数据对象中设置字段。DataWeaveScriptResource.ScriptNameDataWeave.ScriptDataWeaveScriptResource.ScriptNamecreateScript()isGlobalDataWeaveResource

<?xml version="1.0" encoding="UTF-8"?>
<DataWeaveResource xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>58.0</apiVersion>
<isGlobal>true</isGlobal>
</DataWeaveResource>

可捕获的异常可用于错误 处理。DataWeave 中发生的运行时脚本异常会向 Apex 公开 具有此异常类型。System.DataWeaveScriptException

DataWeave 脚本支持使用该函数进行日志记录。记录以下消息 源自 DataWeave 在 Apex 调试日志中作为事件反映在 Apex 代码日志下 类别。log(string, value)DATAWEAVE_USER_DEBUG

支持信息

Apex 支持 DataWeave 2.4 脚本语法, 除了这些限制:DataWeave 在 Apex 中的局限性。

这些工具 支持 DataWeave 脚本的开发。

  • 数据编织 互动学习是一个在线互动游乐场,您可以 用于测试 DataWeave 脚本。
  • DataWeave 2.0 VSCode 市场 扩展添加了代码突出显示和其他功能支持 编辑 DataWeave 脚本。

Apex 中的 DataWeave 示例

以下是演示 Apex 中 DataWeave 的代码示例。

要在 Apex 中使用 DataWeave,请按照以下说明和相关示例进行操作。

  • 创建 DataWeave 脚本源文件。例如:。csvToContacts.dwl%dw 2.0 input records application/csv output application/apex --- records map(record) -> { FirstName: record.first_name, LastName: record.last_name, Email: record.email } as Object {class: "Contact"}
  • 创建关联的元数据文件。例如:。csvToContacts.dwl-meta.xml<?xml version="1.0" encoding="UTF-8"?> <DataWeaveResource xmlns="http://soap.sforce.com/2006/04/metadata"> <apiVersion>58.0</apiVersion> <isGlobal>false</isGlobal> </DataWeaveResource>
  • 使用 Salesforce CLI 版本 v7.151.9 将源代码推送到临时组织,或者 高等。请参阅 Salesforce CLI 版本 笔记。
  • 从 Apex 调用 DataWeave 脚本并检查来自 anonymous 的结果 顶点。此示例调用脚本。csvToContacts.dwl// CSV data for Contacts String inputCsv = 'first_name,last_name,email\nCodey,"The Bear",codey@salesforce.com'; DataWeave.Script dwscript = new DataWeaveScriptResource.csvToContacts(); DataWeave.Result dwresult = dwscript.execute(new Map<String, Object>{'records' => inputCsv}); List<Contact> results = (List<Contact>)dwresult.getValue(); Assert.areEqual(1, results.size()); Contact codeyContact = results[0]; Assert.areEqual('Codey',codeyContact.FirstName); Assert.areEqual('The Bear',codeyContact.LastName);

注意

演示 Apex 中的 DataWeave 功能的大量代码示例包括 在 Developerforce 上可用。

DataWeave 在 Apex 中的局限性

Apex 中的 DataWeave 具有这些限制。

  • DataWeave Java 桥接,即绑定到静态 Java 方法的能力是 禁用。参见 Mule 简介 4. 与环境交互的功能(如 和 功能)也被禁用。这些检查是在脚本创建时完成的 的运行时。readURLenvVar
  • 必须为要强制转换为字符串的二进制输入 (Apex Blob) 指定编码:。binaryVariable as String {encoding: ‘utf8’ }”
  • DataWeave 受到限制,不允许加载其他库。因此 脚本必须是独立的。
  • 不支持 DataWeave 模块和导入其他脚本。例如,根据使用映射 不支持 DataWeave 脚本中的文件。import modules::MyMapping注意该功能支持 内置模块。请参阅 DataWeave 参考。
  • Apex 中的 DataWeave 不支持这些内容类型。
    • 平面文件格式 (application/flatfile)
    • 胜过 (application/xlsx)
    • 阿尔沃 (application/avro)
  • Apex 类必须为 API 版本 53.0 或更高版本才能访问 DataWeave 集成 方法。
  • 每个组织最多有 50 个 DataWeave 脚本。
  • 一个 DataWeave 脚本的最大正文大小为 100,000(十万) 字符。

注意

目前或将来都不支持 XML 实体扩展作为保护 防止拒绝服务攻击。

使用触发器审核 Feed 项

为 FeedItem 编写触发器以自动审核 组织或 Experience Cloud 站点。使用触发器确保帖子符合贵公司的 沟通策略,并且不包含不需要的字词或短语。

在插入触发器之前编写一个 Apex 以查看源项正文并更改 Feed 项的状态(如果其中包含列入黑名单的短语)。为源创建触发器 设置中的项目,输入快速 “查找”框,然后选择“FeedItem 触发器”。或者 您可以从开发者控制台创建触发器,方法是单击“文件”|”新品 |顶点触发器,然后从 sObject 下拉列表中选择 FeedItem 列表。FeedItem Triggers

此示例演示 FeedItem 上的“插入前”触发器,该触发器用于查看每个 新帖子。如果帖子包含不需要的短语,则触发器还会设置 发布到 .PendingReview

trigger ReviewFeedItem on FeedItem (before insert) {
    for (Integer i = 0; i<trigger.new.size(); i++) {

        // We don't want to leak "test phrase" information.

        if (trigger.new[i].body.containsIgnoreCase('test phrase')) {
            trigger.new[i].status = 'PendingReview'; 
            System.debug('caught one for pendingReview');
        }
    }
}

Experience Cloud 站点

Experience Cloud 站点是供您的员工、客户和合作伙伴使用的品牌空间 连接。您可以自定义和创建网站以满足您的业务需求,然后进行转换 它们之间无缝衔接。

使用该类与 Apex 中的 Experience Cloud 站点进行交互,并在命名空间的 Apex 类中使用 Connect。NetworkConnectApi

Connect in Apex 有一个类 返回有关网站信息的方法。许多 Apex 中的 Connect 方法采用参数,而某些 Apex 中的 Connect 方法采用参数ConnectApi.CommunitiescommunityIdsiteId

使用触发器审核 Chatter 私人消息

为 ChatterMessage 编写一个触发器,以自动审核 组织或 Experience Cloud 站点中的私人消息。使用触发器确保消息 符合贵公司的邮件策略,并且不包含列入黑名单的内容 的话。

适用于:Salesforce Classic
适用于:EnterprisePerformanceUnlimited 和 Developer Edition
用户权限 需要
要保存 ChatterMessage 的 Apex 触发器,请执行以下操作:作者 Apex和管理 Chatter 消息和 私信

在插入触发器之前编写 Apex 以查看私人消息 正文和有关发件人的信息。您可以将验证消息添加到记录或 正文字段,这会导致消息失败并返回错误给用户。

虽然您可以创建插入后触发器, ChatterMessage 不可更新,因此任何插入后触发 修改 ChatterMessage 将在运行时失败,并显示相应的错误消息。

要从“设置”中为私人消息创建触发器,请输入“快速查找”框,然后选择“ChatterMessage 触发器”。或者,您可以通过以下方式从开发人员控制台创建触发器 单击“文件”|”新品 |顶点触发器,然后从 sObject 下拉列表中选择 ChatterMessage。ChatterMessage Triggers

下表列出了公开的字段 喋喋不休的消息。

Apex 数据类型描述
同上编号Chatter 消息的唯一标识符
身体字符串发件人发布的 Chatter 消息的正文
发件人 ID编号发件人的用户 ID
发送日期日期时间发送邮件的日期和时间
SendingNetworkId编号发送消息的网络(站点)。仅当 已启用数字体验,并且至少在一个中启用了私人消息 网站。

此示例显示了 ChatterMessage 上用于查看的 before insert 触发器 每条新消息。此触发器调用类方法 ,以在插入每条新消息之前对其进行检查。moderator.review()

trigger PrivateMessageModerationTrigger on ChatterMessage (before insert) {
    ChatterMessage[] messages = Trigger.new;
    
    // Instantiate the Message Moderator using the factory method
    MessageModerator moderator = MessageModerator.getInstance();
    
    for (ChatterMessage currentMessage : messages) {
        moderator.review(currentMessage);    
    }
}

如果邮件违反了您的政策,例如,当邮件正文包含列入黑名单时 words,可以通过调用 Apex 方法阻止发送消息。您可以调用以在字段或 整个消息。以下代码片段显示了向消息“正文”字段添加错误的方法的一部分。addErroraddErrorreviewContent

if (proposedMsg.contains(nextBlockListedWord)) {
             theMessage.Body.addError(
                 'This message does not conform to the acceptable use policy');
             System.debug('moderation flagged message with word: ' 
                 + nextBlockListedWord);
             problemsFound=true;
             break;
          }

以下是完整的课程, 其中包含用于查看发件人和邮件内容的方法。部分 为简洁起见,已删除此类中的代码。MessageModerator

public class MessageModerator {
   private Static List<String> blocklistedWords=null;
   private Static MessageModerator instance=null;
   
   /**
     Overall review includes checking the content of the message,
     and validating that the sender is allowed to send messages.
   **/
   public void review(ChatterMessage theMessage) {
    reviewContent(theMessage);
    reviewSender(theMessage);
   }
   
   /**
     This method is used to review the content of the message. If the content
     is unacceptable, field level error(s) are added.
   **/
   public void reviewContent(ChatterMessage theMessage) {
      // Forcing to lower case for matching
      String proposedMsg=theMessage.Body.toLowerCase();  
      boolean problemsFound=false; // Assume it's acceptable
      // Iterate through the blocklist looking for matches
      for (String nextBlockListedWord : blocklistedWords) {
          if (proposedMsg.contains(nextBlockListedWord)) {
             theMessage.Body.addError(
                 'This message does not conform to the acceptable use policy');
             System.debug('moderation flagged message with word: ' 
                 + nextBlockListedWord);
             problemsFound=true;
             break;
          }
         }
         
       // For demo purposes, we're going to add a "seal of approval" to the 
       // message body which is visible.
       if (!problemsFound) {
         theMessage.Body = theMessage.Body + 
             ' *** approved, meets conduct guidelines';
       }
         
    }
   
   /**
     Is the sender allowed to send messages in this context?
     -- Moderators -- always allowed to send
     -- Internal Members -- always allowed to send
     -- Site Members -- in general only allowed to send if they have 
           a sufficient Reputation
     -- Site Members -- with insufficient reputation may message the 
           moderator(s)
   **/
   public void reviewSender(ChatterMessage theMessage) {
      // Are we in a Site Context?
      boolean isSiteContext = (theMessage.SendingNetworkId != null);
 
      // Get the User
      User sendingUser = [SELECT Id, Name, UserType, IsPortalEnabled 
                          FROM User where Id = :theMessage.SenderId ];  
      // ...          
   }   
   
   /**
     Enforce a singleton pattern to improve performance
   **/
   public static MessageModerator getInstance() {
     if (instance==null) {
        instance = new MessageModerator();
     }
     return instance;
   }
   

   /**
     Default contructor is private to prevent others from instantiating this class 
     without using the factory.
     Initializes the static members.
   **/
   private MessageModerator() {
      initializeBlockList();
   }
   /** 
     Helper method that does the "heavy lifting" to load up the dictionaries 
     from the database.  
     Should only run once to initialize the static member which is used for 
     subsequent validations.
   **/
   private void initializeBlockList() {
      if (blocklistedWords==null) {
          // Fill list of blocklisted words
          // ...
      }
   }
}