news 2026/8/20 19:44:32

ABP框架07.在 DDD中把查询作为可演进的契约

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ABP框架07.在 DDD中把查询作为可演进的契约

数据查询进阶(上):作者热度榜报表与 Dapper 的“可控查询”

哈喽,我是黑棠
在CRUD中,通用仓储IRepository<T>+ EF Core 的查询表达式,足以覆盖列表、详情、简单筛选。
但在“报表类查询”里,常见的痛点不是写不出来,而是不可控:SQL 形态难以预测、性能难以定位、规则容易被复制粘贴到多个入口

这一章用一个更贴近业务的例子,说明什么时候应该把查询从 LINQ 世界里抽离出来,收敛到自定义仓储,用 Dapper 做“可审计、可调优、可复用”的实现。

一、报表需求:作者热度榜

假设运营侧要一个“作者热度榜”页面,用于观察某个定价区间里哪些作者更活跃。页面展示字段如下:

  • 作者:作者名
  • 作品:作品A / 作品B / 作品C(展示最近 3 本)
  • 单价区间:20-50¥(作为筛选条件,同时在报表上显示)
  • 最新作品:作品C

热度的口径在这里先简化为:指定价格区间内,该作者作品数量越多越“热”。口径看似简单,但它天然会牵扯跨表 JOIN、聚合、TopN、二次取数(拿到作者集合后再取作品明细)等形态,已经超出了“标准 CRUD”那套查询模式。

二、仓储的边界:把查询当成可演进的契约

在 DDD 语境里,仓储不仅是“封装数据库访问”,更关键的是把查询能力做成领域的接口契约,避免 SQL/表名/数据库语法扩散到应用层。

当你需要满足下面任意一种特征时,自定义仓储通常比在应用服务里拼 LINQ 更稳定:

  • 查询跨聚合或明显偏离 CRUD(报表、统计、窗口函数/CTE、TopN)
  • 查询只读且高频,希望执行计划和索引策略可控
  • 查询需要数据库特性(聚合函数、JSON、全文检索等)

三、实现路径:Dapper + ABP 工作单元

目标是两点同时成立:

  • 复杂查询写成可控的 SQL(便于分析、压测和调优)
  • 连接与事务仍然复用 ABP/EF Core 的工作单元边界(不“偷跑”到另一条连接上)

1. 定义报表返回模型

把“报表返回形态”定义成一个明确的模型(这是查询契约的一部分)。

Lecture 07/src/Acme.BookStore.Domain/Books/AuthorHotRankItem.cs

publicclassAuthorHotRankItem{publicGuidAuthorId{get;set;}publicstringAuthorName{get;set;}=string.Empty;publicstringWorks{get;set;}=string.Empty;publicstringLatestWork{get;set;}=string.Empty;}

2. 扩展领域仓储接口

Lecture 07/src/Acme.BookStore.Domain/Books/IBookRepository.cs

publicinterfaceIBookRepository:IRepository<Book,Guid>{Task<List<Book>>GetListByPriceAsync(floatminPrice,floatmaxPrice);Task<List<AuthorHotRankItem>>GetAuthorHotRankAsync(floatminPrice,floatmaxPrice,inttop,intmaxWorksPerAuthor);}

3. 在 EfCoreRepository 中用 Dapper 实现报表查询

这个报表很容易写成循环查询“先查作者 TopN,再逐个作者查作品”的形式,但那会把查询次数放大为 1 + N(下一章会专门拆解这个坑)。

这里采用两段式查询:

  1. 先用聚合拿到 TopN 作者(只返回 AuthorId)
  2. 再一次性拉取这些作者在区间内的作品明细,在内存里组装“作品列表 + 最新作品”

Lecture 07/src/Acme.BookStore.EntityFrameworkCore/Books/EfCoreBookRepository.cs

publicasyncTask<List<AuthorHotRankItem>>GetAuthorHotRankAsync(floatminPrice,floatmaxPrice,inttop,intmaxWorksPerAuthor){vardbContext=awaitGetDbContextAsync();varconnection=dbContext.Database.GetDbConnection();vartransaction=dbContext.Database.CurrentTransaction?.GetDbTransaction();vartopAuthorsSql=""" SELECT b."AuthorId"AS"AuthorId",COUNT(*)AS"BookCount",MAX(b."PublishDate")AS"LatestPublishDate"FROM"AppBooks"b WHERE b."Price">=@MinPrice AND b."Price"<=@MaxPrice GROUP BY b."AuthorId"ORDER BY"BookCount"DESC,"LatestPublishDate"DESC LIMIT @Top""";vartopAuthors=(awaitconnection.QueryAsync<TopAuthorRow>(topAuthorsSql,new{MinPrice=minPrice,MaxPrice=maxPrice,Top=top},transaction:transaction)).AsList();if(topAuthors.Count==0){return[];}varauthorIds=topAuthors.Select(x=>x.AuthorId).ToArray();varrankByAuthorId=topAuthors.Select((x,index)=>new{x.AuthorId,index}).ToDictionary(x=>x.AuthorId,x=>x.index);vardetailsSql=""" SELECT a."Id"AS"AuthorId",a."Name"AS"AuthorName",b."Name"AS"BookName",b."PublishDate"AS"PublishDate"FROM"AppAuthors"a INNER JOIN"AppBooks"b ON b."AuthorId"=a."Id"WHERE b."Price">=@MinPrice AND b."Price"<=@MaxPrice AND a."Id"=ANY(@AuthorIds)ORDER BY a."Id",b."PublishDate"DESC,b."Name"""";vardetails=(awaitconnection.QueryAsync<AuthorBookRow>(detailsSql,new{MinPrice=minPrice,MaxPrice=maxPrice,AuthorIds=authorIds},transaction:transaction)).AsList();varresult=details.GroupBy(x=>new{x.AuthorId,x.AuthorName}).Select(g=>{varorderedBooks=g.OrderByDescending(x=>x.PublishDate).ThenBy(x=>x.BookName).ToList();returnnewAuthorHotRankItem{AuthorId=g.Key.AuthorId,AuthorName=g.Key.AuthorName,LatestWork=orderedBooks.FirstOrDefault()?.BookName??string.Empty,Works=string.Join(" / ",orderedBooks.Select(x=>x.BookName).Take(maxWorksPerAuthor))};}).OrderBy(x=>rankByAuthorId[x.AuthorId]).ToList();returnresult;}

这段实现的关键点是:连接来自DbContext.Database.GetDbConnection(),事务来自CurrentTransaction,因此不会破坏工作单元边界。

为了让示例更聚焦在“查询形态”,上面的代码省略了TopAuthorRow/AuthorBookRow这两个用于 Dapper 映射的内部类型;在仓储实现里它们只是承载AuthorId/BookCount/PublishDate等字段的简单模型。

如果你把这条查询投入生产,通常还需要补齐两件事:

  • 索引策略:至少保证AppBooks(Price)AppBooks(AuthorId, PublishDate)这类组合索引覆盖筛选与排序方向,否则 TopN 聚合容易退化成全表扫描。
  • 方言细节:本示例基于 PostgreSQL,ANY(@AuthorIds)能把参数数组下推到数据库侧;如果换成 SQL Server/MySQL,需要把“IN 列表”写法和参数绑定方式一起调整。

4. 提供应用服务接口

在应用层,我们把报表封装成一个明确的 API:输入是价格区间与 TopN,输出是“作者热度榜”的展示字段。

Lecture 07/src/Acme.BookStore.Application.Contracts/Books/GetAuthorHotRankInput.cs

publicclassGetAuthorHotRankInput{[Range(0,double.MaxValue)]publicfloatMinPrice{get;set;}=20;[Range(0,double.MaxValue)]publicfloatMaxPrice{get;set;}=50;[Range(1,1000)]publicintTop{get;set;}=10;[Range(1,20)]publicintMaxWorksPerAuthor{get;set;}=3;}

Lecture 07/src/Acme.BookStore.Application.Contracts/Books/AuthorHotRankDto.cs

publicclassAuthorHotRankDto{publicstringAuthorName{get;set;}=string.Empty;publicstringWorks{get;set;}=string.Empty;publicstringPriceRange{get;set;}=string.Empty;publicstringLatestWork{get;set;}=string.Empty;}

Lecture 07/src/Acme.BookStore.Application/Books/BookAppService.cs(节选):

publicasyncTask<ListResultDto<AuthorHotRankDto>>GetAuthorHotRankAsync(GetAuthorHotRankInputinput){if(input.MaxPrice<input.MinPrice){thrownewUserFriendlyException("MaxPrice must be greater than or equal to MinPrice.");}varitems=await_bookRepository.GetAuthorHotRankAsync(input.MinPrice,input.MaxPrice,input.Top,input.MaxWorksPerAuthor);varpriceRange=$"{input.MinPrice:0.##}-{input.MaxPrice:0.##}¥";vardtos=items.Select(x=>newAuthorHotRankDto{AuthorName=x.AuthorName,Works=x.Works,LatestWork=x.LatestWork,PriceRange=priceRange}).ToList();returnnewListResultDto<AuthorHotRankDto>(dtos);}

四、把“报表查询”做成可维护资产

把报表放进仓储,不是为了“追求 Dapper”,而是为了把它当成一种可维护资产

  • SQL 可见且可审计,便于用 EXPLAIN/慢查询日志定位问题
  • 接口契约稳定,规则变更集中在一处(而不是散落在多个应用服务里)
  • 你可以围绕这条查询做更工程化的治理:索引、缓存、压测基线、回归用例

下一章会继续用这个报表,讨论两个更容易被忽略的问题:规则复用(规约)与 N+1(查询次数失控)


本文首发于CSDN:[黑棠会长],转载请注明来源。
关注我,一起用轻松的方式读懂前沿科技。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/19 18:11:24

如何从Target平台获取搜索列表数据的API接口

在现代Web开发中&#xff0c;API&#xff08;应用程序接口&#xff09;是实现平台数据交互的核心工具。本文将以Target平台为例&#xff0c;详细介绍如何通过其API接口获取搜索列表数据。Target平台提供了一个RESTful API&#xff0c;允许开发者查询关键词相关的搜索结果&#…

作者头像 李华
网站建设 2026/8/4 18:26:49

Apple生态自动化理想之选!亚马逊云科技Mac实例一键部署OpenClaw

企业或团队真正需要的&#xff0c;不是一个“会聊天的AI玩具”&#xff0c;而是一个能在云端持续工作、每个操作都留痕可查、无缝融入现有沟通工具的生产力助手。OpenClaw&#xff08;原名Clawdbot、Moltbot&#xff09;火了之后&#xff0c;大家发现&#xff1a;部署方式直接决…

作者头像 李华
网站建设 2026/8/17 19:55:50

AIGS范式革命:Java企业智能化转型的核心路径

在人工智能技术的演进中&#xff0c;从AIGC&#xff08;人工智能生成内容&#xff09;到AIGS&#xff08;人工智能生成服务&#xff09;的跨越&#xff0c;标志着AI从“辅助性内容工具”升级为“系统性服务重塑者”。对于长期以Java技术栈为核心架构的企业而言&#xff0c;传统…

作者头像 李华
网站建设 2026/8/4 18:27:44

第21届全国大学生智能汽车竞赛提问与回答:飞跃雷区组别

第二十一届全国大学生智能汽车竞赛比赛规则第21届全国大学生智能汽车竞赛提问于回答&#xff08;总入口&#xff09;第21届智能车竞赛飞跃雷区组比赛车模与比赛场地说明 01 提问与回答 2月份 序号提问回答备注1卓大&#xff0c;飞越雷区中用于驱动车辆识别的led的芯片必须是用…

作者头像 李华
网站建设 2026/8/19 18:06:17

深度学习实战119-AI大模型+Agent重塑医疗行业:智能诊疗助手项目完整代码实战

文章目录 引言:从一次误诊经历说起 项目背景与需求分析 医疗AI的现状与挑战 项目目标与功能规划 技术架构设计 环境准备与依赖安装 核心Agent架构实现 基础组件定义 医疗领域提示模板设计 Agent状态管理 医疗知识库构建与检索 知识库架构设计 知识库数据准备 Agent工具系统实现…

作者头像 李华