9.5 KiB
9.5 KiB
ADR-005: 描述功能技术选型
状态
已接受
日期
2026-02-02
背景
描述功能是 Salesforce Partner API 的核心功能之一,用于获取 Salesforce 对象的元数据信息。在设计描述功能时,需要做出以下关键决策:
- 描述结果格式:如何处理和返回 Salesforce 的 Describe 结果
- 批量描述策略:如何高效地描述多个对象
- 元数据缓存:是否需要缓存对象元数据以提高性能
- 字段信息处理:如何处理复杂的字段定义(Picklist、关系字段等)
决策
决策 1:使用 Map<String, Object> 格式转换描述结果
选择方案:将 Salesforce 返回的 DescribeSObjectResult、DescribeGlobalResult 等复杂对象转换为 Map<String, Object> 格式
理由:
- 解耦:将 Salesforce 特定的描述结果对象转换为通用的 Map 结构,降低客户端与 Salesforce API 的耦合
- 灵活性:Map 结构更灵活,易于序列化为 JSON 返回给前端
- 一致性:与 CRUD 操作、批量操作和查询功能的结果格式保持一致
- 可读性:过滤掉内部实现细节,只保留有用的元数据信息
- 易于扩展: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) 方法批量描述多个对象
理由:
- 性能优化:减少 API 调用次数,一次请求可以描述最多 100 个对象
- Salesforce 原生支持:Partner API 原生支持批量描述功能
- 一致性:与批量操作的设计理念一致
- 错误处理:支持部分失败场景,可以识别哪些对象描述失败
实现细节:
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 小时,提供缓存刷新机制
理由:
- 性能优化:对象元数据不经常变化,缓存可以减少 API 调用次数
- 响应速度:缓存可以显著提高响应速度,改善用户体验
- 降低 Salesforce API 负载:减少对 Salesforce 服务器的压力
- 灵活性:缓存是可选的,可以根据实际需求启用或禁用
- 一致性:缓存时间统一为 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 选项、关系字段引用等
理由:
- 完整性:提供完整的字段元数据,支持前端动态表单生成
- 灵活性:支持各种字段类型的特殊处理(Picklist、Lookup、Master-Detail 等)
- 可用性:前端可以根据字段元数据自动生成表单、验证规则等
- 一致性:统一的字段信息格式,便于客户端处理
实现细节:
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;
}
放弃方案:只返回基本字段信息
- 理由:信息不完整,前端无法充分利用元数据进行动态表单生成和验证
后果
正面影响
- 解耦性提升:使用 Map 格式转换描述结果,降低了客户端与 Salesforce API 的耦合
- 性能优化:批量描述和缓存策略显著提高了性能
- 响应速度提升:缓存机制减少了 API 调用,提高了响应速度
- 用户体验改善:完整的字段信息支持前端动态表单生成
- 代码一致性:与现有模块(CRUD、批量操作、查询)保持一致的编码风格
- 可维护性:统一的转换逻辑便于维护和扩展
负面影响
- 内存占用:缓存机制会增加内存占用
- 复杂性增加:转换逻辑增加了代码复杂度
- 缓存一致性:需要处理缓存刷新和失效逻辑
- 开发成本:完整的字段转换需要更多开发工作
替代方案
替代方案 1:使用 DTO 映射替代 Map
- 优点:类型安全,IDE 支持好,编译时检查
- 缺点:需要为每种描述结果创建 DTO 类,灵活性差,与现有代码风格不一致
- 适用场景:对类型安全要求极高的场景
替代方案 2:使用 Jackson 序列化原始对象
- 优点:实现简单,无需手动转换
- 缺点:可能包含不必要的内部字段,JSON 结构复杂,客户端处理困难
- 适用场景:快速原型开发,对 JSON 结构要求不高的场景
替代方案 3:使用 Redis 作为分布式缓存
- 优点:支持分布式部署,缓存容量大,支持持久化
- 缺点:引入额外依赖,增加系统复杂度,需要配置 Redis 服务器
- 适用场景:分布式部署,缓存需求大的场景