分页扩展
上一篇你学会了仓储层。这一篇讲项目里一个关键的查询基础设施:分页排序扩展——它让所有列表查询的分页和排序逻辑只写一次。
它解决了什么问题
没有这个扩展之前,每个列表查询都要手写排序校验和分页:
// ❌ 没有扩展:每个 Queries 方法都要写一遍
public func getBlogPostList(model: BlogPostSearchModel): PageResult<VBlogPost> {
var query = context.vBlogPosts.whereIf("is_public = 1")
// 手动处理排序——每个方法都要复制这段
if (let Some(orders) <- model.orders) {
for ((name, value) in orders) {
if (name == "created_at") {
query.orderBy("created_at ${value}")
}
// ... 每个字段都要写一遍
}
}
// 手动算总数 + 分页
let total = query.count()
let list = query.skip((model.pageIndex - 1) * model.pageSize).take(model.pageSize).toList()
return this.data(list, total)
}问题很明显:排序逻辑和分页逻辑在每个查询里重复。字段一多,if-else 就爆炸。
扩展做了什么
项目里的 QueryableExtension.cj 把排序 + 分页封装成了 Queryable<T> 的扩展方法:
// QueryableExtension.cj
package admin.application.queries
import sqlsharp.*
import std.reflect.*
import std.collection.*
import admin.abstractions.*
import sqlsharp.serialization.*
public interface QueryableExtension<T> where T <: ISerialization<T> {
func orderBy(model: QueryModel): Queryable<T>
func toPageResult(model: QueryModel): (ArrayList<T>, Int64)
}
extend<T> Queryable<T> <: QueryableExtension<T> where T <: ISerialization<T> {
public func orderBy(model: QueryModel): Queryable<T> {
if (let Some(orders) <- model.orders) {
let metadata = this.context.model
let entityType = metadata.getEntityType(TypeInfo.of<T>())
for ((name, value) in orders) {
// 必须是合法的字段——白名单校验
if (entityType.findProperty(name).isNone()) {
continue
}
if (value.equalsIgnoreAsciiCase("DESC")) {
this.orderBy("${name} DESC")
} else {
this.orderBy("${name} ASC")
}
}
}
return this
}
public func toPageResult(model: QueryModel): (ArrayList<T>, Int64) {
return this.orderBy(model).toPageResult(model.pageIndex, model.pageSize)
}
}核心逻辑只有两步:
1. 排序白名单校验
let metadata = this.context.model
let entityType = metadata.getEntityType(TypeInfo.of<T>())
// 从实体元数据里找到真实的属性列表
for ((name, value) in orders) {
if (entityType.findProperty(name).isNone()) {
continue // 这个字段不存在于实体 → 跳过,不拼进 SQL
}
// 字段存在 → 安全拼接
this.orderBy("${name} ${value}")
}不是直接把调用方传来的字段名拼进 SQL,而是先查实体的元数据:只有实体上真实存在的属性才允许用来排序。不存在的字段直接 continue 跳过,从根本上防止非法排序字段注入。
2. 分页包装
public func toPageResult(model: QueryModel): (ArrayList<T>, Int64) {
return this.orderBy(model).toPageResult(model.pageIndex, model.pageSize)
}先调用 orderBy 处理排序,然后调用 SQLSharp 内置的 toPageResult(pageIndex, pageSize) 处理分页。返回 (数据列表, 总条数) 元组。
在查询里怎么用
有了扩展之后,任何列表查询都可以链式调用:
// BlogQueries.cj — 所有列表查询的统一写法
public func getBlogPostList(model: BlogPostSearchModel): PageResult<VBlogPost> {
var (list, total) = context
.vBlogPosts
.params(model)
.whereIf("is_public = 1")
.whereIf("category_code = @categoryCode", model.categoryCode.isSome())
.toPageResult(model) // ← 一行搞定排序 + 分页
return this.data(list, total)
}// 评论列表查询——同样一行
public func getComments(postId: Int64, model: QueryModel): PageResult<VComment> {
let (list, total) = context
.vComments
.whereIf("post_id = ${postId}")
.toPageResult(model) // ← 复用同一个扩展
return this.data(list, total)
}不管查什么实体,toPageResult(model) 的行为都是一致的:
- 从
model.orders里读排序规则,过白名单校验后拼ORDER BY - 从
model.pageIndex和model.pageSize里读分页参数,拼LIMIT/OFFSET - 自动执行
COUNT(*)拿总数
QueryModel 是什么
toPageResult(model) 依赖的 QueryModel 是分页和排序的载体:
// abstractions/QueryModel.cj
@Model
@Serialization
public open class QueryModel {
private var _pageSize: Int64 = 10 // 默认每页 10 条
private var _pageIndex: Int64 = 1 // 默认第 1 页
private var _orders: ?OrderModel = OrderModel() // 排序规则
}OrderModel 本质是一个 Map<String, String>,key 是字段名,value 是 "ASC" 或 "DESC":
// 排序参数示例
{ "orders": { "created_at": "DESC" }, "pageIndex": 1, "pageSize": 10 }完整的分页查询骨架
把分页扩展和 Queries 基类的 data() 方法组合起来,一个列表查询的完整骨架就是:
public class SomeQueries <: Queries {
public SomeQueries(let context: AdminDbContext) {}
public func getList(model: SomeSearchModel): PageResult<SomeEntity> {
var (list, total) = context
.someEntities // DbSet 或视图
.params(model) // 绑定查询参数
.whereIf("固定条件")
.whereIf("动态条件", model.someField.isSome())
.toPageResult(model) // 排序 + 分页(扩展方法)
return this.data(list, total) // 包装成 PageResult
}
}新的查询类只要照这个骨架填业务条件就行,排序和分页不用再写一遍。
与仓储层的配合
分页扩展解决的是查询表达力问题——怎么写查询更简洁。仓储层解决的是查询复用问题——重复的查询条件放哪。
Queries 层: 用 toPageResult 拼动态筛选 → 收拢查询表达力
Repository 层:封装固定条件查询 → 收拢重复查询模板两者互补,不是取代关系。列表分页查询通常留在 Queries 层(条件组合多变),而固定条件查询(如 findByAuthorId)适合放进 Repository。