集合
大约 5 分钟
集合
Sea-ORM 的查询结果通常是 Vec<Model>、(Model, RelatedModel) 元组或自定义投影(FromQueryResult)。本节聚焦“取回后的集合处理”:映射到 VO、分组/索引、构建树、聚合统计、分页归并与并行处理等。
要点
- 查询层尽量把筛选/排序/聚合放在数据库侧完成
- 集合层负责业务结构化与组装(VO、树、Map 索引、统计拼装)
- 大集合注意内存与分批处理(分页/主键范围)
1) 映射到 VO(View Object)
把数据库模型转换为对外输出的 VO,隐藏内部字段(如 password_hash)。
#[derive(Debug, serde::Serialize)]
pub struct UserVO {
pub id: i64,
pub username: String,
pub real_name: Option<String>,
pub dept_id: Option<i64>,
pub status: i32,
}
impl From<entity::sys_user::Model> for UserVO {
fn from(m: entity::sys_user::Model) -> Self {
Self {
id: m.id,
username: m.username,
real_name: m.real_name,
dept_id: m.dept_id,
status: m.status,
}
}
}
// 查询并映射
let models = entity::sys_user::Entity::find().all(&db).await?;
let vos: Vec<UserVO> = models.into_iter().map(UserVO::from).collect();
选择列并直接映射到轻量结构(FromQueryResult):
use sea_orm::{QuerySelect, FromQueryResult};
#[derive(Debug, FromQueryResult, serde::Serialize)]
struct UserLite { id: i64, username: String }
let items: Vec<UserLite> = entity::sys_user::Entity::find()
.select_only()
.column(entity::sys_user::Column::Id)
.column(entity::sys_user::Column::Username)
.into_model::<UserLite>()
.all(&db).await?;
2) 分组与索引(Vec -> HashMap/分桶)
- 按字段生成索引 Map:id -> Model
use std::collections::HashMap;
let users = entity::sys_user::Entity::find().all(&db).await?;
let by_id: HashMap<i64, entity::sys_user::Model> =
users.into_iter().map(|u| (u.id, u)).collect();
- 按部门分桶:dept_id -> Vec<UserVO>
use std::collections::HashMap;
let users = entity::sys_user::Entity::find().all(&db).await?;
let mut bucket: HashMap<i64, Vec<UserVO>> = HashMap::new();
for u in users {
if let Some(did) = u.dept_id {
bucket.entry(did).or_default().push(UserVO::from(u));
}
}
3) 构建树(无限级分类:菜单/部门)
从“平铺列表”构建树(parent_id 关系):
#[derive(Debug, serde::Serialize, Clone)]
pub struct MenuNode {
pub id: i64,
pub parent_id: Option<i64>,
pub name: String,
pub path: String,
pub icon: Option<String>,
pub sort: i32,
pub children: Vec<MenuNode>,
}
impl From<entity::sys_menu::Model> for MenuNode {
fn from(m: entity::sys_menu::Model) -> Self {
Self {
id: m.id,
parent_id: m.parent_id,
name: m.name,
path: m.path,
icon: m.icon,
sort: m.sort,
children: vec![],
}
}
}
fn build_tree(mut items: Vec<MenuNode>) -> Vec<MenuNode> {
use std::collections::HashMap;
let mut idx: HashMap<i64, usize> = HashMap::new();
for (i, n) in items.iter().enumerate() { idx.insert(n.id, i); }
let mut roots = vec![];
let mut result = items; // 复用内存
for i in 0..result.len() {
if let Some(pid) = result[i].parent_id {
if let Some(&pidx) = idx.get(&pid) {
result[pidx].children.push(result[i].clone());
} else {
roots.push(result[i].clone());
}
} else {
roots.push(result[i].clone());
}
}
// 可选:递归对子节点按 sort 排序
fn sort_tree(nodes: &mut [MenuNode]) {
nodes.sort_by_key(|n| n.sort);
for n in nodes.iter_mut() {
sort_tree(&mut n.children);
}
}
sort_tree(&mut roots);
roots
}
// 用法
let menus = entity::sys_menu::Entity::find()
.order_by_asc(entity::sys_menu::Column::Sort)
.all(&db).await?;
let nodes: Vec<MenuNode> = menus.into_iter().map(MenuNode::from).collect();
let tree = build_tree(nodes);
提示
- 如需“仅返回有权限的菜单”,先在 SQL 层筛选(join 用户-角色-菜单表),再构建树。
- 大树建议一次取齐,避免 N+1。
4) 汇总与聚合(结合数据库结果)
数据库聚合 + 集合拼装展示:
use sea_orm::sea_query::{Expr, Alias};
use sea_orm::{QuerySelect, FromQueryResult};
#[derive(FromQueryResult, serde::Serialize)]
struct DeptStat { dept_id: i64, user_count: i64 }
let stats: Vec<DeptStat> = entity::sys_user::Entity::find()
.select_only()
.column(entity::sys_user::Column::DeptId)
.column_as(Expr::val(1).count(), Alias::new("user_count"))
.group_by(entity::sys_user::Column::DeptId)
.into_model::<DeptStat>()
.all(&db).await?;
// 结合部门信息,组装 VO
5) 分页归并与分批处理
- 拉取大量数据时,请分页或分批(避免一次性占用太多内存):
use sea_orm::{PaginatorTrait, QuerySelect};
let page_size = 500_u64;
let paginator = entity::sys_user::Entity::find().paginate(&db, page_size);
let total_pages = paginator.num_pages().await?;
for page in 0..total_pages {
let chunk = paginator.fetch_page(page).await?;
// 处理本批数据...
}
- 主键范围分批(Keyset Pagination/范围扫描):
let mut last_id = 0_i64;
loop {
let batch = entity::sys_user::Entity::find()
.filter(entity::sys_user::Column::Id.gt(last_id))
.order_by_asc(entity::sys_user::Column::Id)
.limit(500)
.all(&db).await?;
if batch.is_empty() { break; }
last_id = batch.last().unwrap().id;
// 处理 batch ...
}
Keyset 适合大表稳定分页(避免 OFFSET 性能问题)。
6) (Model, Related) 结果重组
`find_also_related` 返回 Vec<(A, Option<B>)>,可整形为嵌套 VO:
#[derive(serde::Serialize)]
struct UserWithDeptVO { user: UserVO, dept: Option<DeptVO> }
let rows = entity::sys_user::Entity::find()
.find_also_related(entity::sys_dept::Entity)
.all(&db).await?;
let items: Vec<UserWithDeptVO> = rows.into_iter().map(|(u, d)| {
UserWithDeptVO { user: UserVO::from(u), dept: d.map(DeptVO::from) }
}).collect();
大量“多对一”建议使用“预加载”能力(见“预加载”章节)以减少 N+1。
7) 并行处理与限流
对 CPU 密集或 I/O 密集的后续处理,可并行执行,但务必限流避免压垮下游:
use futures::stream::{self, StreamExt};
let vos = stream::iter(models)
.map(|m| async move { heavy_convert(m).await }) // 异步转换
.buffer_unordered(16) // 并发 16
.collect::<Vec<_>>()
.await;
8) 序列化与 JSON
- 直接返回 VO/自定义投影(Serde 序列化)
- 对包含
DateTime<Utc>
的模型,启用 chrono 的 serde 特性 - 对 JSON 列(serde_json::Value)可原样传递或在 VO 中转换为具体类型
#[derive(serde::Serialize)]
struct UserProfileVO { id: i64, profile: serde_json::Value }
9) 去重、排序与稳定性
- Rust 端对 Vec 排序:
items.sort_by_key(|x| x.sort_field);
- 去重(根据 id):使用 HashSet 过滤
- 稳定排序(稳定性保障 UI 一致):
sort_by_key
本身稳定
use std::collections::HashSet;
let mut seen = HashSet::new();
items.retain(|x| seen.insert(x.id)); // 仅保留首次出现
10) 性能与内存提示
- 优先在 SQL 端过滤/聚合/排序,减少传输与内存占用
- 大集合分页/范围扫描;避免 all() 拉全表
- 映射/组装分阶段进行,释放中间临时 Vec
- 合理使用并发与限流(buffer_unordered)
- 对热路径编写基准测试,关注 95/99 分位耗时
小结
- 集合处理是“查询后的第二程”:把结构化数据按业务组装成 VO/树/索引
- 分批、并发与限流是大数据量处理的关键技巧
- 与“预加载/关联/作用域”配合,构建高性能的数据访问层