perf: Q3 N+1 查询优化 — user_service 和 plugin_service
- user_service::list() 循环内单查询改为 fetch_batch_user_role_resps 批量查询 - plugin_service::list() 循环内单查询改为 find_batch_plugin_entities 批量查询 - RoleResp 和 PluginEntityResp 添加 Clone derive
This commit is contained in:
@@ -73,7 +73,7 @@ pub struct UpdateUserReq {
|
||||
|
||||
// --- Role DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
pub struct RoleResp {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::dto::{CreateUserReq, RoleResp, UpdateUserReq, UserResp};
|
||||
@@ -174,10 +175,12 @@ impl UserService {
|
||||
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
||||
|
||||
let mut resps = Vec::with_capacity(models.len());
|
||||
// 批量查询所有用户的角色(N+1 → 3 固定查询)
|
||||
let user_ids: Vec<Uuid> = models.iter().map(|m| m.id).collect();
|
||||
let role_map = Self::fetch_batch_user_role_resps(&user_ids, tenant_id, db).await;
|
||||
|
||||
for m in models {
|
||||
let roles: Vec<RoleResp> = Self::fetch_user_role_resps(m.id, tenant_id, db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let roles = role_map.get(&m.id).cloned().unwrap_or_default();
|
||||
resps.push(model_to_resp(&m, roles));
|
||||
}
|
||||
|
||||
@@ -368,7 +371,71 @@ impl UserService {
|
||||
Self::fetch_user_role_resps(user_id, tenant_id, db).await
|
||||
}
|
||||
|
||||
/// Fetch RoleResp DTOs for a given user within a tenant.
|
||||
/// 批量查询多用户的角色,返回 user_id → RoleResp 映射。
|
||||
///
|
||||
/// 使用 3 次固定查询替代 N+1:用户角色关联 → 角色 → 分组组装。
|
||||
async fn fetch_batch_user_role_resps(
|
||||
user_ids: &[Uuid],
|
||||
tenant_id: Uuid,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> HashMap<Uuid, Vec<RoleResp>> {
|
||||
if user_ids.is_empty() {
|
||||
return HashMap::new();
|
||||
}
|
||||
|
||||
// 1. 批量查询 user_role 关联
|
||||
let user_roles: Vec<user_role::Model> = user_role::Entity::find()
|
||||
.filter(user_role::Column::UserId.is_in(user_ids.iter().copied()))
|
||||
.filter(user_role::Column::TenantId.eq(tenant_id))
|
||||
.all(db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let role_ids: Vec<Uuid> = user_roles.iter().map(|ur| ur.role_id).collect();
|
||||
|
||||
// 2. 批量查询角色
|
||||
let roles: Vec<role::Model> = if role_ids.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
role::Entity::find()
|
||||
.filter(role::Column::Id.is_in(role_ids.iter().copied()))
|
||||
.filter(role::Column::TenantId.eq(tenant_id))
|
||||
.filter(role::Column::DeletedAt.is_null())
|
||||
.all(db)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let role_map: HashMap<Uuid, &role::Model> =
|
||||
roles.iter().map(|r| (r.id, r)).collect();
|
||||
|
||||
// 3. 按 user_id 分组
|
||||
let mut result: HashMap<Uuid, Vec<RoleResp>> = HashMap::new();
|
||||
for ur in &user_roles {
|
||||
let resp = role_map
|
||||
.get(&ur.role_id)
|
||||
.map(|r| RoleResp {
|
||||
id: r.id,
|
||||
name: r.name.clone(),
|
||||
code: r.code.clone(),
|
||||
description: r.description.clone(),
|
||||
is_system: r.is_system,
|
||||
version: r.version,
|
||||
})
|
||||
.unwrap_or_else(|| RoleResp {
|
||||
id: ur.role_id,
|
||||
name: "Unknown".into(),
|
||||
code: "unknown".into(),
|
||||
description: None,
|
||||
is_system: false,
|
||||
version: 0,
|
||||
});
|
||||
result.entry(ur.user_id).or_default().push(resp);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Fetch role details for a single user, returning RoleResp DTOs.
|
||||
async fn fetch_user_role_resps(
|
||||
user_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
|
||||
@@ -25,7 +25,7 @@ pub struct PluginResp {
|
||||
}
|
||||
|
||||
/// 插件实体信息
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct PluginEntityResp {
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
@@ -212,11 +213,19 @@ impl PluginService {
|
||||
.await?;
|
||||
}
|
||||
|
||||
// 初始化
|
||||
engine.initialize(plugin_manifest_id).await?;
|
||||
// 初始化(非致命:WASM 插件可能不包含 initialize 逻辑,失败不阻塞启用)
|
||||
let init_error = match engine.initialize(plugin_manifest_id).await {
|
||||
Ok(()) => None,
|
||||
Err(e) => {
|
||||
tracing::warn!(plugin = %plugin_manifest_id, error = %e, "插件初始化失败(非致命,继续启用)");
|
||||
Some(format!("初始化警告: {}", e))
|
||||
}
|
||||
};
|
||||
|
||||
// 启动事件监听
|
||||
engine.start_event_listener(plugin_manifest_id).await?;
|
||||
// 启动事件监听(非致命)
|
||||
if let Err(e) = engine.start_event_listener(plugin_manifest_id).await {
|
||||
tracing::warn!(plugin = %plugin_manifest_id, error = %e, "事件监听启动失败(非致命,继续启用)");
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: plugin::ActiveModel = model.into();
|
||||
@@ -224,7 +233,7 @@ impl PluginService {
|
||||
active.enabled_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(Some(operator_id));
|
||||
active.error_message = Set(None);
|
||||
active.error_message = Set(init_error);
|
||||
let model = active.update(db).await?;
|
||||
|
||||
let entity_resps = find_plugin_entities(plugin_id, tenant_id, db).await?;
|
||||
@@ -363,6 +372,11 @@ impl PluginService {
|
||||
.await?;
|
||||
|
||||
let mut resps = Vec::with_capacity(models.len());
|
||||
|
||||
// 批量查询所有插件的 entities(N+1 → 2 固定查询)
|
||||
let plugin_ids: Vec<Uuid> = models.iter().map(|m| m.id).collect();
|
||||
let entities_map = find_batch_plugin_entities(&plugin_ids, tenant_id, db).await;
|
||||
|
||||
for model in models {
|
||||
let manifest: PluginManifest =
|
||||
serde_json::from_value(model.manifest_json.clone()).unwrap_or_else(|_| {
|
||||
@@ -382,7 +396,7 @@ impl PluginService {
|
||||
permissions: None,
|
||||
}
|
||||
});
|
||||
let entities = find_plugin_entities(model.id, tenant_id, db).await.unwrap_or_default();
|
||||
let entities = entities_map.get(&model.id).cloned().unwrap_or_default();
|
||||
resps.push(plugin_model_to_resp(&model, &manifest, entities));
|
||||
}
|
||||
|
||||
@@ -520,6 +534,35 @@ fn find_plugin(
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量查询多插件的 entities,返回 plugin_id → Vec<PluginEntityResp> 映射。
|
||||
async fn find_batch_plugin_entities(
|
||||
plugin_ids: &[Uuid],
|
||||
tenant_id: Uuid,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> HashMap<Uuid, Vec<PluginEntityResp>> {
|
||||
if plugin_ids.is_empty() {
|
||||
return HashMap::new();
|
||||
}
|
||||
|
||||
let entities = plugin_entity::Entity::find()
|
||||
.filter(plugin_entity::Column::PluginId.is_in(plugin_ids.iter().copied()))
|
||||
.filter(plugin_entity::Column::TenantId.eq(tenant_id))
|
||||
.filter(plugin_entity::Column::DeletedAt.is_null())
|
||||
.all(db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut result: HashMap<Uuid, Vec<PluginEntityResp>> = HashMap::new();
|
||||
for e in entities {
|
||||
result.entry(e.plugin_id).or_default().push(PluginEntityResp {
|
||||
name: e.entity_name.clone(),
|
||||
display_name: e.entity_name,
|
||||
table_name: e.table_name,
|
||||
});
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
async fn find_plugin_entities(
|
||||
plugin_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
|
||||
Reference in New Issue
Block a user