Salesforce 外部对象深度解析:架构师视角下的无缝数据集成策略

背景与应用场景

作为一名 Salesforce 架构师 (Salesforce Architect),我们面临的核心挑战之一是如何在 Salesforce 平台与企业内外部的众多系统之间构建高效、可靠且可扩展的数据策略。一个常见的难题是:是否应该将所有外部数据都通过 ETL (Extract, Transform, Load) 过程同步到 Salesforce 中?当面对海量数据、实时性要求极高或受数据主权法规限制的场景时,传统的数据复制模式往往显得力不从心,不仅增加了存储成本,还可能导致数据一致性问题。

为了解决这一挑战,Salesforce 提供了 Salesforce Connect,这是一种强大的集成服务,它允许我们通过外部对象 (External Objects) 以“数据虚拟化”的方式实时访问外部系统的数据,而无需将这些数据实际存储在 Salesforce 中。外部对象在用户界面和 API 中的行为与标准或自定义对象非常相似,用户可以查看、搜索甚至编辑(取决于配置)这些外部数据,仿佛它们就存在于 Salesforce 内部。

典型的应用场景包括:

  • 大型 ERP 系统集成:实时查看来自 SAP 或 Oracle ERP 系统的订单历史、发票信息或库存水平,而无需进行大规模数据同步。
  • 数据仓库访问:在客户的 360 度视图中,直接呈现存储在 Snowflake 或 BigQuery 等数据仓库中的聚合分析数据。
  • 遵守数据驻留法规:对于某些国家或地区(如欧洲的 GDPR),敏感数据必须存储在本地服务器上。通过外部对象,Salesforce 用户可以访问这些数据,而数据本身仍保留在合规的地理位置。
  • 旧系统现代化改造:在逐步淘汰旧系统的过程中,可以利用外部对象将旧系统的数据无缝集成到新的 Salesforce 应用中,实现平滑过渡。

从架构师的角度来看,外部对象提供了一种“引用而非复制 (Reference, Don't Copy)”的集成模式,是构建灵活、轻量级数据架构的关键组件。


原理说明

外部对象的核心工作原理是基于 Salesforce Connect 的适配器 (Adapters)。当用户或系统尝试查询一个外部对象时,Salesforce 并不会查询其内部数据库,而是通过指定的适配器向外部数据源发起一个实时的 API 请求。外部数据源处理该请求后返回数据,Salesforce Connect 再将这些数据转换为 Salesforce 用户界面或 API 能够理解的格式进行呈现。

整个过程对最终用户是透明的。他们可能正在查看一个客户的联系人列表,其中一些联系人是 Salesforce 的标准记录,而另一些则是通过外部对象从另一个 CRM 系统实时拉取的。

Salesforce Connect 主要提供三种类型的适配器:

  1. OData 适配器:OData (Open Data Protocol) 是一个基于 HTTP 的开放标准,用于构建和使用 RESTful API。如果外部系统支持 OData 2.0 或 4.0 端点,我们可以通过简单的点击配置,快速将外部数据表映射为 Salesforce 外部对象。这是最常用且标准化的方式。
  2. 跨组织 (Cross-Org) 适配器:这种适配器专门用于连接两个不同的 Salesforce 组织。它利用 Salesforce API,可以轻松地将一个组织中的标准或自定义对象数据在另一个组织中以外部对象的形式呈现。
  3. Apex 自定义适配器 (Apex Custom Adapter):当外部系统不提供 OData 接口,或者我们需要处理复杂的认证、数据转换或查询逻辑时,Apex 自定义适配器提供了终极的灵活性。我们可以编写自己的 Apex 代码来实现 DataSource.ProviderDataSource.Connection 接口,从而连接到几乎任何支持 API 的数据源(如 REST、SOAP、GraphQL 等)。

对于架构师而言,选择哪种适配器取决于外部系统的能力、集成复杂度和开发资源。我们的目标是尽可能使用标准适配器,仅在必要时才诉诸自定义开发。


示例代码

对于需要最大灵活性的场景,Apex 自定义适配器是我们的首选。以下是一个来自 Salesforce 官方文档的示例,它实现了一个连接到外部文件存储系统的自定义适配器。这个适配器可以将外部系统的文件元数据(如文件名、大小等)作为外部对象记录进行查询。

该示例包含两个核心类:Provider 类用于定义适配器的能力和获取连接实例,Connection 类则负责处理实际的数据操作,如查询和搜索。

1. DataSource.Provider 实现

这个类声明了数据源支持哪些能力(例如,查询、搜索),并提供了获取连接实例的方法。

/*
 * 此 Provider 类用于为名为 "File" 的外部数据源定义适配器。
 */
global class FileDataSourceProvider extends DataSource.Provider {
    // 构造函数:初始化时可以设置一些默认值或进行检查
    global FileDataSourceProvider() {
    }

    // getCapabilities() 方法声明了此适配器支持的功能。
    // 在本例中,我们声明支持 QUERY (查询) 和 SEARCH (搜索)。
    // 其他可选能力包括 ROW_QUERY (按行查询) 和 REQUIRES_ENDPOINT (需要端点)。
    override global List<DataSource.Capability> getCapabilities() {
        List<DataSource.Capability> capabilities =
            new List<DataSource.Capability>();
        capabilities.add(DataSource.Capability.QUERY);
        capabilities.add(DataSource.Capability.SEARCH);
        return capabilities;
    }

    // getConnection() 方法是 Provider 的核心。
    // 当 Salesforce 需要与外部系统交互时,它会调用此方法来获取一个 Connection 实例。
    // Connection 实例包含了处理数据交互的实际逻辑。
    // context 参数包含了关于当前请求的上下文信息。
    override global DataSource.Connection getConnection(DataSource.ConnectionParams connectionParams) {
        return new FileDataSourceConnection(connectionParams);
    }
}

2. DataSource.Connection 实现

这个类是数据交互的执行者。它实现了查询 (query) 和搜索 (search) 的具体逻辑。

/*
 * 此 Connection 类处理与外部文件系统的实际数据交互。
 */
global class FileDataSourceConnection extends DataSource.Connection {
    private DataSource.ConnectionParams connectionParams;

    // 构造函数:保存从 Provider 传递过来的连接参数
    global FileDataSourceConnection(DataSource.ConnectionParams connectionParams) {
        this.connectionParams = connectionParams;
    }

    // sync() 方法用于元数据同步。
    // 当管理员点击“验证并同步”时,Salesforce 会调用此方法。
    // 它应该返回一个 DataSource.Table 列表,定义了外部系统中有哪些“表”可以映射为外部对象。
    // 每个表定义了其名称、标签和列(即字段)。
    override global List<DataSource.Table> sync() {
        List<DataSource.Table> tables = new List<DataSource.Table>();
        List<DataSource.Column> columns = new List<DataSource.Column>();
        columns.add(DataSource.Column.text('fileName', 255)); // 文件名
        columns.add(DataSource.Column.text('fileExtension', 20)); // 文件扩展名
        columns.add(DataSource.Column.number('fileSize', 18, 0)); // 文件大小
        columns.add(DataSource.Column.url('downloadUrl')); // 下载链接
        columns.add(DataSource.Column.text('DisplayUrl')); // Salesforce 要求的标准外部对象字段
        columns.add(DataSource.Column.text('ExternalId')); // Salesforce 要求的标准外部对象字段

        // 创建一个名为 "files" 的表,并将其添加到列表中
        tables.add(DataSource.Table.builder().name('files').label('Files').columns(columns).build());
        return tables;
    }

    // query() 方法是处理 SOQL 查询的核心。
    // context 参数包含了查询的详细信息,如查询的表、筛选条件、排序规则等。
    override global DataSource.Results query(DataSource.QueryContext context) {
        // 在实际项目中,这里会调用外部系统的 API 来获取数据。
        // 为简化示例,我们在这里虚构一些数据。
        List<Map<String, Object>> sampleData = getSampleData();
        List<Map<String, Object>> response = new List<Map<String, Object>>();
        
        for (Map<String, Object> row : sampleData) {
            // 这里可以添加逻辑来处理 context 中的筛选和排序条件。
            // 例如:if (meetsFilterCriteria(row, context.filters)) { ... }
            response.add(row);
        }

        // 使用 DataSource.ResultsBuilder 来构建并返回结果。
        return DataSource.Results.builder().rows(response).build();
    }

    // search() 方法处理 SOSL 搜索请求。
    override global DataSource.SearchResponse search(DataSource.SearchContext context) {
        // 在实际项目中,这里会调用外部系统的搜索 API。
        // 为简化示例,我们返回一个固定的结果。
        List<List<Map<String, Object>>> searchResults = new List<List<Map<String, Object>>>();
        searchResults.add(getSampleData());
        
        return DataSource.SearchResponse.builder().searchResult(searchResults).build();
    }
    
    // 辅助方法,用于生成示例数据
    private List<Map<String, Object>> getSampleData() {
        List<Map<String, Object>> data = new List<Map<String, Object>>();
        Map<String, Object> row1 = new Map<String, Object>();
        row1.put('ExternalId', 'doc101');
        row1.put('DisplayUrl', '/services/data/v58.0/sobjects/MyExternalObject__x/doc101');
        row1.put('fileName', 'Architecture-Diagram');
        row1.put('fileExtension', 'pdf');
        row1.put('fileSize', 2048);
        row1.put('downloadUrl', 'https://example.com/files/doc101');
        data.add(row1);
        return data;
    }
}

注意事项

虽然外部对象功能强大,但在架构设计时必须充分考虑其局限性,以避免在项目后期遇到无法逾越的障碍。

1. 性能与延迟 (Performance & Latency)

每一次对外部对象的访问(查看记录、运行报表、执行 SOQL)都会触发一个实时的 API Callout。这意味着性能直接受制于外部系统的响应时间和网络延迟。如果外部系统响应缓慢,Salesforce 的用户界面也会相应地出现卡顿。设计页面布局时应尽量减少外部对象字段和相关列表的显示,避免一次性加载过多数据。

2. 数据与功能限制 (Data & Feature Limitations)

外部对象并不支持所有的 Salesforce 标准功能。以下是一些关键限制:

  • 不支持大多数关系类型:外部对象不能位于主从关系 (Master-Detail Relationship) 的“明细”端。
  • 有限的报表和分析功能:虽然外部对象可以用于报表,但存在诸多限制,例如不能作为“运行用户”筛选标准,且性能可能不佳。联合报表是整合内部和外部数据的一种方式。
  • 不支持公式字段:不能在外部对象本身上创建公式字段。如果需要计算字段,必须在外部数据源端实现或通过 Apex/Flow 等方式间接处理。
  • 无触发器和验证规则:不能在外部对象上定义 Apex 触发器、验证规则或工作流规则。所有业务逻辑和数据校验必须在外部系统或通过客户端逻辑实现。
  • 搜索行为:外部对象的搜索行为依赖于适配器的实现。对于 OData,搜索能力取决于外部系统的配置。对于自定义适配器,我们需要在 `search()` 方法中自行实现搜索逻辑。全局搜索 (SOSL) 的支持也有限。

3. API 调用与治理 (API Callouts & Governance)

Salesforce Connect 的每次调用都会消耗 Salesforce 的 API Callout 限制。一个复杂的页面或报表可能会触发多次调用。必须仔细规划和监控 API 使用情况,确保不会超出组织的每日限制。同时,也要考虑外部数据源的 API 速率限制,避免因请求过多而被阻止。

4. 安全与权限 (Security & Permissions)

外部对象的访问权限由 Salesforce 简档 (Profile)权限集 (Permission Set) 控制。但是,记录级别的可见性通常由外部系统决定。我们通过命名凭据 (Named Credentials) 来管理对外部系统的身份验证,它将认证信息与代码和配置解耦,是管理外部连接安全性的最佳实践。

5. 写入操作与数据一致性 (Write Operations & Data Consistency)

并非所有外部对象都是可写的。可写性取决于外部数据源是否通过 API 暴露了创建、更新和删除操作,以及 Salesforce Connect 适配器是否支持这些操作。启用写入功能时,必须仔细考虑事务边界和数据一致性。由于操作跨越两个系统,无法实现像 Salesforce 内部那样的原子性事务。必须设计好补偿逻辑和错误处理机制,以应对部分操作失败的情况。


总结与最佳实践

作为 Salesforce 架构师,外部对象是我们工具箱中一件用于应对复杂数据集成场景的利器。它完美诠释了“数据引力”法则——将计算和应用逻辑移向数据,而不是反过来移动海量数据。然而,它并非万能药,而是需要在特定场景下审慎使用的架构模式。

以下是几条关键的最佳实践:

  1. 明确数据策略——“引用”还是“复制”?:在设计之初,就要明确每块数据最适合的集成模式。对于需要实时性、数据量巨大或有驻留要求的数据,优先考虑外部对象(引用)。对于需要复杂 Salesforce 业务逻辑(如触发器、复杂报表)且对实时性要求不高的数据,传统的 ETL 同步(复制)可能是更好的选择。
  2. 优先选择标准适配器:如果外部系统支持 OData,应始终优先使用 OData 适配器。它的配置简单、维护成本低,并且由 Salesforce 官方支持。只有在标准适配器无法满足需求时,才考虑投入资源开发 Apex 自定义适配器。
  3. 为性能而设计:始终将性能作为首要考量。与业务方沟通,仅在页面和报表中展示最关键的外部数据字段。利用外部查找关系 (External Lookup) 将外部数据与 Salesforce 内部数据关联起来,但要避免在列表视图中默认显示过多的外部查找字段。
  4. 安全是根本:使用命名凭据 (Named Credentials) 集中管理所有外部连接的身份验证。根据最小权限原则,为不同的简档和权限集分配对外部对象的访问权限。
  5. 理解总体拥有成本 (TCO):Salesforce Connect 是一个付费附加产品。在提出解决方案时,必须将许可证费用、潜在的 Apex 开发成本、以及对外部系统的维护和 API 治理成本一并纳入考量。

总之,通过深入理解外部对象的工作原理、适用场景及其局限性,Salesforce 架构师可以设计出更加优雅、高效且可持续的集成解决方案,从而最大限度地发挥 Salesforce 平台作为企业级应用核心的价值。

评论

此博客中的热门博文

Salesforce Einstein AI 编程实践:开发者视角下的智能预测

Salesforce 登录取证:深入解析用户访问监控与安全

Salesforce Experience Cloud 技术深度解析:构建社区站点 (Community Sites)