仓储层实战
上一篇你理解了整体架构。这一篇专门讲一个关键的工程决策:为什么要在 DbContext 之上加一层 Repository,以及怎么加。
先看问题:没有仓储层的代码长什么样
假设你正在写博客项目的 BlogQueries 和 BlogService。目前代码直接依赖 AdminDbContext:
// BlogQueries.cj — 直接操作 DbContext
public class BlogQueries <: Queries {
public BlogQueries(let context: AdminDbContext, let userAccessor: UserAccessor) {}
public func getBlogPostList(model: BlogPostSearchModel): PageResult<VBlogPost> {
var (list, total) = context
.vBlogPosts
.params(model)
.whereIf("is_public = 1")
.whereIf("author_id = @authorId", model.authorId.isSome())
.toPageResult(model)
return this.data(list, total)
}
}
// UserService.cj — 也在直接操作 DbContext
public class UserService {
public UserService(let context: AdminDbContext) {}
public func getUserPosts(authorId: Int64): ArrayList<BlogPost> {
let param = DbParameters()
param.add("authorId", authorId)
return context.blogPosts
.params(param)
.whereIf("author_id = @authorId AND is_public = 1")
.toList()
}
}这里的 "author_id = @authorId AND is_public = 1" 已经在两个地方各写了一遍。如果你还有一个管理后台接口只查未公开文章,还得再写一遍。当"查询条件"散落在多个类里时,字段名一旦变化,你要全局搜索替换。
这就是仓储层要解决的第一个问题:把数据访问逻辑收拢到一处。
什么是仓储层
仓储(Repository)就是在 DbContext 和业务代码之间加的一层。它不是替代 DbContext,而是封装重复的查询模板。
原来:Queries / Service → DbContext → 数据库
现在:Queries / Service → Repository → DbContext → 数据库Repository 做的事情很简单:
- 把
whereIf("author_id = @authorId AND is_public = 1")这种查询条件写成一个方法 - 所有需要"查某个用户的公开文章"的地方,都调这个方法
第1步:定义仓储接口
先从最简单的开始。你只需要把当前散落在各处的数据访问操作,按表分组,抽象成接口:
// infrastructure/repositories/IBlogRepository.cj
package admin.infrastructure.repositories
import std.collection.*
import admin.domain.entities.*
public interface IBlogRepository {
// 查
func findPublishedByAuthor(authorId: Int64): ArrayList<BlogPost>
func findDetailById(id: Int64): ?BlogPost
// 增
func add(post: BlogPost): Unit
// 改
func update(post: BlogPost): Unit
// 删
func remove(id: Int64): Unit
}接口方法命名用业务语言,不用数据库术语:
findPublishedByAuthor而不是queryWhereAuthorIdAndIsPublicremove而不是deleteById
第2步:实现仓储
接口有了,实现类就是把原来散落在 Queries 和 Service 里的数据访问代码搬过来:
// infrastructure/repositories/BlogRepository.cj
package admin.infrastructure.repositories
import sqlsharp.*
import std.collection.*
import admin.domain.entities.*
import admin.infrastructure.AdminDbContext
public class BlogRepository <: IBlogRepository {
private let context: AdminDbContext
public init(context: AdminDbContext) {
this.context = context
}
// 查某个作者的公开文章 —— 查询条件只写这一次
public func findPublishedByAuthor(authorId: Int64): ArrayList<BlogPost> {
let param = DbParameters()
param.add("authorId", authorId)
return context.blogPosts
.params(param)
.whereIf("author_id = @authorId AND is_public = 1")
.orderByDesc("created_at")
.toList()
}
// 按 id 查文章详情
public func findDetailById(id: Int64): ?BlogPost {
return context.blogPosts.find(id)
}
// 新增
public func add(post: BlogPost): Unit {
context.blogPosts.add(post)
}
// 更新
public func update(post: BlogPost): Unit {
context.blogPosts.update(post)
}
// 删除(软约定:传带 id 的实体即可)
public func remove(id: Int64): Unit {
let entity = BlogPost()
entity.id = id
context.blogPosts.remove(entity)
}
}第3步:注册到 DI 容器
在 InfrastructureModule 里注册接口和实现:
// InfrastructureModule.cj — 新增两行
public class InfrastructureModule <: Module {
public override func configureServices(context: ServiceConfigurationContext): Unit {
// ... 原有的 DbContext 注册 ...
// 注册仓储 —— 接口对实现,方便以后替换
context.services.addScoped<IBlogRepository, BlogRepository>()
context.services.addScoped<IUserRepository, UserRepository>()
context.services.addScoped<ICommentRepository, CommentRepository>()
}
}用 addScoped 是因为 Repository 依赖 DbContext(也是 Scoped),它们生命周期一致:一次请求一个实例。
第4步:改造业务代码
原来 BlogQueries 和 UserService 直接注 AdminDbContext,现在改为注 IBlogRepository:
// BlogQueries.cj — 改造后
public class BlogQueries <: Queries {
let blogRepo: IBlogRepository
public init(blogRepo: IBlogRepository, let userAccessor: UserAccessor) {
this.blogRepo = blogRepo
}
public func getBlogPostList(model: BlogPostSearchModel): PageResult<VBlogPost> {
// 列表查询仍然可以直接用 DbContext 视图(视图查询不经过仓储也合理)
// 或者你也可以在 IBlogRepository 上加 findPaged(model) 方法
// ... 视情况而定
}
}
// UserService.cj — 改造后,重复代码消失
public class UserService {
let blogRepo: IBlogRepository
public init(blogRepo: IBlogRepository) {
this.blogRepo = blogRepo
}
public func getUserPosts(authorId: Int64): ArrayList<BlogPost> {
return blogRepo.findPublishedByAuthor(authorId) // 一行搞定
}
}对比一下改之前和改之后 UserService.getUserPosts 的代码量:
// 改之前:5 行,SQL 条件硬编码
let param = DbParameters()
param.add("authorId", authorId)
return context.blogPosts
.params(param)
.whereIf("author_id = @authorId AND is_public = 1")
.toList()
// 改之后:1 行,语义清晰
return blogRepo.findPublishedByAuthor(authorId)仓储层的三个收益
1. 查询条件只写一次
"author_id = @authorId AND is_public = 1" 只存在于 BlogRepository.findPublishedByAuthor 这一个方法里。哪天 is_public 字段改成 status,你只改一个地方。
2. 测试可以替换数据访问
没有仓储时,要测 UserService 必须连真实数据库:
// ❌ 测试时:必须起一个 MariaDB
let context = AdminDbContext(...)
let service = UserService(context)
// 要测 getUserPosts,得先往数据库里插数据有仓储后,你可以写一个假的实现来测:
// ✅ 测试时:用假仓储,不连数据库
class FakeBlogRepository <: IBlogRepository {
public func findPublishedByAuthor(authorId: Int64): ArrayList<BlogPost> {
// 直接返回假数据
let post = BlogPost()
post.title = "测试文章"
return [post]
}
// ...
}
let repo = FakeBlogRepository()
let service = UserService(repo)
// 现在可以稳定地、快速地测试 UserService 的逻辑3. 换数据库成本更低
换数据库时,只需要实现一套新的 Repository。业务代码(Queries、Service、Controller)一行不动。
针对 MariaDB:BlogRepository → 改 SQL 方言
针对 OpenGauss:BlogRepositoryForOpenGauss → 用 PG 语法
业务代码:完全不变不是所有查询都要走仓储
仓储层也有边界。一个常见问题是:列表分页查询要不要走仓储?
建议这样分:
| 查询类型 | 走不走仓储 | 原因 |
|---|---|---|
| 按固定条件查单条/集合 | 走仓储 | 条件固定、重复使用 |
| 动态筛选的列表分页 | 直接走 Queries + DbContext | 条件变化多,仓储方法参数会爆炸 |
| 聚合统计 | 走仓储 | 统计逻辑集中 |
| 简单 FindById | 走仓储 | 太基础了,写一次到处用 |
// ✅ 适合走仓储:条件固定
blogRepo.findPublishedByAuthor(authorId)
userRepo.findByUsername(username)
// ✅ 直接走 Queries:条件动态变化
context.vBlogPosts
.params(model)
.whereIf("POSITION(@search IN title) > 0", model.search.isSome())
.whereIf("category_code = @categoryCode", model.categoryCode.isSome())
.whereIf("id IN (SELECT pt.post_id FROM post_tag ... )", model.tagName.isSome())
.toPageResult(model)后者的 whereIf 条件完全由调用方传参决定,可能有搜索、可能没有、可能按分类、可能按标签,组合太多。这种场景强塞进仓储只会让接口变得臃肿。
从零开始加仓储的步骤
把这个过程总结成一个可操作的清单:
| 步骤 | 做什么 | 产出 |
|---|---|---|
| 1 | 梳理现有代码里重复的数据访问模式 | 找出被写了两次以上的查询条件 |
| 2 | 按表定义仓储接口 | IBlogRepository、IUserRepository |
| 3 | 实现仓储,把重复代码搬进去 | BlogRepository、UserRepository |
| 4 | 在 Module 里注册接口和实现 | addScoped<IBlogRepository, BlogRepository>() |
| 5 | 改 Queries/Service 的构造函数,注入接口 | init(blogRepo: IBlogRepository) |
| 6 | 删掉原来散落的重复数据访问代码 | 享受"只改一处"的自由 |