05. 数据访问层(DataProvider)
1. 背景与原理
1.1 SOVD 的数据抽象
SOVD 把"诊断数据"抽象为可寻址的资源,而非 UDS 的"按 DID 读写"。每个实体(Component/App/Area)可以挂一组数据项,每项有:
id、name、category(identData/currentData/storedData/sysInfo)groups、tags(多维分类,用于过滤)schema(JSON Schema,可随响应下发)is_readable/is_writable
服务端不关心数据从哪来(传感器、配置文件、UDS 适配器、内存变量),只关心一个统一接口。这就是 DataProvider trait 存在的意义------它是 OpenSOVD 最重要的扩展点。
1.2 为什么需要类型擦除
理想 API 是泛型资源:
rust
trait ReadableDataResource {
type Value: Serialize + JsonSchema;
async fn read(&self) -> Result<Self::Value, DataError>;
}
但 type Value 是关联类型 → trait 不对象安全 → 无法放进 Box<dyn ...> → 无法把不同类型的数据项存在同一个 IndexMap 里(而拓扑要求一个 provider 持有多个异构数据项)。
因此必须做类型擦除 :用一个统一载体(serde_json::Value)在边界上转换。
2. 当前实现架构
2.1 核心 trait(opensovd-core/src/data.rs)
rust
#[async_trait]
pub trait DataProvider: Send + Sync + 'static {
// 必需
async fn list(&self, filter: DataFilter) -> Result<Vec<Metadata>>;
async fn read(&self, data_id: &str, include_schema: bool) -> Result<Data>;
async fn write(&self, data_id: &str, value: serde_json::Value) -> Result<()>;
// 默认实现(基于 list() 推导)
async fn categories(&self) -> Result<Vec<CategoryInfo>> { ... }
async fn groups(&self, category_filter: Option<&str>) -> Result<Vec<GroupInfo>> { ... }
async fn tags(&self) -> Result<Vec<TagInfo>> { ... }
}
配套类型:
rust
pub enum DataScope { Groups(Vec<String>), Categories(Vec<String>) } // 互斥
pub struct DataFilter { pub scope: Option<DataScope>, pub tags: Vec<String> }
pub enum DataError { NotFound(String), ReadOnly, Internal(String) }
2.2 参考实现(opensovd-providers)
DataResource (supertrait)
├─ ReadableDataResource { type Value: Serialize + JsonSchema; async fn read() }
└─ WriteableDataResource { type Value: DeserializeOwned + JsonSchema; async fn write() }
│
│ 适配器(类型擦除)
▼
DataResourceDyn (对象安全) { async fn read() -> Value; async fn write(Value) }
│
▼
DataProviderBuilder ──build()──> BuiltDataProvider ──implements──> DataProvider
三套适配器:
| 适配器 | read | write |
|---|---|---|
ReadOnlyAdapter<T> |
真读 | Err(DataError::ReadOnly) |
WriteOnlyAdapter<T> |
Err(Internal("resource is write-only")) |
真写 |
ReadWriteAdapter<T> |
转发 | 转发 |
2.3 内置资源
Constant<T>:构造时把值包进 SOVD 信封 {"value": T}(符合 ISO 17978-3 的 object envelope 要求),并一次性生成 schema 缓存。
3. 核心流程与算法
3.1 类型擦除算法
50:100:opensovd-providers/src/data/builder.rs
#[async_trait]
trait DataResourceDyn: Send + Sync + 'static {
async fn read(&self) -> Result<Value, DataError>;
async fn write(&self, value: Value) -> Result<(), DataError>;
}
impl<T: ReadableDataResource> DataResourceDyn for ReadOnlyAdapter<T> {
async fn read(&self) -> Result<Value, DataError> {
let v = self.0.read().await?;
serde_json::to_value(v).map_err(|e| DataError::Internal(e.to_string())) // ← 擦除点
}
async fn write(&self, _: Value) -> Result<(), DataError> {
Err(DataError::ReadOnly)
}
}
算法:
- 调用泛型资源拿到具体类型
T::Value serde_json::to_value转成Value(擦除点,也是唯一的运行时开销)- 写入方向:
serde_json::from_value反序列化回T::Value ResourceEntry { metadata, resource: Box<dyn DataResourceDyn> }存入IndexMap
3.2 Builder 组装流程
rust
DataProviderBuilder::new()
.read_data("voltage", Constant::new(12.6)) // 注册时即取 schema 并写入 Metadata
.groups(["power"])
.tags(["sensor"])
.build()
- schema 生成时机 :注册时(builder 期)一次性生成 并存入
Metadata.schema,请求期只 clone------这是正确的性能设计(对比服务端include-schema每次重算,见 §4)。 - 后置修饰器 :
groups/tags/translation_id/schema只对resources.last_mut()生效(修饰最近注册的项)。 - 唯一校验:重复 ID 检测
245:254:opensovd-providers/src/data/builder.rs
pub fn build(self) -> Result<BuiltDataProvider, BuildError> {
let mut resources = IndexMap::with_capacity(self.resources.len());
for entry in self.resources {
let id = entry.metadata.id.clone();
if resources.insert(id.clone(), entry).is_some() {
return Err(BuildError::DuplicateResourceId(id));
}
}
Ok(BuiltDataProvider { resources })
}
3.3 过滤匹配算法
267:288:opensovd-providers/src/data/builder.rs
fn matches_filter(metadata: &Metadata, filter: &DataFilter) -> bool {
match &filter.scope {
Some(DataScope::Groups(groups)) =>
groups.iter().any(|g| metadata.groups.contains(g)), // OR 语义
Some(DataScope::Categories(cats)) =>
cats.iter().any(|c| &metadata.category == c), // OR 语义
None => true,
} && (filter.tags.is_empty()
|| filter.tags.iter().any(|t| metadata.tags.contains(t))) // 空则不过滤
}
语义 :groups 与 categories 互斥(枚举保证),组内为 OR;tags 为空表示不过滤,否则 OR。
3.4 categories/groups/tags 的两种实现
| 实现 | 算法 | 复杂度 |
|---|---|---|
| core 默认实现 | 全量 list(DataFilter::default()) → 内存去重 |
O(全部元数据) |
BuiltDataProvider 覆写 |
直接遍历 IndexMap + HashSet 去重 |
O(n),无中间 Vec |
BuiltDataProvider 覆写了这三个方法,绕过了全量 list 的默认实现------说明默认实现只是"兜底",真实实现应当覆写。
core 默认实现示例:
94:105:opensovd-core/src/data.rs
async fn categories(&self) -> Result<Vec<CategoryInfo>> {
let all = self.list(DataFilter::default()).await?;
let mut seen = std::collections::HashSet::new();
Ok(all.into_iter()
.filter(|m| seen.insert(m.category.clone()))
.map(|m| CategoryInfo { category: m.category, translation_id: m.translation_id })
.collect())
}
3.5 读写流程(服务端视角)
GET /v1/components/{id}/data/{data_id}?include-schema=true
→ topology.read() → get_component → data_provider()
→ provider.read(data_id, true).await
→ Data { data: Value, schema: Option<Value> }
→ ReadResponse { id, data, errors: None, schema }
PUT /v1/components/{id}/data/{data_id}
→ 同上取 provider
→ provider.write(data_id, body.data).await
→ 204 No Content
错误映射:NotFound → 404;ReadOnly → 400;Internal → 500(且消息脱敏)。
4. 待完善与风险
4.1 语义与一致性(严重)
- 读 write-only 资源返回 500(高) :
WriteOnlyAdapter::read抛DataError::Internal("resource is write-only")→ 500 + 消息脱敏。GET 一个只写资源应返回 405 Method Not Allowed 或 400 + 明确 error_code,而非"内部错误"。这是语义错误 + 可观测性损失。 - 写只读资源返回 400 而非 405(中) :
DataError::ReadOnly→ 400error-response。HTTP 语义上 405 更准确;SOVD 语义上precondition-not-fulfilled更贴切。 groups与categories同时出现被静默吞掉(中) :路由层data_filter()中 groups 优先、categories 被丢弃(见 07 章)。客户端传了两个参数却只生效一个,无任何提示。应返回 400 +incomplete-request。categories()的translation_id取首个资源(中) :Metadata.translation_id语义是数据项 的翻译 ID,被当作 category 的翻译 ID 返回;同 category 下不同资源翻译 ID 不同时结果不确定。groups()按 group id 全局去重(中) :同名 group 跨 category 时只保留首次出现的 category,另一 category 下的该 group 会消失。且GroupInfo的category_translation_id/group/group_translation_id恒为None,TagInfo的description/translation_id同样恒None------本地化信息在 API 层必然丢失。- 结果未排序(低) :
categories()/tags()的顺序取决于list()返回顺序且未排序,跨调用不稳定,不利于缓存与比对。
4.2 错误模型(中)
DataError表达能力不足(中) :只有NotFound/ReadOnly/Internal三个变体。缺少:InvalidFilter(非法过滤条件 → 400)InvalidValue(写入值不符合 schema → 400,现在只能兜Internal)Unauthorized(数据级权限)Unavailable(后端临时不可用 → 503,可重试)
结果是 HTTP 层无法做出精确的 4xx/5xx 区分。
4.3 性能(中)
- 服务端
include-schema每次请求重算(中) :schemars::schema_for!在请求路径上执行(data.rs:192/348),未缓存。schema 是静态可预生成的,应在 lazy static 中缓存一次。 - 默认实现全量
list()(中) :未覆写categories/groups/tags的 provider 每次都要拉全量元数据。建议在 trait 文档中明确要求覆写,或提供基于list()的缓存层。 DataFilter.tags在默认实现中被忽略(低) :有意为之(分类应穷举),但DataFilter的语义边界未文档化,实现者容易误解。
4.4 安全与合规(中)
WriteRequest.signature被解析但完全忽略(高) :models 层定义了signature字段,标准也有invalid-signature错误码,但服务端不做任何校验 。对于车载写操作(尤其是storedData配置写入),签名校验是安全与合规的硬要求。这是当前最明确的安全缺口。- 写操作无审计(中) :
PUT成功只返回 204,无操作日志(谁、何时、写了什么、旧值是什么)。车规场景通常需要审计日志(对应 SOVD 的logs/communication-logs能力)。 - 无写权限细分(中) :授权器只能看到 HTTP method + path(见 08 章),无法做到"允许读 voltage 但禁止写 config"。数据级授权需要 provider 层配合。
4.5 API 可用性(低)
- builder 后置修饰器静默失效(中) :
groups()/tags()等在resources为空时是 no-op。典型误用(修饰器写在read_data之前)既无编译期错误也无运行期提示。建议用类型状态(builder 阶段标记"已注册")或在build()时校验。 Constant强制T: JsonSchema(低) :即使关闭jsonschemafeature 也需要,导致 providers 硬依赖 schemars(见 02 章 §4.1)。- mock 全只读(中) :24 个数据项全为
read_data,PUT路径在集成测试中只能命中 400,写链路的正向用例覆盖缺失。
4.6 建议的改进顺序
| 优先级 | 事项 |
|---|---|
| P0 | 实现 signature 校验(或明确声明"暂不支持签名"并在文档中标注) |
| P0 | DataError 扩充变体,HTTP 层精确映射 4xx |
| P1 | 修正 write-only → 405、read-only → 405/409 的语义 |
| P1 | 缓存 schema_for! 结果(lazy static) |
| P1 | groups()/tags() 修正去重键与本地化字段填充 |
| P2 | 数据项级授权钩子(provider 层 or 授权器 input 扩展) |
| P2 | 写操作审计日志 |
5. 关键代码位置
| 内容 | 路径 |
|---|---|
DataProvider trait 与默认实现 |
opensovd-core/src/data.rs:85-145 |
DataFilter / Metadata / DataError |
opensovd-core/src/data.rs:10-57 |
| 对象安全 trait + 三套适配器 | opensovd-providers/src/data/builder.rs:50-100 |
| Builder 与重复 ID 校验 | opensovd-providers/src/data/builder.rs:102-256 |
BuiltDataProvider 实现与过滤算法 |
opensovd-providers/src/data/builder.rs:267-365 |
| 资源 trait 与 blanket impl | opensovd-providers/src/data/resource.rs:16-61 |
Constant 实现 |
opensovd-providers/src/data/constant.rs:29-63 |
SOVD 信封 Value<T> |
opensovd-providers/src/data/mod.rs:22-26 |
| 服务端数据路由 | opensovd-server/src/routes/data.rs:37-424 |
| mock 拓扑(24 数据项) | opensovd-mocks/src/lib.rs |