Skip to content

手写 SQL

这一页关注 SqlSharp 的另一条路径:保留对象映射和参数绑定,但直接手写 SQL。

适用场景

  • Queryable 不足以表达复杂查询
  • 需要执行 UPDATEDELETE、DDL 或数据库方言 SQL
  • 希望复用 SqlSharp 的参数绑定、结果映射和类型映射能力

核心问题

SqlSharp 没有强迫你只能走链式查询。DbContext 直接提供了三类入口:

  • execute() 执行增删改和 DDL
  • query<T>() 查询列表
  • executeScalar<T>() 查询标量值

快速开始

手写 SQL 的最短路径——参数绑定 + 查询列表:

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

let list = ctx.query<Product>(
    "SELECT id, name, price FROM products WHERE price > @minPrice",
    p
)

@minPrice 会被解析为 ?,参数值通过 TypeMapper 安全绑定,结果行自动映射回 Product 对象。

除了 query<T>(),还有 execute()(增删改/DDL)和 executeScalar<T>()(标量值)两个入口,详见下方三类入口。

核心概念

参数类型

execute()query<T>()executeScalar<T>() 的第二个参数是 Any,支持以下四种类型:

类型示例适用场景
DbParametersDbParameters().add("id", 1)手写参数绑定
Map<String, Any>{"id": Int64(1), "name": "A"}少量参数快速传值
Collection<(String, Any)>[("id", Int64(1)), ("name", "A")]保持参数顺序
@Model / @Serialization 对象ctx.query<T>(sql, model)实体对象直接作为参数源
cangjie
// 方式 1:DbParameters(推荐,类型安全)
let p = DbParameters()
p.add("minPrice", Int64(100))
ctx.query<Product>("SELECT * FROM products WHERE price > @minPrice", p)

// 方式 2:Map
ctx.query<Product>("SELECT * FROM products WHERE id = @id", {"id": Int64(1)})

// 方式 3:Collection 元组
ctx.query<Product>("SELECT * FROM products WHERE id = @id AND name = @name",
    [("id", Int64(1)), ("name", "Widget")])

// 方式 4:实体对象(自动提取属性值)
let filter = ProductFilter(minPrice: 100, maxPrice: 999)
ctx.query<Product>("SELECT * FROM products WHERE price > @minPrice AND price < @maxPrice", filter)

推荐优先使用 DbParameters——它提供明确的方法签名,编译器可以检查参数名拼写,值类型也有保证。

结果映射规则

当 SQL 返回列名和属性名不完全一致时,SqlSharp 会自动做两层匹配:

优先级规则示例
1忽略大小写精确匹配idid
2去掉下划线后再匹配user_nameuserName

如果都匹配不上,会跳过该列,不会因为单列失败而中断整个结果映射。

三类入口

DbContext 提供三个方法直接执行 SQL:

cangjie
// query<T>() — 查询列表,结果自动映射为对象
let list = ctx.query<Product>("SELECT * FROM products WHERE price > @min", p)

// execute() — 执行增删改或 DDL,返回影响行数
ctx.execute("UPDATE products SET price = @price WHERE id = @id", p)

// executeScalar<T>() — 查询标量值(COUNT、MAX 等)
let cnt = ctx.executeScalar<Int64>("SELECT COUNT(*) FROM products")

不需要 @Entity 的 DTO 也可以参与映射,只要具备序列化能力即可。

常见用法

IN 查询

集合参数可以自动展开:

cangjie
let p = DbParameters()
p.add("ids", [1, 2, 3])

let list = ctx.query<Product>(
    "SELECT * FROM products WHERE id IN @ids",
    p
)

执行 DDL

execute() 同样可以执行建表、加索引等 DDL 语句:

cangjie
ctx.execute("CREATE TABLE t_log (id BIGINT PRIMARY KEY, msg TEXT)")
ctx.execute("ALTER TABLE products ADD COLUMN stock INT DEFAULT 0")

动态拼接

需要按条件动态拼接 SQL 时,可以先构造片段:

cangjie
let sb = SqlBuilder()
sb.whereIf("price > @minPrice", minPrice > 0)
sb.whereIf("name IS NOT NULL", needName)
sb.orderBy("price DESC")

let sql = "SELECT * FROM products${sb.whereSql}${sb.orderSql}"
ctx.query<Product>(sql, params)

与其他模块组合

  • 查询结果仍然依赖 定义实体 里的映射规则
  • 自定义值转换会沿用 类型映射 的配置
  • 写操作需要事务时,可以配合 事务

生产建议

  • 手写 SQL 不是退路,而是复杂场景下的正常选择
  • SQL 文本里只放结构,参数一律用 DbParameters
  • 动态排序、动态字段名不要直接拼外部输入

常见问题

手写 SQL 会不会丢掉对象映射能力

不会。只要目标类型具备序列化映射能力,query<T>() 仍然会把列映射回对象。

DTO 需要 @Entity

不需要。只做结果映射时,通常只要 @Serialization