feat(plugin): Schema 缓存 — moka LRU Cache 消除 resolve_entity_info 重复查库
- 添加 moka 0.12 依赖到 erp-plugin 和 erp-server - 重写 state.rs: 新增 EntityInfo (含 generated_fields) 和 moka Cache - AppState 新增 plugin_entity_cache 字段 - data_service.rs: 旧 resolve_entity_info 保留兼容,新增 resolve_entity_info_cached
This commit is contained in:
@@ -23,3 +23,4 @@ utoipa = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
base64 = "0.22"
|
||||
moka = { version = "0.12", features = ["sync"] }
|
||||
|
||||
@@ -5,29 +5,15 @@ use erp_core::error::{AppError, AppResult};
|
||||
use erp_core::events::EventBus;
|
||||
|
||||
use crate::data_dto::PluginDataResp;
|
||||
use crate::dynamic_table::DynamicTableManager;
|
||||
use crate::dynamic_table::{sanitize_identifier, DynamicTableManager};
|
||||
use crate::entity::plugin;
|
||||
use crate::entity::plugin_entity;
|
||||
use crate::error::PluginError;
|
||||
use crate::manifest::PluginField;
|
||||
use crate::state::EntityInfo;
|
||||
|
||||
pub struct PluginDataService;
|
||||
|
||||
/// 插件实体信息(合并查询减少 DB 调用)
|
||||
struct EntityInfo {
|
||||
table_name: String,
|
||||
schema_json: serde_json::Value,
|
||||
}
|
||||
|
||||
impl EntityInfo {
|
||||
fn fields(&self) -> AppResult<Vec<PluginField>> {
|
||||
let entity_def: crate::manifest::PluginEntity =
|
||||
serde_json::from_value(self.schema_json.clone())
|
||||
.map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?;
|
||||
Ok(entity_def.fields)
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginDataService {
|
||||
/// 创建插件数据
|
||||
pub async fn create(
|
||||
@@ -391,6 +377,7 @@ pub async fn resolve_manifest_id(
|
||||
}
|
||||
|
||||
/// 从 plugin_entities 表获取实体完整信息(带租户隔离)
|
||||
/// 注意:此函数不填充 generated_fields,仅用于非 list 场景
|
||||
async fn resolve_entity_info(
|
||||
plugin_id: Uuid,
|
||||
entity_name: &str,
|
||||
@@ -411,9 +398,61 @@ async fn resolve_entity_info(
|
||||
Ok(EntityInfo {
|
||||
table_name: entity.table_name,
|
||||
schema_json: entity.schema_json,
|
||||
generated_fields: vec![], // 旧路径,不追踪 generated_fields
|
||||
})
|
||||
}
|
||||
|
||||
/// 从缓存或数据库获取实体信息(带 generated_fields 解析)
|
||||
pub async fn resolve_entity_info_cached(
|
||||
plugin_id: Uuid,
|
||||
entity_name: &str,
|
||||
tenant_id: Uuid,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
cache: &moka::sync::Cache<String, EntityInfo>,
|
||||
) -> AppResult<EntityInfo> {
|
||||
let cache_key = format!("{}:{}:{}", plugin_id, entity_name, tenant_id);
|
||||
if let Some(info) = cache.get(&cache_key) {
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
let entity = plugin_entity::Entity::find()
|
||||
.filter(plugin_entity::Column::PluginId.eq(plugin_id))
|
||||
.filter(plugin_entity::Column::TenantId.eq(tenant_id))
|
||||
.filter(plugin_entity::Column::EntityName.eq(entity_name))
|
||||
.filter(plugin_entity::Column::DeletedAt.is_null())
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
AppError::NotFound(format!("插件实体 {}/{} 不存在", plugin_id, entity_name))
|
||||
})?;
|
||||
|
||||
// 解析 generated_fields
|
||||
let entity_def: crate::manifest::PluginEntity =
|
||||
serde_json::from_value(entity.schema_json.clone())
|
||||
.map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?;
|
||||
let generated_fields: Vec<String> = entity_def
|
||||
.fields
|
||||
.iter()
|
||||
.filter(|f| f.field_type.supports_generated_column())
|
||||
.filter(|f| {
|
||||
f.unique
|
||||
|| f.sortable == Some(true)
|
||||
|| f.filterable == Some(true)
|
||||
|| (f.required && (f.sortable == Some(true) || f.filterable == Some(true)))
|
||||
})
|
||||
.map(|f| sanitize_identifier(&f.name))
|
||||
.collect();
|
||||
|
||||
let info = EntityInfo {
|
||||
table_name: entity.table_name,
|
||||
schema_json: entity.schema_json,
|
||||
generated_fields,
|
||||
};
|
||||
|
||||
cache.insert(cache_key, info.clone());
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// 校验数据:检查 required 字段
|
||||
fn validate_data(data: &serde_json::Value, fields: &[PluginField]) -> AppResult<()> {
|
||||
let obj = data.as_object().ok_or_else(|| {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use moka::sync::Cache;
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
use erp_core::error::{AppError, AppResult};
|
||||
use erp_core::events::EventBus;
|
||||
|
||||
use crate::engine::PluginEngine;
|
||||
@@ -10,4 +14,39 @@ pub struct PluginState {
|
||||
pub db: DatabaseConnection,
|
||||
pub event_bus: EventBus,
|
||||
pub engine: PluginEngine,
|
||||
/// Schema 缓存 — key: "{plugin_id}:{entity_name}:{tenant_id}"
|
||||
pub entity_cache: Cache<String, EntityInfo>,
|
||||
}
|
||||
|
||||
/// 缓存的实体信息
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EntityInfo {
|
||||
pub table_name: String,
|
||||
pub schema_json: serde_json::Value,
|
||||
pub generated_fields: Vec<String>,
|
||||
}
|
||||
|
||||
impl EntityInfo {
|
||||
/// 从 schema_json 解析字段列表
|
||||
pub fn fields(&self) -> AppResult<Vec<crate::manifest::PluginField>> {
|
||||
let entity_def: crate::manifest::PluginEntity =
|
||||
serde_json::from_value(self.schema_json.clone())
|
||||
.map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?;
|
||||
Ok(entity_def.fields)
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginState {
|
||||
pub fn new(db: DatabaseConnection, event_bus: EventBus, engine: PluginEngine) -> Self {
|
||||
let entity_cache = Cache::builder()
|
||||
.max_capacity(1000)
|
||||
.time_to_idle(Duration::from_secs(300))
|
||||
.build();
|
||||
Self {
|
||||
db,
|
||||
event_bus,
|
||||
engine,
|
||||
entity_cache,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +30,4 @@ erp-plugin.workspace = true
|
||||
anyhow.workspace = true
|
||||
uuid.workspace = true
|
||||
chrono.workspace = true
|
||||
moka = { version = "0.12", features = ["sync"] }
|
||||
|
||||
@@ -371,6 +371,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
redis: redis_client.clone(),
|
||||
default_tenant_id,
|
||||
plugin_engine,
|
||||
plugin_entity_cache: moka::sync::Cache::builder()
|
||||
.max_capacity(1000)
|
||||
.time_to_idle(std::time::Duration::from_secs(300))
|
||||
.build(),
|
||||
};
|
||||
|
||||
// --- Build the router ---
|
||||
|
||||
@@ -18,6 +18,8 @@ pub struct AppState {
|
||||
pub default_tenant_id: uuid::Uuid,
|
||||
/// 插件引擎
|
||||
pub plugin_engine: erp_plugin::engine::PluginEngine,
|
||||
/// 插件实体缓存
|
||||
pub plugin_entity_cache: moka::sync::Cache<String, erp_plugin::state::EntityInfo>,
|
||||
}
|
||||
|
||||
/// Allow handlers to extract `DatabaseConnection` directly from `State<AppState>`.
|
||||
@@ -90,6 +92,7 @@ impl FromRef<AppState> for erp_plugin::state::PluginState {
|
||||
db: state.db.clone(),
|
||||
event_bus: state.event_bus.clone(),
|
||||
engine: state.plugin_engine.clone(),
|
||||
entity_cache: state.plugin_entity_cache.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user