Skip to content

数据访问

你已经理解了分层架构、仓储层和分页扩展。这一篇专门讲 SQLSharp 在项目里的几个关键用法和工程习惯——这些是后面内容模块和认证模块的数据访问基础。

它在项目里承担了什么

SQLSharp 在这个项目里做四件事:

  1. DbContext — 统一数据库入口,每个请求共用一个实例
  2. 实体映射 — 表和视图映射到仓颉类
  3. Queryable 查询whereIf + toPageResult 链式查询
  4. DB-First 代码生成 — 从数据库生成实体,而不是手写

DbContext 的注册和生命周期

AdminDbContextInfrastructureModule 里注册:

cangjie
// InfrastructureModule.cj
context.services.addDbContext<AdminDbContext> {
    options => options.useMysql(connectionString, "mariadb")
}

addDbContext 默认是 Scoped 生命周期——一个 HTTP 请求内共用一个实例。这意味着同一个请求里的 Repository、Queries、Service 拿到的都是同一个 DbContext,事务才能统一控制。

AdminDbContext 本身的结构:每个表一个 DbSet 属性,每个视图一个 DbSet 属性:

cangjie
public class AdminDbContext <: DbContext & IServiceFactory<AdminDbContext> {
    // 表
    public prop users: DbSet<User> { get() { this.set<User>() } }
    public prop blogPosts: DbSet<BlogPost> { get() { this.set<BlogPost>() } }
    public prop comments: DbSet<Comment> { get() { this.set<Comment>() } }
    public prop categories: DbSet<Category> { get() { this.set<Category>() } }
    public prop tags: DbSet<Tag> { get() { this.set<Tag>() } }
    public prop postTags: DbSet<PostTag> { get() { this.set<PostTag>() } }
    public prop stars: DbSet<Star> { get() { this.set<Star>() } }
    public prop follows: DbSet<Follow> { get() { this.set<Follow>() } }

    // 视图
    public prop vBlogPosts: DbSet<VBlogPost> { get() { this.set<VBlogPost>() } }
    public prop vComments: DbSet<VComment> { get() { this.set<VComment>() } }
    public prop vUserProfiles: DbSet<VUserProfile> { get() { this.set<VUserProfile>() } }
    // ... 其他视图
}

为什么大量使用视图

这个项目里,读操作几乎全部查视图,不查原始表

看看 v_blog_post 视图的定义(简化版):

sql
CREATE VIEW v_blog_post AS
SELECT
    p.*,
    c.code AS category_code,
    c.label AS category_label,
    u.username AS author_username,
    u.avatar AS author_avatar,
    COALESCE(s.star_count, 0) AS stars,
    COALESCE(cm.comment_count, 0) AS comments
FROM blog_post p
JOIN category c ON c.id = p.category_id
JOIN user u ON u.id = p.author_id
LEFT JOIN (SELECT post_id, COUNT(*) AS star_count FROM star GROUP BY post_id) s ...
LEFT JOIN (SELECT post_id, COUNT(*) AS comment_count FROM comment GROUP BY post_id) cm ...

一个视图就把文章 + 分类 + 作者 + 统计全部 join 好了。对应的仓颉视图实体:

cangjie
// VBlogPost.cj — 代码生成
public class VBlogPost <: Entity {
    // 文章字段
    public mut prop title: String
    public mut prop slug: String
    public mut prop content: String
    // 分类字段(来自 join)
    public mut prop categoryCode: String
    public mut prop categoryLabel: String
    public mut prop categoryColor: String
    // 作者字段(来自 join)
    public mut prop authorId: Int64
    public mut prop authorUsername: String
    public mut prop authorAvatar: String
    // 统计字段(来自子查询)
    public mut prop stars: Int64
    public mut prop comments: Int64
}

好处是应用层代码完全不用写 join 和 count。对于内容展示型项目(博客、CMS),这是效率最高的方式。如果视图查询变慢(数据量大以后),加索引或升级为物化视图即可。

whereIf 的两种用法

项目里 whereIf 有两种写法,用在不同场景:

固定条件(始终生效):

cangjie
.whereIf("is_public = 1")   // 只要一个参数,始终添加

条件参数(有值才生效):

cangjie
.whereIf("category_code = @categoryCode", model.categoryCode.isSome())
//        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^    ^^^^^^^^^^^^^^^^^^^^^^^^^^
//        SQL 条件                           这个条件是否生效

第二个参数是 Boolmodel.categoryCode.isSome() 返回 true 时条件才拼进 SQL。这意味着调用方不传分类,后端就不按分类筛选。

DbParameters 什么时候用

简单场景直接用字符串插值:

cangjie
.whereIf("id = ${id}")         // id 是整数,无注入风险

复杂场景用参数化:

cangjie
let param = DbParameters()
param.add("slug", slug)
context.vBlogPosts.params(param).whereIf("slug = @slug")   // slug 是字符串,防注入

规则:整数可以直接 ${} 拼接,字符串必须通过 @参数 绑定的方式防 SQL 注入。

DB-First 代码生成

实体不是手写的,是 sqlsharp_utils 工具从数据库反向生成的。生成入口是 entity.bat

bash
cd web-api
./entity.bat

生成的文件分布在三个目录:

目录来源用途
domain/entities/数据库表CRUD 操作
domain/views/数据库视图读操作(列表、详情)
domain/models/表结构传给客户端的 DTO

表结构变了就重跑——不要手改生成的文件。这是数据库驱动项目的标准工程习惯。

常见问题

ConnectionStrings 里为什么用 mariadb 协议

连接字符串的 scheme 是 mariadb://,不是 mysql://options.useMysql(connectionString, "mariadb") 里的第二个参数 "mariadb" 指定了驱动类型。这是 SQLSharp 的连接方式。

视图和表在代码里怎么区分

表实体(BlogPost)用于增删改操作,视图实体(VBlogPost)用于查询。区分方式就是类名前缀 V——这是代码生成工具的约定。

什么时候用 params,什么时候不用

需要传变量参数时用 params(param) + @参数名 占位。固定条件直接写在字符串里不需要 params:

cangjie
// 需要 params
let param = DbParameters()
param.add("authorId", authorId)
context.blogPosts.params(param).whereIf("author_id = @authorId")

// 不需要 params
context.blogPosts.whereIf("is_public = 1")

延伸阅读