Skip to content

Queryable 查询

链式拼接 SQL、参数绑定、分页排序、聚合统计、DTO 投影——一套统一的查询入口。

适用场景

  • 单表或多表关联的常规查询
  • 需要分页、排序、条件过滤
  • 需要 COUNT / SUM / AVG 等聚合统计
  • 需要只查部分列并投影到 DTO
  • 想把通用查询逻辑提炼成复用方法

核心问题

DbSet<T> 继承 Queryable<T>。每个 Queryable 实例内部维护一个 SqlBuilder,链式调用逐步往 SQL 上追加 SELECTWHEREORDER BYGROUP BYLIMIT 等子句,最后调用末尾执行方法真正发起数据库查询。

核心规则只有两条:

  • 前半段方法(whereIforderByskip 等)返回 Queryable<T>,可继续链式拼接
  • 末尾方法(toListfirstcount 等)执行 SQL 并返回结果

快速开始

先看 whereIf + toList 这条最短路径:

cangjie
import sqlsharp.*
import sqlsharp.annotations.*

main(): Int64 {
    // ctx 是自定义 DbContext,products 是 DbSet<Product>
    try (ctx = MyDbContext()) {
        let list = ctx.products
            .whereIf("price > 0")
            .orderBy("price DESC")
            .toList()
    }
    return 0
}

这段代码生成 SELECT ... FROM product WHERE (price > 0) ORDER BY price DESC,返回 ArrayList<Product>

带参数的标准写法:

cangjie
let p = DbParameters()
p.add("minPrice", Int64(100))

let list = ctx.products
    .whereIf("price >= @minPrice")
    .params(p)
    .orderBy("price")
    .toList()

@minPrice 会被 SqlParser 解析为 ? 占位符,参数值通过 TypeMapper 安全绑定。这就是 Queryable 最核心的使用模式。

核心概念

链式构建方法

这些方法追加 SQL 子句,返回 Queryable<T> 自身,可继续链式调用:

方法作用示例
.whereIf("sql")条件过滤,多个自动 ANDwhereIf("price > 0")
.whereIf("sql", flag)条件过滤,仅在 flag 为 true 时生效whereIf("name != ''", name.size > 0)
.orderBy("col")排序orderBy("price DESC")
.groupBy("col")分组groupBy("category_id")
.having("cond")分组后过滤having("COUNT(*) > 5")
.skip(n)跳过前 n 条skip(10)
.take(m)取 m 条take(20)
.asNoTracking()查询结果不进入变更跟踪asNoTracking()
.params(DbParameters)绑定命名参数params(p)
.params(model)从实体对象提取参数params(entity)
.select<TResult>(["col1"])投影到 DTOselect<DTO>(["id", "name"])
.alias("name")设置表别名alias("p")

末尾执行方法

这些方法生成并执行 SQL,返回最终结果:

方法返回类型说明
.toList()ArrayList<T>查询全部结果
.toPageResult(index, size)(ArrayList<T>, Int64)分页 + 总行数
.find([keyValues])?T按主键查找,未找到返回 None
.first()T第一条,无结果抛异常
.firstOrDefault()?T第一条,无结果返回 None
.any()Bool是否存在匹配行
.count(["*"])Int64计数
.sum<T>(["col"])T求和
.avg<T>(["col"])T平均值
.max<T>(["col"])T最大值
.min<T>(["col"])T最小值

默认情况下,toList() 会自动按主键升序排列。显式指定 orderBy 后会覆盖这个默认行为。

常见用法

按主键查找

cangjie
if (let Some(entity) <- ctx.products.find([id])) {
    // 找到了,entity 处于 Tracked 状态
}

find() 内部遍历主键属性,自动拼接 WHERE pk = @pk,无需手写 SQL。

条件过滤

多个 whereIf 自动以 AND 连接。第二个参数为 false 时跳过该条件,适合动态筛选:

cangjie
let p = DbParameters()
p.add("minPrice", 0)
p.add("maxPrice", 999)
p.add("keyword", String.empty)

ctx.products
    .whereIf("price >= @minPrice")
    .whereIf("price <= @maxPrice")
    .whereIf("name LIKE @keyword", keyword.size > 0)  // keyword 为空则跳过
    .params(p)
    .toList()

IN 查询

cangjie
let p = DbParameters()
p.add("ids", [Int64(1), Int64(2), Int64(3)])
let list = ctx.products.whereIf("id IN @ids").params(p).toList()

// 多个 IN 参数可以混用
p.add("ids", [Int64(1), Int64(2)])
p.add("names", ["A", "B"])
ctx.products
    .whereIf("id IN @ids")
    .whereIf("name IN @names")
    .params(p)
    .toList()

集合参数会自动展开为 @ids_0, @ids_1, ...。集合数量不大、参数类型简单时优先用 Queryable;上百个值或复杂子查询时切 手写 SQL

分页

手动控制偏移量:

cangjie
// 第 3 页,每页 10 条
let list = ctx.products
    .orderBy("price DESC")
    .skip(20)
    .take(10)
    .toList()

一次拿到数据 + 总数:

cangjie
let (list, total) = ctx.products
    .whereIf("price > @minPrice")
    .params(p)
    .orderBy("price DESC")
    .toPageResult(3, 10)
// list 是第 3 页数据,total 是符合条件的总行数

toPageResult 内部执行两条 SQL:一条查询分页数据,一条 SELECT COUNT(*) 查总数。

聚合统计

cangjie
let total = ctx.products.count(["*"])
let exists = ctx.products.whereIf("price > 100").any()
let sum = ctx.products.sum<Int64>(["price"])
let avg = ctx.products.avg<Decimal>(["price"])
let max = ctx.products.max<Int64>(["price"])
let min = ctx.products.min<Int64>(["price"])

聚合方法返回的是聚合值本身,不是 ArrayList<T>

DTO 投影

只查需要的列,映射到轻量 DTO:

cangjie
@Serialization
public class ProductSummary {
    private var _id: Int64 = 0
    private var _name: String = String.empty
    private var _price: Int64 = 0
}

let dtos = ctx.products
    .select<ProductSummary>(["id", "name", "price"])
    .whereIf("price > @minPrice")
    .params(p)
    .orderBy("price DESC")
    .toList()

select<TResult> 返回的是 Queryable<TResult>,后续链式方法操作的是投影后的列。

分组与 HAVING

cangjie
// 按 category_id 分组,只取商品数 > 5 的分组
let groups = ctx.products
    .groupBy("category_id")
    .having("COUNT(*) > 5")
    .select<ProductGroupView>(["category_id", "COUNT(*) AS cnt"])
    .toList()

不跟踪的只读查询

列表查询、报表、导出等只读场景务必加 asNoTracking(),详见 变更跟踪

扩展 Queryable

将通用分页、排序逻辑提炼为扩展方法:

cangjie
@Model
@Serialization
public class PageQuery {
    private var _pageIndex: Int64 = 1
    private var _pageSize: Int64 = 10
    private var _orderBy: ?String = None
}

extend<T> Queryable<T> where T <: ISerialization<T> {
    public func toPageResult(model: PageQuery): (ArrayList<T>, Int64) {
        if (let Some(orderBy) <- model.orderBy) {
            this.orderBy(orderBy)
        }
        return this.toPageResult(model.pageIndex, model.pageSize)
    }
}

排序字段来自外部输入时务必做白名单校验,不要直接拼接原始字符串。

常见问题

toList()toPageResult() 的排序行为不一样?

toList() 在不指定 orderBy 时默认按主键升序,保证结果顺序稳定。toPageResult() 内部先拼接分页 SQL 再执行,推荐显式指定 orderBy 避免依赖默认行为。

select<TResult>() 的列名必须和 DTO 字段名一致?

列名必须和实体属性名一致(不是数据库列名)。SqlSharp 内部按属性名做映射——实体上的 @Column("db_name") 已经完成了"数据库列名 → 属性名"的转换,投影时用的是属性名。

asNoTracking() 之后还能 saveChanges() 吗?

不能。asNoTracking() 返回的实体处于 Detached 状态,ChangeTracker 不跟踪它。如果需要更新,先用 find()first() 查出跟踪状态的实体,再修改。

相关专题

  • 定义实体@Entity / @Model / @Serialization 的用法
  • 变更跟踪asNoTracking() 和 ChangeTracker 的关系
  • 手写 SQL — 复杂查询、多表关联、数据库方言场景
  • 事务 — 多步查询放在一个事务中执行