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
          // ...
      }
   }
}

在 Apex 功能中连接

本主题介绍用于使用公共 Connect 的类和方法 Apex 功能。

也可以直接转到 ConnectApi 命名空间参考内容。

  • 使用操作链接 操作链接
    是源元素上的按钮。单击操作链接可以将用户带到网页、启动文件下载或调用对 Salesforce 或外部服务器的 API 调用。操作链接包括 URL 和 HTTP 方法,并且可以包含请求正文和标头信息,例如用于身份验证的 OAuth 令牌。使用操作链接将 Salesforce 和第三方服务集成到源中,以便用户可以提高工作效率并加速创新。
  • 使用摘要和摘要元素
    Chatter 摘要是摘要元素的容器。抽象类是类的父类,表示源帖子,类表示源中的捆绑包和建议。ConnectApi.FeedElementConnectApi.FeedItemConnectApi.GenericFeedElement
  • 在 Experience Cloud 站点中访问 ConnectApi 数据 许多方法在单个 Experience Cloud 站点
    的上下文中工作。ConnectApi
  • 可供 Experience Cloud 访客用户
    使用的方法 如果您的 Experience Cloud 站点允许在不登录的情况下进行访问,则访客用户可以访问许多 Apex 方法。这些方法返回来宾用户有权访问的信息。
  • DBT 段支持的验证 创建或更新段
    时,ConnectApi.CdpSegmentInput 类需要接受一些 SQL 验证。

使用操作链接

操作链接是源元素上的按钮。单击操作链接可以将用户 到网页,启动文件下载,或调用对 Salesforce 或外部的 API 调用 服务器。操作链接包括 URL 和 HTTP 方法,并且可以包含请求正文和 标头信息,例如用于身份验证的 OAuth 令牌。使用操作链接进行集成 将 Salesforce 和第三方服务添加到源中,以便用户可以提高生产力和 加速创新。

工作流程

此源项包含一个操作链接组,其中包含一个 可见操作链接,加入

Feed 项上的加入操作链接

使用 Feed 元素创建和发布操作链接的工作流:

  1. (可选)创建操作链接 模板。
  2. 调用 ConnectApi.ActionLinks.createActionLinkGroupDefinition(communityId, actionLinkGroup) 来定义一个操作链接组,该操作链接组至少包含 一个操作链接。
  3. 调用 ConnectApi.ChatterFeeds.postFeedElement(communityId, feedElement) 以 发布 Feed 元素并将操作链接与其关联。

使用这些方法处理操作链接。

ConnectApi 方法任务
ActionLinks.createActionLinkGroupDefinition​(communityId, actionLinkGroup)ActionLinks.deleteActionLinkGroupDefinition(communityId, actionLinkGroupId)ActionLinks.getActionLinkGroupDefinition(communityId, actionLinkGroupId)创建操作链接组定义。将操作链接组与 源元素,首先创建操作链接组定义。然后发布提要 元素。
ChatterFeeds.postFeedElement(communityId, feedElement)发布具有关联操作功能的源元素。关联多达 10 个 带有 Feed 元素的操作链接组。
ActionLinks.getActionLink(communityId, actionLinkId)获取有关操作链接的信息,包括上下文的状态 用户。
ActionLinks.getActionLinkGroup(communityId, actionLinkGroupId)获取有关操作链接组的信息,包括上下文的状态 用户。
ActionLinks.getActionLinkDiagnosticInfo(communityId, actionLinkId)获取执行操作链接时返回的诊断信息。诊断 仅向可以访问操作链接的用户提供信息。
ChatterFeeds.getFeedElementsFromFeed()从指定的源类型获取源元素。如果 Feed 元素具有操作 链接,则操作链接数据将返回到 Feed 元素的 关联的操作功能。
  • 操作链接概述、身份验证和安全性
    了解 Apex 操作链接安全性、身份验证、标签和错误。
  • 操作链接用例
    使用操作链接将 Salesforce 和第三方服务与源集成。操作链接可以向 Salesforce 或第三方 API 发出 HTTP 请求。操作链接还可以下载文件或打开网页。本主题包含一个示例用例。

操作链接概述、身份验证和安全性

了解 Apex 操作链接安全性、身份验证、标签和错误。

工作流程

此源项包含一个操作链接组 带有一个可见的操作链接,即加入

Feed 项上的加入操作链接

使用 Feed 元素创建和发布操作链接的工作流:

  1. (可选)创建操作链接 模板。
  2. 调用 ConnectApi.ActionLinks.createActionLinkGroupDefinition(communityId, actionLinkGroup) 来定义一个操作链接组,该操作链接组至少包含 一个操作链接。
  3. 调用 ConnectApi.ChatterFeeds.postFeedElement(communityId, feedElement) 以 发布 Feed 元素并将操作链接与其关联。

操作链接模板

在安装程序中创建操作链接模板,以实例化具有通用的操作链接组 性能。您可以打包模板并将其分发给其他 Salesforce 组织。

在模板中指定绑定变量,并在以下情况下设置变量的值 实例化操作链接组。例如,对 API 版本使用绑定变量 number、用户 ID 或 OAuth 令牌。

您还可以在模板中指定上下文变量。当用户执行操作时 link,Salesforce 为这些变量提供值,例如谁执行了链接和 哪个组织。

若要实例化操作链接组,请调用 ConnectApi.ActionLinks.createActionLinkGroupDefinition(communityId, actionLinkGroup) 方法。指定模板 ID 和任何值 模板中定义的绑定变量。

请参阅设计操作链接模板。

操作链接的类型

定义操作链接时,在属性中指定操作链接类型。actionType

有四种类型的操作链接:

  • Api– 操作链接调用同步 操作 URL 中的 API。Salesforce 将状态设置为或基于您返回的 HTTP 状态代码 服务器。SuccessfulStatusFailedStatus
  • ApiAsync– 操作链接调用 异步 API 位于操作 URL。该操作将保持该状态,直到第三方做出 请求将 异步操作的状态或异步操作为 完成。PendingStatus/connect/action-links/actionLinkIdSuccessfulStatusFailedStatus
  • Download– 操作链接下载文件 从操作 URL。
  • Ui– 操作链接将用户带到 操作 URL 中的网页。

认证

定义操作链接时,请指定 URL () 和 HTTP 标头 () 需要向该 URL 发出请求。actionUrlheaders

如果外部资源需要身份验证,请在 资源需要。

如果 Salesforce 资源需要身份验证,您可以在 HTTP 标头,或者可以在 URL 中包含持有者令牌。

Salesforce 会自动对这些资源进行身份验证。

  • 模板中的相对 URL
  • 从 Apex 实例化操作链接组时开始的相对 URL/services/apexrest

请勿将这些资源用于敏感操作。

安全

HTTPS协议操作链接中的操作 URL 必须以 https:// 开头,或者是 与上一“身份验证”部分中的规则之一匹配的相对 URL。加密API 详细信息以加密方式存储,并为客户端进行模糊处理。的 、 和 数据 未从模板实例化的操作链接使用 组织的加密密钥。操作 URL、HTTP 标头和操作链接的 HTTP 请求正文 模板未加密。实例化操作链接时使用的绑定值 模板中的组使用组织的加密密钥进行加密。actionURLheadersrequestBody操作链接模板只有具有“自定义应用程序”用户权限的用户才能创建、编辑、删除和 安装程序中的包操作链接模板。不要在模板中存储敏感信息。使用绑定变量添加敏感变量 实例化操作链接组时的信息。操作链接组是 实例化后,值以加密格式存储。请参阅定义绑定变量 in 设计 操作链接模板。连接的应用程序通过连接的应用程序创建操作链接时,最好使用 使用使用者密钥连接的应用 这永远不会离开你的控制。连接的应用程序用于 服务器到服务器通信,并且不会编译到可能 反编译。有效期定义操作链接组时,请指定到期日期 ()。在该日期之后,操作链接在 组无法执行并从源中消失。如果操作链接组 定义包括一个 OAuth 令牌,将组的到期日期设置为与 OAuth 令牌的到期日期。expirationDate操作链接模板使用略有不同的机制来排除用户。请参阅设置 设计中的操作链接组过期时间 操作链接模板。排除用户或指定用户使用操作链接的属性 定义输入,以排除单个用户执行操作。excludeUserId使用操作链接的属性 定义输入,用于指定只有该用户才能执行操作的用户的 ID。如果你 不要指定属性,或者如果传递,则任何用户都可以执行该操作。你不能 为操作链接指定 和userIduserIdnullexcludeUserIduserId操作链接模板使用略有不同的机制来排除用户。请参阅设置 谁可以看到设计中的操作链接 操作链接模板。读取、修改或删除操作链接组定义操作链接和 操作链接组:定义和上下文用户的视图。定义 包括潜在的敏感信息,例如身份验证信息。这 上下文用户的视图按可见性选项进行筛选,这些值反映 上下文用户的状态。操作链接组定义可以包含敏感信息(例如 OAuth 令牌)。因此,要读取、修改或删除定义,用户必须具有 创建了定义或具有“查看所有数据”权限。此外,在 Connect REST 中 API,则必须通过创建定义的同一连接应用发出请求。在 Apex,则必须从创建定义的同一命名空间进行调用。

上下文变量

使用上下文变量传递有关 执行操作链接的用户以及调用该链接的上下文 通过调用操作链接发出的 HTTP 请求。您可以在操作链接定义输入请求正文或对象的 、 和 属性中使用上下文变量。你 还可以在操作链接模板的“操作 URL”、“HTTP 请求正文”和“HTTP 标头”字段中使用上下文变量。您可以编辑这些字段,包括添加和 在发布模板后删除上下文变量。actionUrlheadersrequestBodyConnectApi.ActionLinkDefinitionInput

上下文变量包括:

上下文变量描述
{!actionLinkId}用户执行的操作链接的 ID。
{!actionLinkGroupId}包含用户操作链接的操作链接组的 ID 执行。
{!communityId}用户在其中执行操作链接的站点的 ID。的值 您的内部组织是空键。“000000000000000000”
{!communityUrl}用户在其中执行操作链接的网站的 URL。的值 您的内部组织是空字符串。“”
{!orgId}用户在其中执行操作链接的组织 ID。
{!userId}执行操作链接的用户的 ID。

版本控制

为避免由于 API 中的升级或功能更改而导致的问题,我们建议使用 定义操作链接时的版本控制。例如,ConnectApi.ActionLinkDefinitionInput中的属性类似于 https://www.example.com/api/v1/exampleResource。actionUrl

可以使用模板更改 、 或 属性的值,即使在分发模板之后也是如此 在包中。假设您发布了需要新输入的新 API 版本。管理员可以 更改安装程序中操作链接模板中的输入,甚至更改操作链接 与源元素关联,使用新输入。但是,不能添加新绑定 变量添加到已发布的操作链接模板。actionUrlheadersrequestBody

如果 API 未进行版本控制,则可以使用 ConnectApi.ActionLinkGroupDefinitionInput 的属性来避免由于以下原因导致的问题 升级或更改 API 中的功能。请参阅设置操作链接组过期 设计操作中的时间 链接模板。expirationDate

错误

使用操作链接诊断信息方法 (ConnectApi.ActionLinks.getActionLinkDiagnosticInfo(communityId, actionLinkId)) 返回执行操作链接时的状态代码和错误。诊断信息仅提供给以下用户 可以访问操作链接。Api

本地化标签

操作链接使用在 ConnectApi.ActionLinkDefinitionInput 请求正文的属性和操作链接模板的 Label 字段中指定的一组预定义的本地化标签。labelKey

有关标签的列表,请参阅操作链接标签。

注意

如果标签键值对操作链接没有意义,请指定自定义标签 在操作链接模板的“标签”字段中,将“标签”设置为“标签” 键到无。但是,自定义标签未本地化。

操作链接用例

使用操作链接将 Salesforce 和第三方服务与源集成。一 操作链接可以向 Salesforce 或第三方 API 发出 HTTP 请求。操作链接还可以 下载文件或打开网页。本主题包含一个示例用例。

从源开始视频聊天

假设您在一家拥有 Salesforce 组织的公司担任 Salesforce 开发人员,并且 一家名为“VideoChat”的虚构公司的帐户。用户一直在说他们想要 通过移动设备执行更多操作。系统会要求你创建一个应用,让用户创建和 直接从他们的移动设备加入视频聊天。当用户在 Salesforce 中打开 VideoChat 应用程序时,系统会要求他们为视频聊天命名 聊天室,并邀请组或个人用户加入视频聊天室。当用户 单击确定,VideoChat 应用程序启动视频聊天室并发布 将项目提要给选定的组或用户,要求他们加入视频 通过单击标记为“加入”的操作链接进行聊天。当 被邀请者单击“加入”,操作链接将打开一个包含 视频聊天室。

批准和拒绝操作链接

作为考虑如何创建操作链接 URL 的开发人员,您会想到这些 要求:

  1. 当用户单击“加入”时,操作链接 URL 必须打开 他们被邀请到视频聊天室。
  2. 操作链接 URL 必须告诉视频聊天室谁正在加入。

若要动态创建操作链接 URL,请在 设置。

对于第一个要求,在“操作 URL 模板”字段中创建一个绑定变量。当用户单击“确定”以创建视频聊天时 room,您的 Apex 代码会生成一个唯一的房间 ID。Apex 代码使用该唯一房间 ID 作为 绑定变量值,当它实例化操作链接组时,将其与 源项,并发布源项。{!Bindings.roomId}

对于第二个要求,操作链接必须包含用户 ID。 操作链接支持 一组预定义的上下文变量。调用操作链接时,Salesforce 将变量替换为值。上下文变量包括有关人员的信息 单击操作链接以及在什么上下文中调用该链接。您决定在操作中包含上下文变量 URL,以便当用户单击源中的操作链接时,Salesforce 替换用户的 ID,视频聊天室知道谁在进入。{!userId}

这是“加入”操作链接的操作链接模板。

设置中操作 URL 字段中的上下文变量和绑定变量。

每个操作链接都必须与一个操作链接组相关联。该组定义 与其关联的所有操作链接共享的属性。即使您使用的是单个 操作链接(如本示例所示),它必须与组相关联。第一个字段 操作链接模板是操作链接组模板,在本例中为 是视频聊天,这是动作链接组模板的动作 链接模板与。

操作链接组模板

使用源和源元素

Chatter 摘要是摘要元素的容器。摘要 class 是 的父类 表示源的类 帖子和班级, 表示 Feed 中的捆绑包和建议。ConnectApi.FeedElementConnectApi.FeedItemConnectApi.GenericFeedElement

注意

Salesforce 帮助将源项目称为帖子,将捆绑包称为捆绑帖子。

能力

作为使饲料多样化的努力的一部分,饲料中的一些功能 元素已分解为功能。功能提供一致的方式 与源进行交互。请勿检查源元素类型以确定哪个 功能可用于 Feed 元素。检查功能,它会告诉你 明确说明什么是可用的。检查是否存在某种功能,以确定 客户端可以对 Feed 元素执行操作。

ConnectApi.FeedElement.capabilities 属性包含一组 能力。

功能既包括指示功能是可能的,也包括关联的数据 具有该功能。如果源元素上存在功能属性,则该功能为 可用,即使尚无与该功能关联的任何数据。例如 如果 capability 属性存在 在源元素上,上下文用户可以喜欢该源元素。如果能力 属性在 Feed 元素上不存在,因此不可能喜欢该 Feed 元素。chatterLikes

发布源元素时,请在 ConnectApi.FeedElementInput.capabilities 属性中指定其特征。

Salesforce UI 如何显示源项

客户端可以使用该属性 以确定它可以对 Feed 元素执行哪些操作以及如何呈现 Feed 元素。为 除 之外的所有源元素子类,客户端不必知道子类类型。 相反,客户端可以查看功能。源项确实具有功能,但 它们也有一些属性,例如 , 这些功能未公开为功能。因此,客户端必须处理 Feed 项 与其他 Feed 元素略有不同。ConnectApi.FeedElement.capabilitiesConnectApi.FeedItemactor

Salesforce UI 使用一种布局来 显示每个 Feed 项。这种单一布局为客户提供了一致的 Feed 视图 项目,并为开发人员提供了一种创建 UI 的简单方法。布局始终包含相同的 碎片和碎片始终处于同一位置。仅布局的内容 件数变化。

具有突出显示的布局元素的 Feed 项

源项 (ConnectApi.FeedItem) 布局元素包括:

  1. 执行组件 ()- A 源项创建者的照片或图标。(您可以在 Feed 商品类型级别。例如,仪表板快照源项类型显示 仪表板作为创建者。ConnectApi.FeedItem.actor
  2. 标头 () – 源项的上下文。一样 Feed 项可以具有不同的标题,具体取决于发布者及其位置 张贴。例如,Ted 将此源项发布到一个组。ConnectApi.FeedElement.header时间戳 () – 日期和时间 Feed 项的发布时间。如果源项的存续时间少于两天,则 日期和时间的格式为相对的本地化字符串,例如“17M 前”。否则,日期和时间的格式将设置为绝对的本地化 字符串。ConnectApi.FeedElement.relativeCreatedDate
  3. 正文 () – 所有源项都有一个正文。正文 可以是 ,这是当用户 不为源项提供文本。因为 body 可以是 ,所以不能将其用作默认情况 用于呈现文本。请改用该属性,该属性始终包含 价值。ConnectApi.FeedElement.bodynullnullConnectApi.FeedElement.header.text
  4. 辅助主体 () – 可视化 能力。请参阅功能。ConnectApi.FeedElement.capabilities

Salesforce 如何显示源项以外的源元素

一个 客户端可以使用该属性来确定它可以执行的操作 替换为 Feed 元素以及如何呈现 Feed 元素。本部分使用捆绑包作为 如何呈现源元素的示例,但这些属性可用于每个 feed 元素。通过这些功能,您可以处理源中的所有内容 一贯。ConnectApi.FeedElement.capabilities

注意

捆绑的帖子包含源跟踪的更改,并记录在案 仅限 Feed。

为了给客户提供干净、有条理的提要,Salesforce 聚合了 将 Feed 跟踪的更改整合到捆绑包中。要查看各个 Feed 元素,请点击 捆。

一系列 Feed 跟踪的更改

丛是具有 的对象(它是 的具体子类)。捆绑包布局 元素包括:ConnectApi.GenericFeedElementConnectApi.FeedElementConenctApi.BundleCapability

  • 标头 () – 对于源跟踪的更改捆绑包,此 文本为“更新此记录”。ConnectApi.FeedElement.headerUser Name
  • 时间戳 () – 日期和时间 Feed 项的发布时间。如果 Feed 项的发布时间不足 2 天,则日期 和 time 的格式为相对的本地化字符串,例如“17m 前”。否则,日期和时间的格式将设置为绝对的本地化 字符串。ConnectApi.FeedElement.relativeCreatedDate
  • 辅助主体 () – 束 显示 和 和 属性,用于 捆。如果 Feed 跟踪的更改超过两个,则捆绑包会显示 “显示所有更新”链接。ConnectApi.FeedElement.capabilities.bundle.changesfieldNameoldValuenewValue

Feed 元素可见性

用户看到的 Feed 元素取决于管理员配置 Feed 的方式 跟踪、共享规则和字段级安全性。例如,如果用户不这样做 有权访问记录,但他们看不到该记录的更新。如果用户可以 查看 Feed 元素的父元素,用户可以看到 Feed 元素。通常,用户 查看以下各项的 Feed 更新:

  • @mention用户的源元素(如果用户可以访问 Feed) 元素的父元素)
  • @mention用户所属组的 Feed 元素
  • 其父级记录是用户可以查看的记录的记录上的记录字段更改,包括 用户、组和文件记录
  • 发布给用户的 Feed 元素
  • 发布到用户所属或所属群组的 Feed 元素
  • 标准记录和自定义记录的源元素,例如任务、事件、潜在顾客、 帐户、文件

饲料类型

Feed 有很多种类型。每种 Feed 类型都定义了 Feed 元素的集合。

重要

Feed 元素的集合可能会在版本之间更改。除收藏夹之外的所有源类型都在枚举中公开,并传递给其中一个方法。此示例从上下文用户的新闻源中获取源元素 和主题 饲料。

ConnectApi.FeedTypeConnectApi.ChatterFeeds.getFeedElementsFromFeed

ConnectApi.FeedElementPage newsFeedElementPage = 
   ConnectApi.ChatterFeeds.getFeedElementsFromFeed(null, 
      ConnectApi.FeedType.News, 'me');

ConnectApi.FeedElementPage topicsFeedElementPage = 
   ConnectApi.ChatterFeeds.getFeedElementsFromFeed(null, 
      ConnectApi.FeedType.Topics, '0TOD00000000cld');

若要获取筛选器源,请调用其中一个方法。要获取 收藏夹源,请调用其中一个方法。ConnectApi.ChatterFeeds.getFeedElementsFromFilterFeedConnectApi.ChatterFavorites.getFeedElements

Feed 类型及其说明如下:

  • Bookmarks包含保存为书签的所有源项 上下文用户。
  • Company包含除 类型的 Feed 项之外的所有 Feed 项。要查看 Feed 项,请执行以下操作: 用户必须具有对其父级的共享访问权限。TrackedChange
  • DirectMessageModeration包含标记为 适度。私信审核源仅适用于具有以下条件的用户 审核体验 Chatter 消息权限。
  • DirectMessages包含上下文的所有源项 用户的直接消息。
  • Draft– 包含所有源项 上下文用户起草的。
  • Files包含包含已发布文件的所有源项 按上下文用户关注的人员或组。
  • Filter– 包含新闻源 筛选以包含其父级为指定对象类型的源项。
  • Groups包含来自所有组的所有源项 上下文用户拥有或属于其成员。
  • Home– 包含所有源项 与 Experience Cloud 站点中的任何托管主题相关联。
  • Landing包含 在请求 Feed 时最能提高用户参与度的所有 Feed 项。允许 客户端,以避免在个性化源不多时出现空源 项目。
  • Moderation包含标记为要审核的所有 Feed 项, 除了直接消息。审核源仅适用于 “审核体验源”权限。
  • Mute– 包含所有 上下文用户静音的源项。
  • News包含上下文用户人员的所有更新 关注用户所属的组,并记录用户所属的组 以后。包含其父级为上下文的记录的所有更新 用户。
  • PendingReview包含所有 Feed 项和注释 正在等待审查。
  • People包含所有人发布的所有提要项 上下文用户关注。
  • Record包含其父级为 指定的记录,可以是组、用户、对象、文件或任何其他 标准或自定义对象。当记录为组时,源还包含源 提及该组的项目。当记录是用户时,源仅包含 该用户的源项目。您可以获取其他用户的记录 Feed。
  • Streams包含最多 25 个启用源的实体的任意组合的所有源项 上下文用户在流中订阅的。启用 Feed 的实体示例 包括人员、组和记录,
  • To包含提及上下文用户的所有源项。包含源项 上下文用户注释的 Context 用户和 Feed 项目由 Context 用户创建,这些项目是 评论。
  • Topics包含包含指定 主题。
  • UserProfile– 包含源项 在用户更改可在源中跟踪的记录时创建。包含饲料 父级为用户的项目和@mention用户的源项目。此提要是 与新闻源不同,后者返回更多源项,包括组更新。 您可以获取其他用户的用户个人资料 Feed。
  • Favorites– 包含保存者 上下文用户。收藏夹包括源搜索、列表视图和主题。

使用 postFeedElement 发布源项

提示

方法有 发布提要项目的最简单、最有效的方法,因为与这些方法不同,它们不需要您 传递源类型。Feed 项是您可以发布的唯一 Feed 元素类型。postFeedElementpostFeedItem

使用这些方法发布源项。postFeedElement(communityId, subjectId, feedElementType, text)发布纯文本 Feed 元素。postFeedElement(communityId, feedElement, feedElementFileUpload)(版本 35.0 及更早版本)发布富文本 Feed 元素。包括提及和主题标签主题,附加文件 添加到源元素,并将操作链接组与源元素相关联。您可以 还可以使用此方法共享 Feed 元素并添加注释。postFeedElement(communityId, feedElement) (版本 36.0 和 稍后)发布富文本 Feed 元素。包括提及和主题标签主题,附上 已将文件上传到 Feed 元素,并将操作链接组与 feed 元素。您还可以使用此方法共享 Feed 元素并添加 评论。

发布源项时,将创建标准对象或自定义对象的子对象。指定 参数中的父对象或在参数中传递的对象的属性中的父对象。这 该参数的值决定了在其中显示源项的源。返回对象中的属性包含以下信息 父对象。subjectIdsubjectIdConnectApi.FeedElementInputfeedElementsubjectIdparentConnectApi.FeedItem

使用这些方法可以完成这些任务。发给自己此代码将源项发布到上下文用户。指定 ,这是上下文的别名 用户的 ID。它还可以指定上下文用户的 同上。subjectIdme

ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(null, 'me', ConnectApi.FeedElementType.FeedItem, 'Working from home today.');

新发布的属性 源项包含上下文用户的parentConnectApi.UserSummary发布给其他用户此代码将源项发布到上下文用户以外的用户。指定用户 目标的 ID 用户。subjectId

ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(null, '005D00000016Qxp', ConnectApi.FeedElementType.FeedItem, 'Kevin, do you have information about the new categories?');

新发布的属性 源项包含目标用户的parentConnectApi.UserSummary发帖到小组此代码将源项发布到组。指定组 同上。subjectId

ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();
ConnectApi.MentionSegmentInput mentionSegmentInput = new ConnectApi.MentionSegmentInput();
ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();

messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();

mentionSegmentInput.id = '005RR000000Dme9';
messageBodyInput.messageSegments.add(mentionSegmentInput);

textSegmentInput.text = 'Could you take a look?';
messageBodyInput.messageSegments.add(textSegmentInput);

feedItemInput.body = messageBodyInput;
feedItemInput.feedElementType = ConnectApi.FeedElementType.FeedItem;
feedItemInput.subjectId = '0F9RR0000004CPw';

ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(Network.getNetworkId(), feedItemInput);

新发布的属性 Feed 项包含指定组的 。parentConnectApi.ChatterGroupSummary发布到记录(如文件或帐户)此代码将源项发布到记录并提及组。指定 记录 同上。subjectId

ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();
ConnectApi.MentionSegmentInput mentionSegmentInput = new ConnectApi.MentionSegmentInput();
ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();

messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();

textSegmentInput.text = 'Does anyone know anyone with contacts here?';
messageBodyInput.messageSegments.add(textSegmentInput);

// Mention a group.
mentionSegmentInput.id = '0F9D00000000oOT';
messageBodyInput.messageSegments.add(mentionSegmentInput);

feedItemInput.body = messageBodyInput;
feedItemInput.feedElementType = ConnectApi.FeedElementType.FeedItem;

// Use a record ID for the subject ID.
feedItemInput.subjectId = '001D000000JVwL9';

ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(null, feedItemInput);

新源项的属性 取决于 中指定的记录类型。如果记录类型为 File,则 父级是 。如果 记录类型为 Group,父类型为 。如果记录类型为 User,则 父级是 。对于所有人 其他记录类型,如本使用 Account 的示例中所示,父记录类型为 。parentsubjectIdConnectApi.FileSummaryConnectApi.ChatterGroupSummaryConnectApi.UserSummaryConnectApi.RecordSummary

从 Feed 中获取 Feed 元素

提示

若要返回包含源元素的源,请调用这些方法。饲料 元素类型包括 Feed Item、Bundle 和 Recommendation。

对于每种 Feed 类型,从 Feed 中获取 Feed 项的方式相似,但不完全相同。从 、 、 、 和 源中获取源元素CompanyDirectMessageModerationDirectMessagesHomeModerationPendingReview若要从这些源中获取源元素,请使用这些不需要 .subjectId

  • getFeedElementsFromFeed(communityId, feedType)
  • getFeedElementsFromFeed(communityId, feedType, pageParam, pageSize, sortParam)
  • getFeedElementsFromFeed(communityId, feedType, recentCommentCount, density, pageParam, pageSize, sortParam)
  • getFeedElementsFromFeed(communityId, feedType, recentCommentCount, density, pageParam, pageSize, sortParam, filter)
  • getFeedElementsFromFeed(communityId, feedType, recentCommentCount, density, pageParam, pageSize, sortParam, filter, threadedCommentsCollapsed)

从源中获取源元素Favorites若要从收藏夹源中获取源元素,请指定 .对于这些 Feed,必须 上下文用户的 ID 或别名。favoriteIdsubjectIdme

  • getFeedElements(communityId, subjectId, favoriteId)
  • getFeedElements(communityId, subjectId, favoriteId, pageParam, pageSize, sortParam)
  • getFeedElements(communityId, subjectId, favoriteId, recentCommentCount, elementsPerBundle, pageParam, pageSize, sortParam)

从源中获取源元素Filter若要从筛选器源中获取源元素,请指定 .指示对象 type 和 是对象 ID 的前三个字符。必须是 上下文用户或别名 。keyPrefixkeyPrefixsubjectIdme

  • getFeedElementsFromFilterFeed(communityId, subjectId, keyPrefix)
  • getFeedElementsFromFilterFeed(communityId, subjectId, keyPrefix, pageParam, pageSize, sortParam)
  • getFeedElementsFromFilterFeed(communityId, subjectId, keyPrefix, recentCommentCount, elementsPerBundle, density, pageParam, pageSize, sortParam)

从 、 、 、 、 、 、 和 源 中获取源元素BookmarksFilesGroupsMuteNewsPeopleRecordStreamsToTopicsUserProfile要从这些 Feed 类型中获取 Feed 元素,请指定主题 ID。如果是,则可以是任何记录 ID,包括组 ID。如果是,则必须是流 ID。如果是,则必须是主题 ID。如果是,则可以是任何用户 ID。如果 是任何其他值,则必须是上下文用户的 ID 或别名。feedTypeRecordsubjectIdfeedTypeStreamssubjectIdfeedTypeTopicssubjectIdfeedTypeUserProfilesubjectIdfeedTypesubjectIdme

  • getFeedElementsFromFeed(communityId, feedType, subjectId)
  • getFeedElementsFromFeed(communityId, feedType, subjectId, pageParam, pageSize, sortParam)
  • getFeedElementsFromFeed(communityId, feedType, subjectId, recentCommentCount, density, pageParam, pageSize, sortParam)
  • getFeedElementsFromFeed(communityId, feedType, subjectId, recentCommentCount, density, pageParam, pageSize, sortParam, filter)
  • getFeedElementsFromFeed(communityId, feedType, subjectId, recentCommentCount, density, pageParam, pageSize, sortParam, filter, threadedCommentsCollapsed)

从 Feed 中获取 Feed 元素Record对于 ,请指定 记录 ID。subjectId

提示

记录可以是支持源的任何类型的记录, 包括组。Salesforce UI 中组页面上的源是一条记录 饲料。

  • getFeedElementsFromFeed(communityId, feedType, subjectId, recentCommentCount, density, pageParam, pageSize, sortParam, showInternalOnly)
  • getFeedElementsFromFeed(communityId, feedType, subjectId, recentCommentCount, density, pageParam, pageSize, sortParam, customFilter)
  • getFeedElementsFromFeed(communityId, feedType, subjectId, recentCommentCount, elementsPerBundle, density, pageParam, pageSize, sortParam, showInternalOnly)
  • getFeedElementsFromFeed(communityId, feedType, subjectId, recentCommentCount, elementsPerBundle, density, pageParam, pageSize, sortParam, showInternalOnly, filter)
  • getFeedElementsFromFeed(communityId, feedType, subjectId, recentCommentCount, elementsPerBundle, density, pageParam, pageSize, sortParam, showInternalOnly, customFilter)
  • getFeedElementsFromFeed(communityId, feedType, subjectId, recentCommentCount, elementsPerBundle, density, pageParam, pageSize, sortParam, showInternalOnly, filter, threadedCommentsCollapsed)
  • getFeedElementsFromFeed(communityId, feedType, subjectId, recentCommentCount, elementsPerBundle, density, pageParam, pageSize, sortParam, showInternalOnly, customFilter, threadedCommentsCollapsed)

在 Experience Cloud 中访问 ConnectApi 数据 网站

许多方法在上下文中工作 单个 Experience Cloud 站点。

ConnectApi

许多方法都包含作为第一个参数。如果 您没有启用、使用或用于此目的的数字体验 论点。ConnectApicommunityIdinternalnull

如果启用了数字体验,则参数 指定是否在默认 Experience Cloud 站点的上下文中执行方法 (通过指定 或 ) 或在特定站点的上下文中 (通过 指定 ID)。其他实体引用的任何实体,例如评论或提要项 方法中的参数必须位于指定的站点中。该 ID 包含在返回的 URL 中 在输出中。communityIdinternalnull

某些方法包括作为参数。不像 ,如果你没有 启用数字体验后,您不能使用这些方法。站点 ID 包含在 URL 中 在输出中返回。ConnectApisiteIdcommunityId

输出对象中返回的大多数 URL 是 连接 REST API 资源。ConnectApi如果指定 ID,则输出中返回的 URL 将使用以下命令 格式:

/connect/communities/communityId/resource

如果 指定 ,输出中返回的 URL 使用相同的 格式:

internal

/connect/communities/internal/resource

如果指定 ,则输出中返回的 URL 使用其中之一 格式:

null

/chatter/resource

/connect/resource

可供 Experience Cloud 访客用户使用的方法

如果您的 Experience Cloud 站点允许在不登录的情况下进行访问,则访客用户有权访问 到许多 Apex 方法。这些方法返回来宾用户有权访问的信息 自。这些方法的所有重载都可供来宾用户使用。

重要

如果 此处列出的方法的重载表示 Chatter 是必需的,您还必须启用 public 访问您的 Experience Cloud 站点,使该方法可用于 来宾用户。如果未启用公共访问,则通过以下方法检索的数据 require Chatter 无法在公共站点页面上正确加载。

  • Announcements方法:
    • getAnnouncements()
  • ChatterFeeds方法:
    • getComment()
    • getCommentInContext()
    • getCommentsForFeedElement()
    • getExtensions()
    • getFeed()
    • getFeedElement()
    • getFeedElementBatch()
    • getFeedElementPoll()
    • getFeedElementsFromFeed()
    • getFeedElementsUpdatedSince()
    • getFeedKWithFeedElements()
    • getLike()
    • getLikeKsForComment()
    • getLikesForFeedElement()
    • getLinkMetadata()
    • getPinnedFeedElementsFromFeed()
    • getRelatedPosts()
    • getThreadsForFeedComment()
    • getVotesForComment()
    • getVotesForFeedElement()
    • searchFeedElements()
    • searchFeedElementsInFeed()
    • updatePinnedFeedElements()
  • ChatterGroups方法:
    • getGroup()
    • getGroups()
    • getMembers()
    • searchGroups()
  • ChatterUsers方法:
    • getFollowers()
    • getFollowings()
    • getReputation()
    • getUser()
    • getUserBatch()
    • getUserGroups()
    • getUsers()
    • searchUserGroupDetails()
    • searchUsers()
  • CommerceCart methods:
    • addItemsToCart()
    • addItemToCart()
    • applyCartCoupon()
    • copyCartToWishlist()
    • createCart()
    • deleteCart()
    • deleteCartCoupon()
    • deleteCartItem()
    • deleteInventoryReservation() (developer preview)
    • getCartCoupons()
    • getCartItems()
    • getCartSummary()
    • getOrCreateActiveCartSummary()
    • makeCartPrimary()
    • setCartMessagesVisibility()
    • updateCartItem()
    • upsertInventoryReservation() (developer preview)
  • CommerceCatalog methods:
    • getProduct()
    • getProductCategory()
    • getProductCategoryChildren()
    • getProductCategoryPath()
    • getProductChildCollection()
  • CommercePromotions methods:
    • decreaseRedemption()
    • evaluate()
    • increaseRedemption()
  • CommerceSearch methods:
    • getSortRules()
    • getSuggestions()
    • searchProducts()
  • CommerceStorePricing methods:
    • getProductPrice()
    • getProductPrices()
  • Communities methods:
    • getCommunity()
  • EmployeeProfiles methods:
    • getPhoto()
  • Knowledge methods:
    • getTopViewedArticlesForTopic()
    • getTrendingArticles()
    • getTrendingArticlesForTopic()
  • ManagedContent methods:
    • getAllContent()
    • getAllDeliveryChannels()
    • getAllManagedContent()
    • getContentByContentKeys()
    • getContentByIds()
    • getManagedContentByContentKeys()
    • getManagedContentByIds()
    • getManagedContentByTopics()
    • getManagedContentByTopicsAndContentKeys()
    • getManagedContentByTopicsAndIds()
  • ManagedContentDelivery methods:
    • getCollectionItemsForChannel()
    • getCollectionItemsForSite()
    • getManagedContentChannel()
    • getManagedContentForChannel()
    • getManagedContentForSite()
    • getManagedContentsForChannel()
    • getManagedContentsForSite()
  • ManagedTopics methods:
    • getManagedTopic()
    • getManagedTopics()
  • MarketingIntegration methods:
    • submitForm()
  • NavigationMenu methods:
    • getCommunityNavigationMenu()
  • NextBestActions methods:
    • executeStrategy()
    • setRecommendationReaction()
  • Personalization methods:
    • getAudience()
    • getAudienceBatch()
    • getAudiences()
    • getTarget()
    • getTargetBatch()
    • getTargets()
  • Recommendations方法:
    • getRecommendationsForUser()注意客人只能使用文章和文件推荐 用户。
  • Sites方法:
    • searchSite()
  • Topics方法:
    • getGroupsRecentlyTalkingAboutTopic()
    • getRecentlyTalkingAboutTopicsForGroup()
    • getRecentlyTalkingAboutTopicsForUser()
    • getRelatedTopics()
    • getTopic()
    • getTopics()
    • getTrendingTopics()
  • UserProfiles方法:
    • getPhoto()

DBT 段支持的验证

创建或更新区段时,ConnectApi.CdpSegmentInput 类受 一些 SQL 验证。

您可以使用带有类的 createSegment(input) 方法创建区段。同样,您可以使用具有相同输入类的 updateSegment(segmentApiName, input) 方法更新区段。 嵌套在类中的 ConnectApi.CdpSegmentDbtModelInput 输入类提供 验证 SQL。ConnectApi.CdpSegmentInputConnectApi.CdpSegmentInput

的财产受以下条款的约束 验证。sqlConnectApi.CdpSegmentDbtModelInput

  • select 语句中只允许使用 SegmentOn DMO 的主键。第一张桌子 在 JOIN 子句中必须是配置文件表。
    • 顶层不允许聚合(最小值、最大值、平均值、计数) 选择。---FAIL select max(Individual_dense_viv__dlm.age__c) from Individual_dense_viv__dlm ---FAIL select count(Individual_dense_viv__dlm.individualid__c) from Individual_dense_viv__dlm ---
    • 顶层不允许全选 (*) 表达式 选择。---FAIL select * from Individual_dense_viv__dlm
    • 只有 table 上段的主键(sql 的 from 子句中的第一个表 语句)被允许选择。---PASS select Individual_dense_viv__dlm.individualid__c from Individual_dense_viv__dlm
    • 无法选择多个列,即使其中一个列是 桌子。---FAIL select Individual_dense_viv__dlm.individualid__c, Individual_dense_viv__dlm.age__c from Individual_dense_viv__dlm
    • 主数据中不允许有大小写语句 选择。---FAIL select case when Individual_dense_viv__dlm.individualid__c > 10 then Individual_dense_viv__dlm.individualid__c else null end from Individual_dense_viv__dlm
    • 如果 segmentOn 实体的主键具有键限定符,则必须投影该键 限定符,以及在初选中。首先投影主键,然后投影 限定 符。分组依据还必须包含密钥 限定 符。---PASS select Individual__dlm.id__c, Individual__dlm.fq__id__c from Individual__dlm
    • 如果 segmentOn 实体的主键具有键限定符,则可以提供 加入条件中的附加条件。---PASS select Individual__dlm.id__c from Individual__dlm left join Sales__dlm on Individual__dlm.id__c = Sales__dlm.soldToCustomerId__c and Individual__dlm.kq__id__c is not distinct from Sales__dlm.kq__soldToCustomerId__c
  • 所有列都必须在查询和子选择中由 tablename 完全限定 查询。---FAIL select individualid__c from Individual_dense_viv__dlm
  • 子查询仅在 where 子句中受支持,并且只能发出一个子查询 列。---FAIL select Individual_dense_viv__dlm.individualid__c from (select * from Individual_dense_viv__dlm)
  • 比较相同数据类型的列。若要比较不同数据类型的列,请强制转换一个 或两个操作数,以便它们具有相同的 类型。---PASS select t.id__c from Individual__dlm as t where cast(t.id__c as varchar(100)) = t1.name
  • 支持限制和偏移。---FAIL select Individual_dense_viv__dlm.individualid__c from Individual_dense_viv__dlm limit 10
  • 除 select 语句以外的任何 sql 语句都不是 允许。---FAIL update Individual_dense_viv__dlm set Individual_dense_viv__dlm.individualid__c = 'aa'
  • 只有查询的 from 块中的 DMO 才支持别名。列不能是 锯齿。---PASS select t.id__c from Individual__dlm as t
  • 要加入两个 DMO,DMO 之间必须存在关系,并且必须使用 它们在 join on 条件中的相关 join 键。join on 条件只能包含一个 连接键之间的相等性比较和用于比较的可选附加条件 FQK 字段。---PASS select Individual__dlm.id__c from Individual__dlm left join Sales__dlm on Individual__dlm.id__c = Sales__dlm.soldToCustomerId__c--PASS Individual__dlm left join Sales__dlm on Individual__dlm.id__c = Sales__dlm.soldToCustomerId__c and Individual__dlm.kq__id__c is not distinct from Sales__dlm.kq__soldToCustomerId__c

使用 ConnectApi 输入和输出类

命名空间中的某些类包含 访问 Connect REST API 数据的静态方法。命名空间还包含要传递的输入类 调用静态方法返回的参数和输出类。

ConnectApiConnectApi

ConnectApi方法采取 简单类型或复杂类型。简单类型是原始的 Apex 数据,如整数和字符串。复杂类型是输入对象。ConnectApi

成功执行方法可以 从命名空间返回输出对象。 输出对象可以由其他输出组成 对象。例如,ConnectApi.ActorWithId 输出对象 包含属性,例如 和 ,这些属性包含基元 数据类型。它还包含一个属性,该属性包含一个对象。ConnectApiConnectApiConnectApiidurlmySubscriptionConnectApi.Reference

注意

输出对象中的所有 Salesforce ID 均为 18 个字符 ID。输入对象可以使用 15 个字符的 ID 或 18 个字符的 ID。ConnectApi

了解 ConnectApi 类的限制

命名空间中方法的限制与其他 Apex 类的限制不同。

ConnectApi

对于命名空间中的类, 每个写入操作都会根据 Apex 调控器限制花费一个 DML 语句。 方法调用也是主题 到速率限制。 速率限制 匹配 Connect REST API 速率限制。两者都具有每个用户、每个命名空间、每小时 速率限制。当超过速率限制时,将引发 a。您的 Apex 代码必须捕获和 处理此异常。ConnectApiConnectApiConnectApiConnectApi.RateLimitException

测试代码时,将开始调用 Apex 方法 新的速率限制计数。调用该方法会将速率限制计数设置为该值 那是在你打电话之前.Test.startTestTest.stopTestTest.startTest

打包 ConnectApi 类

如果将类包含在 软件包中,请注意 Chatter 依赖关系。

ConnectApi如果一个类依赖于 Chatter,代码可以编译并安装在未启用 Chatter 的组织中。 但是,如果未启用 Chatter,则代码会在运行时抛出错误 时间。

ConnectApi

System.NoAccessException: Insufficient Privileges: This feature is not currently enabled for this user.

在其参考文档中,每个方法都指示它是否支持 Chatter。ConnectApi

序列化和反序列化 ConnectApi 对象

序列化输出对象时 转换为 JSON,其结构类似于从 Connect REST API 返回的 JSON。当输入对象从 JSON 反序列化时, 格式也类似于 Connect REST API。

ConnectApiConnectApi

Apex 中的 Connect 支持这些 Apex 上下文中的序列化和反序列化。

  • JSON和类 – 将 Apex 输出中的 Connect 序列化为 JSON 并从 JSON 反序列化 Apex 输入中的 Connect。JSONParser
  • Apex REST,将 Apex 输出中的 Connect 序列化为 JSON 作为返回值 值并将 JSON 中的 Apex 输入中的 Connect 反序列化为参数。@RestResource
  • JavaScript Remoting with —serialize 将 Apex 输出中的 Connect 作为返回值转换为 JSON 值并将 JSON 中的 Apex 输入中的 Connect 反序列化为参数。@RemoteAction

Apex 中的 Connect 遵循这些序列化和反序列化规则。

  • 只能序列化输出对象。
  • 只能对顶级输入对象进行反序列化。
  • 枚举值和异常不能序列化或反序列化。

连接Api版本控制和相等性 检查

类中的版本控制如下 与其他 Apex 类的规则不同的特定规则。

ConnectApi

类的版本控制遵循以下命令 规则。ConnectApi

  • 方法调用在 包含方法调用的类版本的上下文。用途 版本类似于 Connect 的部分 REST API URL。ConnectApi/vXX.X
  • 每个输出对象都公开一个方法。此方法 返回创建输出对象的方法所使用的版本 调用。ConnectApigetBuildVersion
  • 与输入对象交互时,Apex 只能访问 封闭的 Apex 类的版本。
  • 传递给方法的输入对象 可能仅包含 Apex 版本支持的非 null 属性 执行方法的类。如果输入对象包含版本不合适的内容 属性,则抛出异常。ConnectApi
  • 仅方法的输出 返回与交互的代码版本中支持的属性 对象。对于输出对象,返回的属性也必须在 内部版本。toString
  • Apex REST、 和序列化仅包括 适合版本的属性。JSON.serialize@RemoteAction
  • Apex REST、和反序列化拒绝属性 版本不合适。JSON.deserialize@RemoteAction
  • 枚举不受版本控制。枚举值在所有 API 版本中都返回。客户 应该优雅地处理他们不理解的值。

以下为类的相等性检查 这些规则。ConnectApi

  • 输入对象 – 比较属性。
  • 输出对象 – 比较属性和生成版本。例如,如果 两个对象具有相同的属性和相同的值,但具有不同的构建 版本,则对象不相等。要获取内部版本,请调用 。getBuildVersion

强制转换 ConnectApi 对象

将某些输出对象向下转换为更多可能很有用 特定类型。

ConnectApi

此技术对于消息段、源项功能和记录特别有用 领域。源项中的消息段是 键入为 。饲料 项目功能类型为 。记录字段的类型为 。这些类 都是抽象的,并且有几个具体的子类。在运行时,您可以使用它来检查具体类型 然后安全地进行相应的下沉。当你 向下,您必须具有处理未知子类的默认大小写。ConnectApi.MessageSegmentConnectApi.FeedItemCapabilityConnectApi.AbstractRecordFieldinstanceof以下示例将 a 向下转换为 :

ConnectApi.MessageSegmentConnectApi.MentionSegment

if(segment instanceof ConnectApi.MentionSegment) {
	ConnectApi.MentionSegment = (ConnectApi.MentionSegment)segment;
}

重要

饲料的成分可以在 释放。编写代码以处理未知子类的实例。

通配符

使用通配符匹配 Connect REST API 和 Connect in Apex 中的文本模式 搜索。通配符的常见用途是搜索源。在参数中传递搜索字符串和通配符。此示例是 Connect REST API 请求:

q

/chatter/feed-elements?q=chat*

此示例是 Connect in Apex 方法 叫:

ConnectApi.ChatterFeeds.searchFeedElements(null, 'chat*');

您可以指定以下通配符来匹配搜索中的文本模式:

通配符描述
*星号与搜索词中间或末尾的零个或多个字符匹配。例如,搜索 john* 会查找以 john 开头的项目, 例如,JohnJohnson 或 Johnny。搜索 mi* meyers 会找到带有 mike meyers 或 michael meyers 的项目。如果要在单词或短语中搜索字面上的星号,请转义星号(在星号前面加上字符)。\
?问号仅匹配搜索词中间或末尾的一个字符。例如,搜索 jo?n 会查找术语为 john 或 joan 的项目,但不会查找 jon 或 johan 的项目。您不能使用 ?在查找搜索中。

使用通配符时,请考虑以下注意事项:

  • 通配符搜索越集中,搜索结果返回的速度就越快,结果就越有可能反映您的意图。例如,要搜索所有 出现单词(或复数形式)时,在搜索字符串中指定比指定限制性较小的通配符搜索(例如 )更有效,后者可以 返回无关的匹配项(如 )。prospectprospectsprospect*prosp*prosperity
  • 定制搜索以查找单词的所有变体。例如,要查找 和 ,则 将指定 .propertypropertiespropert*
  • 标点符号已编入索引。若要查找短语或在短语中查找,必须将搜索字符串括在引号中 你必须逃脱特殊字符。例如,找到短语 。 转义字符 () 是必需的,以便此搜索正常工作。*?“where are you\?”where are you?\

测试 ConnectApi 代码

与所有 Apex 代码一样,Connect in Apex 代码需要测试覆盖率。

Connect in Apex 方法不会在系统模式下运行,而是在 当前用户(也称为上下文用户)的上下文。方法 有权访问上下文用户有权访问的任何内容。Apex 中的 Connect 不会 支持系统方法。runAs

大多数 Connect in Apex 方法都需要访问真实的组织数据,除非在 标记为 的测试方法。@IsTest(SeeAllData=true)

但是,不允许某些 Connect in Apex 方法(例如 )访问 tests,并且必须与特殊的测试方法一起使用,这些方法注册要返回的输出 测试上下文。如果某个方法需要某个方法,则该要求在该方法的“用法”部分中说明。getFeedElementsFromFeedsetTest

测试方法名称是带有前缀的常规方法名称。测试方法签名(参数组合) 匹配常规方法的签名。例如,如果常规方法有三个 过载,测试方法有三个过载。setTest

在 Apex 测试方法中使用 Connect 类似于在 Apex 中测试 Web 服务。首先,构建 您希望方法返回的数据。要构建数据,请创建输出对象并设置其 性能。若要创建对象,可以对任何非抽象对象使用无参数构造函数 输出类。

生成数据后,调用测试方法注册数据。调用测试方法 与正在测试的常规方法具有相同的签名。

注册测试数据后,运行常规方法。当您运行常规方法时, 返回已注册的数据。

重要

使用与常规方法签名匹配的测试方法签名。如果 调用常规时,数据未注册到匹配的参数集 方法,则会收到异常。此示例显示了一个测试,该测试构造并注册它,以便在使用特定 参数组合。

ConnectApi.FeedElementPagegetFeedElementsFromFeed

global class NewsFeedClass {
    global static Integer getNewsFeedCount() {
        ConnectApi.FeedElementPage elements = 
            ConnectApi.ChatterFeeds.getFeedElementsFromFeed(null,
                ConnectApi.FeedType.News, 'me');
        return elements.elements.size();
    }
}
@isTest
private class NewsFeedClassTest {
    @IsTest
    static void doTest() {
        // Build a simple feed item
        ConnectApi.FeedElementPage testPage = new ConnectApi.FeedElementPage();
        List<ConnectApi.FeedItem> testItemList = new List<ConnectApi.FeedItem>();
        testItemList.add(new ConnectApi.FeedItem());
        testItemList.add(new ConnectApi.FeedItem());
        testPage.elements = testItemList;

        // Set the test data
        ConnectApi.ChatterFeeds.setTestGetFeedElementsFromFeed(null,
            ConnectApi.FeedType.News, 'me', testPage);

        // The method returns the test page, which we know has two items in it.
        Test.startTest();
        System.assertEquals(2, NewsFeedClass.getNewsFeedCount());
        Test.stopTest();
    }
}

ConnectApi 类与其他 Apex 之间的差异 类

请注意类和其他 Apex 类之间的这些附加差异。

ConnectApi系统模式和上下文用户Connect in Apex 方法不会在系统模式下运行,而是在 当前用户(也称为上下文用户)的上下文。方法 有权访问上下文用户有权访问的任何内容。Apex 中的 Connect 不会 支持系统方法。runAs当 方法采用subjectId论点 通常,该主题必须是上下文用户。在这些情况下,您可以使用字符串来指定上下文用户,而不是 同上。me默认情况下,Apex 中的 Connect 对 Automated Process 用户不可用。在 Apex 中连接 可供以下用户使用:

  • 仅限 Chatter 的用户
  • 来宾用户
  • 门户用户
  • 标准用户

with sharingwithout sharingApex 中的 Connect 会忽略 和 关键字。相反,上下文用户控制所有安全性、字段级共享和 能见度。例如,如果上下文用户是专用组的成员,则类可以发布到该组。如果 上下文用户不是专用组的成员,代码无法查看 该群组,但无法发布到该群组。with sharingwithout sharingConnectApi异步操作Apex 中的某些 Connect 操作是异步的,也就是说,它们不是 立即发生。例如,如果代码为用户添加了源项,则不会 立即在新闻提要中可用。另一个例子:当你添加一张照片时,它是 不能立即使用。对于测试,如果您添加照片,则无法检索 它立即。Apex 中不支持 XML 休息Apex REST 不支持 XML 序列化和反序列化 在 Apex 对象中连接。Apex REST支持JSON序列化和反序列化 在 Apex 对象中连接。空日志条目有关 Apex 对象中 Connect 的信息不会显示在日志事件中。VARIABLE_ASSIGNMENT没有 Apex SOAP Web 服务 支持Apex 对象中的 Connect 不能在 Apex SOAP Web 服务中使用 用关键字 表示。webservice