news 2026/5/1 4:23:54

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/5/1 5:49:04

Java遇上AI:JBoltAI框架让智能开发更简单

在科技飞速发展的今天&#xff0c;人工智能&#xff08;AI&#xff09;已经渗透到我们生活的方方面面。而对于广大的Java开发者来说&#xff0c;如何利用自己熟悉的Java语言&#xff0c;搭上AI这趟快车&#xff0c;成为了一个值得探讨的话题。今天&#xff0c;我们就来聊聊JBol…

作者头像 李华
网站建设 2026/5/1 8:15:03

Django+vue音乐热度数据分析及音乐推荐系统

标题&#xff1a;Djangovue音乐热度数据分析及音乐推荐系统文档介绍&#xff1a;绪论1.1研究背景与意义在数字化时代&#xff0c;音乐作为一种重要的文化娱乐形式&#xff0c;已经渗透到人们生活的方方面面。随着网络技术的快速发展&#xff0c;音乐资源日益丰富&#xff0c;音…

作者头像 李华
网站建设 2026/5/1 4:53:24

火山引擎记忆库Memo发布

火山引擎记忆库Memo&#xff08;官方名称为火山引擎记忆库 Mem0&#xff09;已于2026年2月2日正式发布&#xff0c;这是火山引擎推出的AI应用记忆系统托管服务&#xff0c;旨在为智能应用提供长期记忆能力&#xff0c;解决大模型"上下文窗口局限"和"会话记忆清零…

作者头像 李华
网站建设 2026/5/1 4:53:00

数据库性能优化

一、优化核心思路&#xff1a;分层递进策略优化层次优化手段预期收益实施难度SQL与索引​慢查询分析、索引优化性能提升50%-90%低&#xff08;见效快&#xff09;架构设计​分库分表、读写分离提升数倍至数十倍中&#xff08;需改造&#xff09;参数配置​内存分配、连接池调优…

作者头像 李华
网站建设 2026/5/1 4:53:51

【收藏】一文搞懂LLM、RAG、Agent、MCP的关系,AI技术路线不再迷茫!

文章解析了AI四大核心概念的关系&#xff1a;LLM是基础智商&#xff0c;RAG解决知识过时问题&#xff0c;Agent实现主动思考&#xff0c;MCP打通外部工具调用。四者协同使AI从对话玩具进化为能解决问题的数字员工&#xff0c;通过投资分析实例展示了它们如何配合完成复杂任务&a…

作者头像 李华