datai/datai-scenes/datai-scene-salesforce/docs/decisions/2026-01-30-004-ADR-查询功能技术选型.md

6.1 KiB
Raw Permalink Blame History

ADR-004: 查询功能技术选型

状态

已接受

日期

2026-01-30

背景

查询功能是 Salesforce Partner API 的核心功能之一,需要支持 SOQL 查询、QueryAll查询所有记录包括已删除、QueryMore分页查询和 SOSL 搜索。在设计查询功能时,需要做出以下关键决策:

  1. 查询构建方式:如何构建 SOQL 查询语句
  2. 结果处理方式:如何处理和返回查询结果
  3. 分页策略:如何处理大量数据的分页查询
  4. 嵌套对象处理:如何处理关系查询返回的嵌套对象

决策

决策 1使用 SoqlBuilder 构建 SOQL 查询

选择方案:使用 datai-salesforce-common 模块中的 SoqlBuilder 工具类构建类型安全的 SOQL 查询

理由

  1. 类型安全SoqlBuilder 提供类型安全的方法链,避免字符串拼接错误
  2. 可读性高:链式调用语法清晰,易于理解和维护
  3. 已存在:项目中已有 SoqlBuilder 实现,无需重复开发
  4. 功能完善:支持 SELECT、WHERE、ORDER BY、LIMIT、OFFSET 等完整语法
  5. 与现有代码一致CRUD 操作和批量操作已使用 SoqlBuilder保持代码风格统一

放弃方案:原生字符串拼接

  • 理由:容易出错、难以维护、缺乏类型检查

决策 2完整转换查询结果

选择方案:将 Salesforce 返回的 SObject[] 完整转换为 List<Map<String, Object>>

理由

  1. 解耦:将 Salesforce 特定的 SObject 对象转换为通用的 Map 结构,降低客户端与 Salesforce API 的耦合
  2. 灵活性Map 结构更灵活,易于序列化为 JSON 返回给前端
  3. 一致性:与 CRUD 操作和批量操作的结果格式保持一致
  4. 嵌套支持:支持递归处理嵌套的 SObject关系查询结果

实现细节

private Map<String, Object> convertSObjectToMap(SObject sObject) {
    Map<String, Object> result = new HashMap<>();
    if (sObject == null) {
        return result;
    }
    
    for (java.lang.reflect.Field field : sObject.getClass().getDeclaredFields()) {
        String fieldName = field.getName();
        Object fieldValue = sObject.getField(fieldName);
        
        // 递归处理嵌套对象
        if (fieldValue instanceof SObject) {
            result.put(fieldName, convertSObjectToMap((SObject) fieldValue));
        } else {
            result.put(fieldName, fieldValue);
        }
    }
    
    return result;
}

放弃方案:懒加载/延迟转换

  • 理由:增加复杂性,需要维护 SObject 引用,不适合无状态 REST API

决策 3使用 QueryMore 进行服务端分页

选择方案:使用 Salesforce 提供的 QueryMore 方法进行服务端分页

理由

  1. 性能优化:只获取需要的数据,减少网络传输和内存占用
  2. Salesforce 原生支持QueryMore 是 Salesforce API 的标准分页方式
  3. QueryLocator:使用 QueryLocator 标识查询位置,支持高效分页
  4. 一致性:与 Salesforce 官方推荐的分页方式保持一致

实现细节

  • 首次查询返回 QueryResult,包含 queryLocatordone 标志
  • 如果 done 为 false使用 queryLocator 调用 queryMore 获取下一页
  • 客户端可以选择是否获取所有数据或分页获取

放弃方案:客户端分页

  • 理由:需要一次性获取所有数据,性能差,不适合大数据量场景

决策 4递归处理嵌套对象

选择方案:递归转换嵌套的 SObject 对象

理由

  1. 支持关系查询SOQL 支持关系查询(如 SELECT Name, Account.Name FROM Contact),返回嵌套对象
  2. 数据完整性:保持查询结果的完整结构
  3. 前端友好:嵌套的 Map 结构易于前端处理和展示

注意事项

  • 需要处理循环引用(虽然 Salesforce 查询通常不会有循环引用)
  • 控制递归深度,避免栈溢出

后果

正面影响

  1. 开发效率高:使用 SoqlBuilder 减少 SQL 拼接错误,提高开发效率
  2. 代码可维护性:统一的转换逻辑,代码易于理解和维护
  3. 性能优化:服务端分页减少数据传输,提高响应速度
  4. 数据完整性:支持关系查询,返回完整的数据结构
  5. 与现有代码一致:与 CRUD 操作和批量操作保持一致的代码风格

负面影响

  1. 内存占用:完整转换所有字段可能占用较多内存(特别是大字段如 Long Text Area
  2. 转换开销SObject 到 Map 的转换有一定性能开销
  3. 递归风险:深度嵌套的对象可能导致栈溢出(需要设置递归深度限制)
  4. 字段过滤:无法自动过滤敏感字段(需要在查询时手动指定字段)

替代方案

方案 2使用原生字符串拼接构建 SOQL

  • 优点
    • 灵活性高,可以构建任意复杂的查询
    • 无需依赖 SoqlBuilder
  • 缺点
    • 容易出错,缺乏类型检查
    • 难以维护,查询逻辑分散
    • SQL 注入风险(如果不小心处理参数)
  • 适用场景:需要构建极其复杂的动态查询

方案 3使用 DTO 映射替代 Map

  • 优点
    • 类型安全,编译期检查
    • IDE 支持好,自动补全
    • 可以添加业务方法
  • 缺点
    • 需要为每个对象类型创建 DTO
    • 不灵活,无法处理动态字段
    • 维护成本高
  • 适用场景:对象类型固定且字段明确的场景

方案 4使用 Jackson 序列化 SObject

  • 优点
    • 无需手动转换
    • 代码简洁
  • 缺点
    • SObject 可能包含内部字段,序列化结果不可控
    • 无法处理嵌套对象的特殊逻辑
    • 与现有代码风格不一致
  • 适用场景:快速原型开发

相关文档