Skip to content

分页扩展

上一篇你学会了仓储层。这一篇讲项目里一个关键的查询基础设施:分页排序扩展——它让所有列表查询的分页和排序逻辑只写一次。

它解决了什么问题

没有这个扩展之前,每个列表查询都要手写排序校验和分页:

cangjie
// ❌ 没有扩展:每个 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> 的扩展方法:

cangjie
// 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. 排序白名单校验

cangjie
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. 分页包装

cangjie
public func toPageResult(model: QueryModel): (ArrayList<T>, Int64) {
    return this.orderBy(model).toPageResult(model.pageIndex, model.pageSize)
}

先调用 orderBy 处理排序,然后调用 SQLSharp 内置的 toPageResult(pageIndex, pageSize) 处理分页。返回 (数据列表, 总条数) 元组。

在查询里怎么用

有了扩展之后,任何列表查询都可以链式调用:

cangjie
// 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)
}
cangjie
// 评论列表查询——同样一行
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.pageIndexmodel.pageSize 里读分页参数,拼 LIMIT / OFFSET
  • 自动执行 COUNT(*) 拿总数

QueryModel 是什么

toPageResult(model) 依赖的 QueryModel 是分页和排序的载体:

cangjie
// 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"

json
// 排序参数示例
{ "orders": { "created_at": "DESC" }, "pageIndex": 1, "pageSize": 10 }

完整的分页查询骨架

把分页扩展和 Queries 基类的 data() 方法组合起来,一个列表查询的完整骨架就是:

cangjie
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
    }
}

新的查询类只要照这个骨架填业务条件就行,排序和分页不用再写一遍。

与仓储层的配合

分页扩展解决的是查询表达力问题——怎么写查询更简洁。仓储层解决的是查询复用问题——重复的查询条件放哪。

text
Queries 层:  用 toPageResult 拼动态筛选 → 收拢查询表达力
Repository 层:封装固定条件查询 → 收拢重复查询模板

两者互补,不是取代关系。列表分页查询通常留在 Queries 层(条件组合多变),而固定条件查询(如 findByAuthorId)适合放进 Repository。