Queryable 查询
链式拼接 SQL、参数绑定、分页排序、聚合统计、DTO 投影——一套统一的查询入口。
适用场景
- 单表或多表关联的常规查询
- 需要分页、排序、条件过滤
- 需要 COUNT / SUM / AVG 等聚合统计
- 需要只查部分列并投影到 DTO
- 想把通用查询逻辑提炼成复用方法
核心问题
DbSet<T> 继承 Queryable<T>。每个 Queryable 实例内部维护一个 SqlBuilder,链式调用逐步往 SQL 上追加 SELECT、WHERE、ORDER BY、GROUP BY、LIMIT 等子句,最后调用末尾执行方法真正发起数据库查询。
核心规则只有两条:
- 前半段方法(
whereIf、orderBy、skip等)返回Queryable<T>,可继续链式拼接 - 末尾方法(
toList、first、count等)执行 SQL 并返回结果
快速开始
先看 whereIf + toList 这条最短路径:
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>。
带参数的标准写法:
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") | 条件过滤,多个自动 AND | whereIf("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"]) | 投影到 DTO | select<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 后会覆盖这个默认行为。
常见用法
按主键查找
if (let Some(entity) <- ctx.products.find([id])) {
// 找到了,entity 处于 Tracked 状态
}find() 内部遍历主键属性,自动拼接 WHERE pk = @pk,无需手写 SQL。
条件过滤
多个 whereIf 自动以 AND 连接。第二个参数为 false 时跳过该条件,适合动态筛选:
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 查询
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。
分页
手动控制偏移量:
// 第 3 页,每页 10 条
let list = ctx.products
.orderBy("price DESC")
.skip(20)
.take(10)
.toList()一次拿到数据 + 总数:
let (list, total) = ctx.products
.whereIf("price > @minPrice")
.params(p)
.orderBy("price DESC")
.toPageResult(3, 10)
// list 是第 3 页数据,total 是符合条件的总行数toPageResult 内部执行两条 SQL:一条查询分页数据,一条 SELECT COUNT(*) 查总数。
聚合统计
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:
@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
// 按 category_id 分组,只取商品数 > 5 的分组
let groups = ctx.products
.groupBy("category_id")
.having("COUNT(*) > 5")
.select<ProductGroupView>(["category_id", "COUNT(*) AS cnt"])
.toList()不跟踪的只读查询
列表查询、报表、导出等只读场景务必加 asNoTracking(),详见 变更跟踪。
扩展 Queryable
将通用分页、排序逻辑提炼为扩展方法:
@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() 查出跟踪状态的实体,再修改。