news 2026/4/15 18:31:21

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/4/13 20:44:14

孤能子视角:“智慧物流“

第一步&#xff1a;分析“作者”——智慧物流体系&#xff08;作为“供应链生命共同体”的超级孤能子&#xff09;启动&#xff1a;三力逼问&#xff0c;定位张力1. 零预设&#xff1a;不预设智慧物流是“运输自动化”或“信息化”&#xff0c;视其为全球贸易与数字技术双重浪潮…

作者头像 李华
网站建设 2026/4/9 22:09:43

别再用老话术!工厂老板吃这套沟通技巧

"我们质量好、价格优、服务到位",这类话术啊,工厂老板早就听腻了&#xff0c;他每天都被无数销售包围着,真正能让他愿意停下脚步的,不是你的产品有多厉害,而是你懂他的生意、能帮他算清账、还能解决他的难题。一、开口不谈"我",先说"你不好的话术: &q…

作者头像 李华
网站建设 2026/4/8 23:34:36

制造大数据应用在哪些工业场景中最值得推荐?

一家汽车焊装车间里&#xff0c;报警灯突然闪烁。某条焊缝的电阻值比历史平均值偏了0.3欧姆——这数字大多数人压根看不懂&#xff0c;系统却已经推送了三套方案&#xff1a;调焊枪压力、换电极帽、查夹具磨损。工程师没打电话&#xff0c;也没翻手册&#xff0c;只是在手机上敲…

作者头像 李华