6.1 KiB
6.1 KiB
ADR-004: 查询功能技术选型
状态
已接受
日期
2026-01-30
背景
查询功能是 Salesforce Partner API 的核心功能之一,需要支持 SOQL 查询、QueryAll(查询所有记录包括已删除)、QueryMore(分页查询)和 SOSL 搜索。在设计查询功能时,需要做出以下关键决策:
- 查询构建方式:如何构建 SOQL 查询语句
- 结果处理方式:如何处理和返回查询结果
- 分页策略:如何处理大量数据的分页查询
- 嵌套对象处理:如何处理关系查询返回的嵌套对象
决策
决策 1:使用 SoqlBuilder 构建 SOQL 查询
选择方案:使用 datai-salesforce-common 模块中的 SoqlBuilder 工具类构建类型安全的 SOQL 查询
理由:
- 类型安全:SoqlBuilder 提供类型安全的方法链,避免字符串拼接错误
- 可读性高:链式调用语法清晰,易于理解和维护
- 已存在:项目中已有 SoqlBuilder 实现,无需重复开发
- 功能完善:支持 SELECT、WHERE、ORDER BY、LIMIT、OFFSET 等完整语法
- 与现有代码一致:CRUD 操作和批量操作已使用 SoqlBuilder,保持代码风格统一
放弃方案:原生字符串拼接
- 理由:容易出错、难以维护、缺乏类型检查
决策 2:完整转换查询结果
选择方案:将 Salesforce 返回的 SObject[] 完整转换为 List<Map<String, Object>>
理由:
- 解耦:将 Salesforce 特定的 SObject 对象转换为通用的 Map 结构,降低客户端与 Salesforce API 的耦合
- 灵活性:Map 结构更灵活,易于序列化为 JSON 返回给前端
- 一致性:与 CRUD 操作和批量操作的结果格式保持一致
- 嵌套支持:支持递归处理嵌套的 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 方法进行服务端分页
理由:
- 性能优化:只获取需要的数据,减少网络传输和内存占用
- Salesforce 原生支持:QueryMore 是 Salesforce API 的标准分页方式
- QueryLocator:使用 QueryLocator 标识查询位置,支持高效分页
- 一致性:与 Salesforce 官方推荐的分页方式保持一致
实现细节:
- 首次查询返回
QueryResult,包含queryLocator和done标志 - 如果
done为 false,使用queryLocator调用queryMore获取下一页 - 客户端可以选择是否获取所有数据或分页获取
放弃方案:客户端分页
- 理由:需要一次性获取所有数据,性能差,不适合大数据量场景
决策 4:递归处理嵌套对象
选择方案:递归转换嵌套的 SObject 对象
理由:
- 支持关系查询:SOQL 支持关系查询(如
SELECT Name, Account.Name FROM Contact),返回嵌套对象 - 数据完整性:保持查询结果的完整结构
- 前端友好:嵌套的 Map 结构易于前端处理和展示
注意事项:
- 需要处理循环引用(虽然 Salesforce 查询通常不会有循环引用)
- 控制递归深度,避免栈溢出
后果
正面影响
- 开发效率高:使用 SoqlBuilder 减少 SQL 拼接错误,提高开发效率
- 代码可维护性:统一的转换逻辑,代码易于理解和维护
- 性能优化:服务端分页减少数据传输,提高响应速度
- 数据完整性:支持关系查询,返回完整的数据结构
- 与现有代码一致:与 CRUD 操作和批量操作保持一致的代码风格
负面影响
- 内存占用:完整转换所有字段可能占用较多内存(特别是大字段如 Long Text Area)
- 转换开销:SObject 到 Map 的转换有一定性能开销
- 递归风险:深度嵌套的对象可能导致栈溢出(需要设置递归深度限制)
- 字段过滤:无法自动过滤敏感字段(需要在查询时手动指定字段)
替代方案
方案 2:使用原生字符串拼接构建 SOQL
- 优点:
- 灵活性高,可以构建任意复杂的查询
- 无需依赖 SoqlBuilder
- 缺点:
- 容易出错,缺乏类型检查
- 难以维护,查询逻辑分散
- SQL 注入风险(如果不小心处理参数)
- 适用场景:需要构建极其复杂的动态查询
方案 3:使用 DTO 映射替代 Map
- 优点:
- 类型安全,编译期检查
- IDE 支持好,自动补全
- 可以添加业务方法
- 缺点:
- 需要为每个对象类型创建 DTO
- 不灵活,无法处理动态字段
- 维护成本高
- 适用场景:对象类型固定且字段明确的场景
方案 4:使用 Jackson 序列化 SObject
- 优点:
- 无需手动转换
- 代码简洁
- 缺点:
- SObject 可能包含内部字段,序列化结果不可控
- 无法处理嵌套对象的特殊逻辑
- 与现有代码风格不一致
- 适用场景:快速原型开发