2026/5/18 23:04:29
网站建设
项目流程
南宁网站建设怎么样,wordpress文章推荐系统,汉字域名网站,wordpress主题放在哪个文件夹数据查询进阶(上)#xff1a;作者热度榜报表与 Dapper 的“可控查询”
哈喽#xff0c;我是黑棠 在CRUD中#xff0c;通用仓储 IRepositoryT EF Core 的查询表达式#xff0c;足以覆盖列表、详情、简单筛选。 但在“报表类查询”里#xff0c;常见的痛点不是写不…数据查询进阶(上)作者热度榜报表与 Dapper 的“可控查询”哈喽我是黑棠在CRUD中通用仓储IRepositoryT 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.cspublicclassAuthorHotRankItem{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.cspublicinterfaceIBookRepository:IRepositoryBook,Guid{TaskListBookGetListByPriceAsync(floatminPrice,floatmaxPrice);TaskListAuthorHotRankItemGetAuthorHotRankAsync(floatminPrice,floatmaxPrice,inttop,intmaxWorksPerAuthor);}3. 在 EfCoreRepository 中用 Dapper 实现报表查询这个报表很容易写成循环查询“先查作者 TopN再逐个作者查作品”的形式但那会把查询次数放大为 1 N下一章会专门拆解这个坑。这里采用两段式查询先用聚合拿到 TopN 作者只返回 AuthorId再一次性拉取这些作者在区间内的作品明细在内存里组装“作品列表 最新作品”Lecture 07/src/Acme.BookStore.EntityFrameworkCore/Books/EfCoreBookRepository.cspublicasyncTaskListAuthorHotRankItemGetAuthorHotRankAsync(floatminPrice,floatmaxPrice,inttop,intmaxWorksPerAuthor){vardbContextawaitGetDbContextAsync();varconnectiondbContext.Database.GetDbConnection();vartransactiondbContext.Database.CurrentTransaction?.GetDbTransaction();vartopAuthorsSql SELECT b.AuthorIdASAuthorId,COUNT(*)ASBookCount,MAX(b.PublishDate)ASLatestPublishDateFROMAppBooksb WHERE b.PriceMinPrice AND b.PriceMaxPrice GROUP BY b.AuthorIdORDER BYBookCountDESC,LatestPublishDateDESC LIMIT Top;vartopAuthors(awaitconnection.QueryAsyncTopAuthorRow(topAuthorsSql,new{MinPriceminPrice,MaxPricemaxPrice,Toptop},transaction:transaction)).AsList();if(topAuthors.Count0){return[];}varauthorIdstopAuthors.Select(xx.AuthorId).ToArray();varrankByAuthorIdtopAuthors.Select((x,index)new{x.AuthorId,index}).ToDictionary(xx.AuthorId,xx.index);vardetailsSql SELECT a.IdASAuthorId,a.NameASAuthorName,b.NameASBookName,b.PublishDateASPublishDateFROMAppAuthorsa INNER JOINAppBooksb ON b.AuthorIda.IdWHERE b.PriceMinPrice AND b.PriceMaxPrice AND a.IdANY(AuthorIds)ORDER BY a.Id,b.PublishDateDESC,b.Name;vardetails(awaitconnection.QueryAsyncAuthorBookRow(detailsSql,new{MinPriceminPrice,MaxPricemaxPrice,AuthorIdsauthorIds},transaction:transaction)).AsList();varresultdetails.GroupBy(xnew{x.AuthorId,x.AuthorName}).Select(g{varorderedBooksg.OrderByDescending(xx.PublishDate).ThenBy(xx.BookName).ToList();returnnewAuthorHotRankItem{AuthorIdg.Key.AuthorId,AuthorNameg.Key.AuthorName,LatestWorkorderedBooks.FirstOrDefault()?.BookName??string.Empty,Worksstring.Join( / ,orderedBooks.Select(xx.BookName).Take(maxWorksPerAuthor))};}).OrderBy(xrankByAuthorId[x.AuthorId]).ToList();returnresult;}这段实现的关键点是连接来自DbContext.Database.GetDbConnection()事务来自CurrentTransaction因此不会破坏工作单元边界。为了让示例更聚焦在“查询形态”上面的代码省略了TopAuthorRow/AuthorBookRow这两个用于 Dapper 映射的内部类型在仓储实现里它们只是承载AuthorId/BookCount/PublishDate等字段的简单模型。如果你把这条查询投入生产通常还需要补齐两件事索引策略至少保证AppBooks(Price)、AppBooks(AuthorId, PublishDate)这类组合索引覆盖筛选与排序方向否则 TopN 聚合容易退化成全表扫描。方言细节本示例基于 PostgreSQLANY(AuthorIds)能把参数数组下推到数据库侧如果换成 SQL Server/MySQL需要把“IN 列表”写法和参数绑定方式一起调整。4. 提供应用服务接口在应用层我们把报表封装成一个明确的 API输入是价格区间与 TopN输出是“作者热度榜”的展示字段。Lecture 07/src/Acme.BookStore.Application.Contracts/Books/GetAuthorHotRankInput.cspublicclassGetAuthorHotRankInput{[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.cspublicclassAuthorHotRankDto{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节选publicasyncTaskListResultDtoAuthorHotRankDtoGetAuthorHotRankAsync(GetAuthorHotRankInputinput){if(input.MaxPriceinput.MinPrice){thrownewUserFriendlyException(MaxPrice must be greater than or equal to MinPrice.);}varitemsawait_bookRepository.GetAuthorHotRankAsync(input.MinPrice,input.MaxPrice,input.Top,input.MaxWorksPerAuthor);varpriceRange${input.MinPrice:0.##}-{input.MaxPrice:0.##};vardtositems.Select(xnewAuthorHotRankDto{AuthorNamex.AuthorName,Worksx.Works,LatestWorkx.LatestWork,PriceRangepriceRange}).ToList();returnnewListResultDtoAuthorHotRankDto(dtos);}四、把“报表查询”做成可维护资产把报表放进仓储不是为了“追求 Dapper”而是为了把它当成一种可维护资产SQL 可见且可审计便于用 EXPLAIN/慢查询日志定位问题接口契约稳定规则变更集中在一处而不是散落在多个应用服务里你可以围绕这条查询做更工程化的治理索引、缓存、压测基线、回归用例下一章会继续用这个报表讨论两个更容易被忽略的问题规则复用规约与 N1查询次数失控。本文首发于CSDN[黑棠会长]转载请注明来源。关注我一起用轻松的方式读懂前沿科技。