这篇文档讨论的不是 LiteOrm 内置查询语法,而是一种前后端协作的接入模式:前端把筛选条件放进 QueryString,后端再把这些参数转换成 LiteOrm Expr 并执行查询。
当查询字段比较稳定、又希望保留“地址栏可分享、可刷新、可回退”的体验时,这通常是成本最低的方案。
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 简单列表筛选 | QueryString | 易于调试、易于分享 URL |
| 条件较固定的后台列表页 | QueryString | 前后端心智成本低 |
| 复杂嵌套逻辑、动态条件组 | 原生 Expr | QueryString 表达力不足 |
| 需要浏览器回退/刷新保留状态 | QueryString | 查询条件天然体现在地址栏 |
落地这类查询接口时,推荐先约定清楚三件事:
Expr如果条件已经发展到多组 AND / OR、可视化条件构造器、多列动态排序,通常应切换到“前端原生 Expr 查询”。
例如 GET /api/orders/query 这类接口通常会支持以下常用参数:
| 参数 | 说明 |
|---|---|
keyword |
同时匹配订单号、客户名、商品名 |
status |
订单状态 |
departmentName |
部门名称包含匹配 |
createdByUserName |
创建人名称包含匹配 |
minTotalAmount / maxTotalAmount |
金额区间 |
createdFrom / createdTo |
创建时间区间 |
sortBy |
排序字段 |
desc |
是否倒序 |
page / pageSize |
分页参数 |
onlyMine |
是否强制只看当前用户自己的数据 |
URLSearchParams 组装参数const params = new URLSearchParams();
params.set("keyword", "Contoso");
params.set("status", "Pending");
params.set("sortBy", "CreatedTime");
params.set("desc", "true");
params.set("page", "1");
params.set("pageSize", "5");
const result = await demoApp.apiFetch(`/api/orders/query?${params.toString()}`);
如果还需要统计汇总,可以复用同一组筛选条件去调用:
const stats = await demoApp.apiFetch(`/api/orders/stats?${params.toString()}`);
async function queryOrders() {
const params = new URLSearchParams();
params.set("keyword", "Contoso");
params.set("status", "Pending");
params.set("sortBy", "CreatedTime");
params.set("desc", "true");
params.set("page", "1");
params.set("pageSize", "5");
const result = await demoApp.apiFetch(`/api/orders/query?${params.toString()}`);
demoApp.renderOrders("resultTable", result.items);
demoApp.renderJson("jsonOutput", result);
}
前端只是传参,真正的查询规则仍应放在后端统一处理。常见职责包括:
keyword、区间、排序字段等参数转换成 Expr这样才能保证 QueryString、统计接口、导出接口共用同一套查询规则。
如果列表接口每次都要返回 total,后端通常会额外执行一次 Count。在筛选条件频繁重复、翻页频繁发生时,这类计数查询很适合做短时缓存。
一个适合示例项目的实现方式是:
Count 结果,不缓存列表数据本身Expr 对象,并结合当前缓存版本号做有效性判断一个比较直接的做法是:
ExprExpr 作为 IMemoryCache 的键IMemoryCache 读取 countCountAsync(...),再把结果写回缓存LiteOrm 的 Expr 已经实现了结构化 Equals/GetHashCode,因此只要表达式结构一致,就可以稳定命中同一条缓存,而不必额外转成 JSON 字符串。
这样可以保证“同一个用户、同一组过滤条件”的分页查询命中同一个 count 缓存,而不同用户范围不会串缓存。
total,不是分页结果;分页列表仍应实时查询查询接口返回的核心字段包括:
| 字段 | 说明 |
|---|---|
page / pageSize |
当前页与每页条数 |
total |
符合条件的总记录数 |
items |
当前页结果 |
sql |
最近执行的 SQL 片段 |
统计接口会返回:
| 字段 | 说明 |
|---|---|
total |
总记录数 |
totalAmount |
金额汇总 |
pendingCount ~ cancelledCount |
各状态计数 |
sql |
最近执行的 SQL 片段 |
QueryString 只是传参方式,不承担授权职责。后端应统一注入用户范围条件:
admin 可以查看全部数据Admin 用户会自动只看到自己的订单onlyMine=true 可以让管理员也主动缩小到自己的数据范围前端推荐把这一点显示在页面说明里,但不要尝试在浏览器端自行替代后端授权。
优先使用 URLSearchParams,避免关键字中含空格、中文、特殊符号时出现编码问题。
total分页列表如果不读取 total,前端无法正确显示总页数、总记录数和翻页边界。
如果已经出现多组 AND/OR、动态排序、多条件组合,应该切换到原生 Expr 方式,而不是继续扩展 QueryString。