datai/datai-scenes/datai-scene-salesforce/docs/decisions/2026-02-02-005-ADR-描述功能技术选型.md

9.5 KiB
Raw Permalink Blame History

ADR-005: 描述功能技术选型

状态

已接受

日期

2026-02-02

背景

描述功能是 Salesforce Partner API 的核心功能之一,用于获取 Salesforce 对象的元数据信息。在设计描述功能时,需要做出以下关键决策:

  1. 描述结果格式:如何处理和返回 Salesforce 的 Describe 结果
  2. 批量描述策略:如何高效地描述多个对象
  3. 元数据缓存:是否需要缓存对象元数据以提高性能
  4. 字段信息处理如何处理复杂的字段定义Picklist、关系字段等

决策

决策 1使用 Map<String, Object> 格式转换描述结果

选择方案:将 Salesforce 返回的 DescribeSObjectResult、DescribeGlobalResult 等复杂对象转换为 Map<String, Object> 格式

理由

  1. 解耦:将 Salesforce 特定的描述结果对象转换为通用的 Map 结构,降低客户端与 Salesforce API 的耦合
  2. 灵活性Map 结构更灵活,易于序列化为 JSON 返回给前端
  3. 一致性:与 CRUD 操作、批量操作和查询功能的结果格式保持一致
  4. 可读性:过滤掉内部实现细节,只保留有用的元数据信息
  5. 易于扩展Map 结构便于添加自定义字段或处理特殊场景

实现细节

private Map<String, Object> convertDescribeResult(DescribeSObjectResult result) {
    Map<String, Object> sObjectMap = new HashMap<>();
    sObjectMap.put("name", result.getName());
    sObjectMap.put("label", result.getLabel());
    sObjectMap.put("keyPrefix", result.getKeyPrefix());
    sObjectMap.put("labelPlural", result.getLabelPlural());
    sObjectMap.put("createable", result.isCreateable());
    sObjectMap.put("updateable", result.isUpdateable());
    sObjectMap.put("deletable", result.isDeletable());
    sObjectMap.put("queryable", result.isQueryable());
    
    // 转换字段列表
    List<Map<String, Object>> fields = new ArrayList<>();
    for (Field field : result.getFields()) {
        fields.add(convertFieldToMap(field));
    }
    sObjectMap.put("fields", fields);
    
    return sObjectMap;
}

放弃方案:直接返回原始 DescribeSObjectResult 对象

  • 理由:客户端需要了解 Salesforce API 的内部结构,耦合度高,不利于维护和扩展

决策 2使用 describeSObjects(String[]) 批量描述多个对象

选择方案:使用 PartnerConnection.describeSObjects(String[] sObjectTypes) 方法批量描述多个对象

理由

  1. 性能优化:减少 API 调用次数,一次请求可以描述最多 100 个对象
  2. Salesforce 原生支持Partner API 原生支持批量描述功能
  3. 一致性:与批量操作的设计理念一致
  4. 错误处理:支持部分失败场景,可以识别哪些对象描述失败

实现细节

public DescribeSObjectResultVo[] describeSObjects(DescribeSObjectsDto dto) {
    try {
        PartnerConnection connection = partnerConnectionFactory.getConnection("source");
        String[] objectTypes = dto.getObjectTypes().toArray(new String[0]);
        DescribeSObjectResult[] results = connection.describeSObjects(objectTypes);
        
        // 转换结果
        List<Map<String, Object>> sObjects = new ArrayList<>();
        for (DescribeSObjectResult result : results) {
            sObjects.add(convertDescribeResult(result));
        }
        
        return DescribeSObjectResultVo.builder()
            .sObjects(sObjects)
            .success(true)
            .build();
    } catch (ConnectionException e) {
        // 异常处理
    }
}

放弃方案:循环调用 describeSObject 逐个描述对象

  • 理由API 调用次数多,性能差,不符合批量操作的最佳实践

决策 3使用可选的元数据缓存策略

选择方案:使用可选的元数据缓存策略,缓存时间 1 小时,提供缓存刷新机制

理由

  1. 性能优化:对象元数据不经常变化,缓存可以减少 API 调用次数
  2. 响应速度:缓存可以显著提高响应速度,改善用户体验
  3. 降低 Salesforce API 负载:减少对 Salesforce 服务器的压力
  4. 灵活性:缓存是可选的,可以根据实际需求启用或禁用
  5. 一致性:缓存时间统一为 1 小时,便于管理

实现细节

@Component
public class MetadataCache {
    private final Map<String, CacheEntry<DescribeSObjectResult>> cache = new ConcurrentHashMap<>();
    private static final long CACHE_DURATION_MS = 60 * 60 * 1000; // 1 小时
    
    public DescribeSObjectResult get(String key) {
        CacheEntry<DescribeSObjectResult> entry = cache.get(key);
        if (entry != null && !entry.isExpired()) {
            return entry.getValue();
        }
        return null;
    }
    
    public void put(String key, DescribeSObjectResult value) {
        cache.put(key, new CacheEntry<>(value, System.currentTimeMillis() + CACHE_DURATION_MS));
    }
    
    public void invalidate(String key) {
        cache.remove(key);
    }
    
    public void invalidateAll() {
        cache.clear();
    }
}

放弃方案:不缓存,每次都调用 Salesforce API

  • 理由:虽然简单,但性能差,响应慢,增加 Salesforce 服务器负载

放弃方案:永久缓存

  • 理由:元数据虽然变化不频繁,但可能会变化(如管理员添加新字段),永久缓存会导致数据不一致

决策 4完整转换字段定义信息

选择方案:完整转换字段定义信息,包括名称、类型、标签、是否可创建/更新、Picklist 选项、关系字段引用等

理由

  1. 完整性:提供完整的字段元数据,支持前端动态表单生成
  2. 灵活性支持各种字段类型的特殊处理Picklist、Lookup、Master-Detail 等)
  3. 可用性:前端可以根据字段元数据自动生成表单、验证规则等
  4. 一致性:统一的字段信息格式,便于客户端处理

实现细节

private Map<String, Object> convertFieldToMap(Field field) {
    Map<String, Object> fieldMap = new HashMap<>();
    fieldMap.put("name", field.getName());
    fieldMap.put("type", field.getType().toString());
    fieldMap.put("label", field.getLabel());
    fieldMap.put("createable", field.isCreateable());
    fieldMap.put("updateable", field.isUpdateable());
    fieldMap.put("nillable", field.isNillable());
    fieldMap.put("defaultedOnCreate", field.isDefaultedOnCreate());
    fieldMap.put("unique", field.isUnique());
    fieldMap.put("custom", field.isCustom());
    
    // Picklist 字段处理
    if (field.getType() == FieldType.picklist || field.getType() == FieldType.multipicklist) {
        List<Map<String, Object>> picklistValues = new ArrayList<>();
        for (PicklistEntry entry : field.getPicklistValues()) {
            Map<String, Object> valueMap = new HashMap<>();
            valueMap.put("value", entry.getValue());
            valueMap.put("label", entry.getLabel());
            valueMap.put("active", entry.isActive());
            valueMap.put("defaultValue", entry.isDefaultValue());
            picklistValues.add(valueMap);
        }
        fieldMap.put("picklistValues", picklistValues);
    }
    
    // 关系字段处理
    if (field.getType() == FieldType.reference) {
        fieldMap.put("referenceTo", field.getReferenceTo());
        fieldMap.put("relationshipName", field.getRelationshipName());
    }
    
    return fieldMap;
}

放弃方案:只返回基本字段信息

  • 理由:信息不完整,前端无法充分利用元数据进行动态表单生成和验证

后果

正面影响

  1. 解耦性提升:使用 Map 格式转换描述结果,降低了客户端与 Salesforce API 的耦合
  2. 性能优化:批量描述和缓存策略显著提高了性能
  3. 响应速度提升:缓存机制减少了 API 调用,提高了响应速度
  4. 用户体验改善:完整的字段信息支持前端动态表单生成
  5. 代码一致性与现有模块CRUD、批量操作、查询保持一致的编码风格
  6. 可维护性:统一的转换逻辑便于维护和扩展

负面影响

  1. 内存占用:缓存机制会增加内存占用
  2. 复杂性增加:转换逻辑增加了代码复杂度
  3. 缓存一致性:需要处理缓存刷新和失效逻辑
  4. 开发成本:完整的字段转换需要更多开发工作

替代方案

替代方案 1使用 DTO 映射替代 Map

  • 优点类型安全IDE 支持好,编译时检查
  • 缺点:需要为每种描述结果创建 DTO 类,灵活性差,与现有代码风格不一致
  • 适用场景:对类型安全要求极高的场景

替代方案 2使用 Jackson 序列化原始对象

  • 优点:实现简单,无需手动转换
  • 缺点可能包含不必要的内部字段JSON 结构复杂,客户端处理困难
  • 适用场景:快速原型开发,对 JSON 结构要求不高的场景

替代方案 3使用 Redis 作为分布式缓存

  • 优点:支持分布式部署,缓存容量大,支持持久化
  • 缺点:引入额外依赖,增加系统复杂度,需要配置 Redis 服务器
  • 适用场景:分布式部署,缓存需求大的场景

相关文档