Repository 仓储,你的归宿究竟在哪?(三)-SELECT 某某某。。。
写在前面
首先,本篇博文主要包含两个主题:
- 领域服务中使用仓储
- SELECT 某某某(有点晕?请看下面。)
上一篇:Repository 仓储,你的归宿究竟在哪?(二)-这样的应用层代码,你能接受吗?
关于仓储这个系列,很多园友问我:为什么纠结仓储?我觉得需要再次说明下(请不要再“纠结”了),引用上一篇博文中某一段评论的回复:
关于“纠结于仓储”这个问题,其实博文中我就有说明,不是说我纠结或是陷入这个问题,而是我觉得在实践领域驱动设计中,仓储的调用是一个很重要的东西,如果使用的不恰当,也许就像上面我所贴出来的应用层代码一样,我个人觉得,这是很多人在实践领域驱动设计中,很容易踩的一个坑,我只是希望可以把这个过程分享出来,给有相同困惑的人,可以借鉴一下。
领域服务和仓储的两种“微妙关系”
这边的“领域服务”和仓储的关系,可以理解为在领域中调用仓储,具体表现为在领域服务中使用。
在很久之前,我为了保持所谓的“领域纯洁”,在领域服务设计的时候,没有参杂仓储任何的调用,但是随着应用程序的复杂,很多业务添加进来,一个单纯的“业务描述”并不能真正去实现业务用例,所以这时候的领域服务就被“架空”了,一些业务实现“迫不得已”放在了应用层,也就是上一篇我所贴出的应用层代码,不知道你能不能接受?反正我是接受不了,所以我做了一些优化,领域服务中调用了仓储。
关于领域服务中调用仓储,在上一篇博文讨论中(czcz1024、Jesse Liu、netfocus、刘标才...),主要得出两种实现方式,这边我再大致总结下:
- 传统方式:仓储接口定义在领域层,实现在基础层,通过规约来约束查询,一般返回类型为聚合根集合对象,如果领域对象的查询逻辑比较多,具体体现就是仓储接口变多。
- IQueryable 方式:和上面不同的是接口的设计变少了,因为返回类型为 IQueryable,具体查询表达式的组合放在了调用层,也就是领域服务中,比如:xxxRepository.GetAll().Where(x=>....)
其实这两种方式都是一把双刃剑,关键在于自己根据具体的业务场景进行选择了,我说一下我的一些理解,比如现实生活中车库的场景,我们可以把车库看作是仓储,取车的过程看作是仓储的调用,车子的摆放根据汽车的规格,也就是仓储中的规约概念,比如我今天要开一辆德系、红色、敞篷、双门的跑车(条件有点多哈),然后我就去车库取车,在车库的“调度系统“(在仓储的具体表现,可以看作是 EF)中输入这些命令,然后一辆兰博基尼就出现在我的眼前了。
在上面描述的现实场景中,如果是第一种传统方式,“我要开一辆德系、红色、敞篷、双门的跑车”这个就可以设计为仓储的一个接口,为什么?因为车库可以换掉,而这些业务用例一般不会进行更改,车库中的“调度系统”根据命令是如何寻找汽车的呢?答案是规格的组合,也就是仓储中规约的组合,我们在针对具体业务场景设计的时候,一般会提炼出这个业务场景中的规约,这个也是不可变的,根据命令来进行对这些规约的组合,这个过车的具体体现就是仓储的实现,约束的是聚合根对象。这种方式中,我个人认为好处是可以充分利用规约,仓储的具体调用统一管理,让调用者感觉不到它是如何工作的,因为它只需要传一个命令过去,就可以得到想要的结果,唯一不好的地方就是:我心情不好,每天开的汽车都不一样,这个就要死人了,因为我要设计不同的仓储接口来进行对规约的组合。
如果是第二种方式,也就是把“调度系统”的使用权交到自己手里(第一种的这个过程可以看作是通过秘书),这种方式的好与坏,我就不多说了,我现在使用的是第一种方式,主要有两个原因:
- 防止 IQueryable 的滥用(领域服务非常像 DAL)。
- 现在应用场景中的查询比较少,没必要。
上一篇博文中贴出的是,发送短消息的应用层代码,发送的业务验证放在了应用层,以致于 SendSiteMessageService.SendMessage 中只有一段“return true”代码,修改之后的领域服务代码:
public class SendSiteMessageService : ISendMessageService
{
public async Task<bool> SendMessage(Message message)
{
IMessageRepository messageRepository = IocContainer.Resolver.Resolve<IMessageRepository>();
if (message.Type == MessageType.Personal)
{
if (System.Web.HttpContext.Current != null)
{
if (await messageRepository.GetMessageCountByIP(Util.GetUserIpAddress()) > 100)
{
throw new CustomMessageException("一天内只能发送100条短消息");
}
}
if (await messageRepository.GetOutboxCountBySender(message.Sender) > 20)
{
throw new CustomMessageException("1小时内只能向20个不同的用户发送短消息");
}
}
return true;
}
}
代码就是这样,如果你觉得有问题,欢迎提出,我再进行修改。
这边再说一下领域服务中仓储的注入,缘由是我前几天看了刘标才的一篇博文:DDD领域驱动设计之领域服务,文中对仓储的注入方式是通过构造函数,这种方式的坏处就是领域服务对仓储产生强依赖关系,还有就是如果领域服务中注入了多个仓储,调用这个领域服务中的某一个方法,而这个方法只是使用了一个仓储,那么在对这个领域服务进行注入的时候,就必须把所有仓储都要进行注入,这就没有必要了。
解决上面的问题的方式就是,在使用仓储的地方对其进行解析,比如:IocContainer.Resolve<IMessageRepository>();
,这样就可以避免了上面的问题,我们还可以把仓储的注入放在 Bootstrapper 中,也就是项目启动的地方。
SELECT 某某某
上面所探讨的都是仓储的调用,而现在这个问题是仓储的实现,这是两种不同的概念。
什么是“SELECT 某某某”?答案就是针对字段进行查询,场景为应用程序的性能优化。我知道你看到“SELECT”就想到了事务脚本模式,不要想歪了哦,你眼中的仓储实现不一定是 ORM,也可以是传统的 ADO.NET,如果仓储实现使用的是数据库持久化机制,其实再高级的 ORM,到最后都会转换成 SQL 代码,具体表现就是对这些代码的优化,似乎不属于领域驱动设计的范畴了,但不可否认,这是应用程序不能不考虑的。
应用程序中的性能问题
我说一下现在短消息项目中仓储的实现(常用场景):底层使用的是 EntityFramework,为了更好的理解,我贴一段查询代码:
protected override async Task<IEnumerable<TAggregateRoot>> FindAll(ISpecification<TAggregateRoot> specification, System.Linq.Expressions.Expression<Func<TAggregateRoot, dynamic>> sortPredicate, SortOrder sortOrder, int pageNumber, int pageSize)
{
var query = efContext.Context.Set<TAggregateRoot>()
.Where(specification.GetExpression());
int skip = (pageNumber - 1) * pageSize;
int take = pageSize;
if (sortPredicate != null)
{
switch (sortOrder)
{
case SortOrder.Ascending:
return query.SortBy(sortPredicate).Skip(skip).Take(take).ToListAsync();
case SortOrder.Descending:
return query.SortByDescending(sortPredicate).Skip(skip).Take(take).ToListAsync();
default:
break;
}
}
return query.Skip(skip).Take(take).ToListAsync();
}
这种方式有什么问题吗?至少在我们做一些 DDD 示例的时候,没有任何问题,为什么?因为你没有实际去应用,也就体会不到一些问题,前一段时间短消息页面加载慢,一个是数据库索引问题(详见:程序员眼中的 SQL Server-执行计划教会我如何创建索引?),还有一个就是消息列表查询的时候,把消息表的所有字段都取出来了,这是完全没有必要的,比如消息内容就不需要进行读取,但是我们在跟踪上面代码执行的时候,会发现 EntityFramework 生成的 SQL 代码为 SELECT *。。。
走过的弯路
上面这个问题,至少从那个数据库索引问题解决完,我就一直郁闷着,也尝试着用各种方式去解决,比如创建 IQueryable 的 Select 表达式,传入的是自定义的聚合根属性,还有就是扩展 Select 表达式,详细过程就不回首了,我贴一下当时在搜索时的一些资料:
- IQueryable C# Select
- The entity cannot be constructed in a LINQ to Entities query
- Cannot implicitly convert type 'System.Collections.Generic.IEnumerable' to 'System.Collections.Generic.List'. An explicit conversion exists (are you missing a cast?)
- Cannot implicitly convert type ‘System.Linq.IQueryable<AnonymousType#1>’ to ‘System.Collections.Generic.IEnumerable’
- Cannot implicitly convert type 'System.Collections.Generic.List<AnonymousType#1>' to 'System.Collections.Generic.IEnumerable
- 使用Entity Framework时要注意的一些性能问题
- 不记得了...
在 EntityFramework 底层,我们 Get 查询的时候,一般都是返回 TAggregateRoot 聚合根集合对象,也就是说,你没有办法在底层进行指定属性查询,因为聚合根只有 ID 一个属性,唯一的办法就是传入 Expression<Func<TAggregateRoot, TAggregateRoot>> selector
表达式,select 两个范型约束为 TSource 和 TDest,这边我们两种类型都为 TAggregateRoot ,但是执行结果为:“The entity or complex type ... cannot be constructed in a LINQ to Entities query.”,给我的教训就是 Select 中的 TSource 和 TDest 不能为同一类型(至少指定属性的情况下)。
我的解决方案
EntityFramework 底层的所有查询返回类型改为 IQueryable<TAggregateRoot>
,仓储的查询返回类型改为 IEnumerable<MessageListDTO>
,为什么是 MessageListDTO 而不是 Message?因为我觉得消息列表的显示,就是对消息的扁平化处理,没必要是一个 Message 实体对象,虽然它是一个消息实体仓储,就好比从车库中取出一个所有汽车列表的单子,有必要把所有汽车实体取出来吗?很显然没有必要,我们只需要取出汽车的一些信息即可,我觉得这是应对业务场景变化所必须要调整的,具体的实现代码:
public async Task<IEnumerable<MessageListDTO>> GetInbox(Contact reader, PageQuery pageQuery)
{
return await GetAll(new InboxSpecification(reader), sp => sp.ID, SortOrder.Descending, pageQuery.PageIndex, pageQuery.PageSize)
.Project().To<MessageListDTO>()
.ToListAsync();
}
“Project().To()” 是什么东西?这是 AutoMapper 对 IQueryable 表达式的一个扩展,详情请参阅:恋爱虽易,相处不易:当 EntityFramework 爱上 AutoMapper,AutoMapper 扩展说明:Queryable Extensions,简单的一段代码就可以完成实体与 DTO 之间的转化,我们再次用 SQL Server Profiler 捕获生成的 SQL 代码,就会发现,这就是我们想要的,根据映射配置 Select 指定字段查询。
写在最后
针对“SELECT 某某某”这个实际应用问题,以上只是我的个人实现方式,如果你有疑问或是有更好的实现,欢迎指教。。。
Repository 仓储,你的归宿究竟在哪?(三)-SELECT 某某某。。。的更多相关文章
- Repository 仓储,你的归宿究竟在哪?(二)-这样的应用层代码,你能接受吗?
写在前面 关于"Repository 仓储,你的归宿究竟在哪?"这个系列,本来是想写个上下篇,但是现在觉得,很有多东西需要明确,我也不知道接下来会写多少篇,所以上一篇的标题就改成了 ...
- Repository 仓储,你的归宿究竟在哪?(一)-仓储的概念
写在前面 写这篇博文的灵感来自<如何开始DDD(完)>,很感谢young.han兄这几天的坚持,陆陆续续写了几篇有关于领域驱动设计的博文,让园中再次刮了一阵"DDD探讨风&quo ...
- Repository 仓储,你的归宿究竟在哪?(上)
Repository 仓储,你的归宿究竟在哪?(上) 写在前面 写这篇博文的灵感来自<如何开始DDD(完)>,很感谢young.han兄这几天的坚持,陆陆续续写了几篇有关于领域驱动设计的博 ...
- Repository 仓储
Repository 仓储 写在前面 首先,本篇博文主要包含两个主题: 领域服务中使用仓储 SELECT 某某某(有点晕?请看下面.) 上一篇:Repository 仓储,你的归宿究竟在哪?(二)-这 ...
- Repository仓储 UnitofWork
Repository仓储 UnitofWork 目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 本章我们来创建仓储类Repository 并且引入 UnitOfWork 我对 ...
- 【无私分享:ASP.NET CORE 项目实战(第五章)】Repository仓储 UnitofWork
目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 本章我们来创建仓储类Repository 并且引入 UnitOfWork 我对UnitOfWork的一些理解 UnitOfW ...
- MVC+Ef项目(2) 如何更改项目的生成顺序;数据库访问层Repository仓储层的实现
我们现在先来看看数据库的生成顺序 居然是 Idal层排在第一,而 web层在第二,model层反而在第三 了 我们需要把 coomon 公用层放在第一,Model层放在第二,接下来是 Idal ...
- 从Entity Framework的实现方式来看DDD中的repository仓储模式运用
一:最普通的数据库操作 static void Main(string[] args) { using (SchoolDBEntities db = new SchoolDBEntities()) { ...
- DDD之:Repository仓储模式
在DDD设计中大家都会使用Repository pattern来获取domain model所需要的数据. 1.什么事Repository? "A Repository mediates b ...
随机推荐
- redux-amrc:用更少的代码发起异步 action
很多人说 Redux 代码多,开发效率低.其实 Redux 是可以灵活使用以及拓展的,经过充分定制的 Redux 其实写不了几行代码.今天先介绍一个很好用的 Redux 拓展-- redux-amrc ...
- Java 线程
线程:线程是进程的组成部分,一个进程可以拥有多个线程,而一个线程必须拥有一个父进程.线程可以拥有自己的堆栈,自己的程序计数器和自己的局部变量,但不能拥有系统资源.它与父进程的其他线程共享该进程的所有资 ...
- socket读写返回值的处理
在调用socket读写函数read(),write()时,都会有返回值.如果没有正确处理返回值,就可能引入一些问题 总结了以下几点 1当read()或者write()函数返回值大于0时,表示实际从缓冲 ...
- Java多线程基础——对象及变量并发访问
在开发多线程程序时,如果每个多线程处理的事情都不一样,每个线程都互不相关,这样开发的过程就非常轻松.但是很多时候,多线程程序是需要同时访问同一个对象,或者变量的.这样,一个对象同时被多个线程访问,会出 ...
- 如何快速优化手游性能问题?从UGUI优化说起
WeTest 导读 本文作者从自身多年的Unity项目UI开发及优化的经验出发,从UGUI,CPU,GPU以及unity特有资源等几个维度,介绍了unity手游性能优化的一些方法. 在之前的文 ...
- [.NET] C# 知识回顾 - 委托 delegate (续)
C# 知识回顾 - 委托 delegate (续) [博主]反骨仔 [原文]http://www.cnblogs.com/liqingwen/p/6046171.html 序 上篇<C# 知识回 ...
- 算法与数据结构(七) AOV网的拓扑排序
今天博客的内容依然与图有关,今天博客的主题是关于拓扑排序的.拓扑排序是基于AOV网的,关于AOV网的概念,我想引用下方这句话来介绍: AOV网:在现代化管理中,人们常用有向图来描述和分析一项工程的计划 ...
- DBA成长路线
从开发转为数据库管理,即人们称为DBA的已经有好几年,有了与当初不一样的体会.数据是企业的血液,数据是石油,数据是一切大数据.云计算的基础.作为DBA是数据的保卫者.管理者,是企业非常重要的角色.对于 ...
- java设计模式之--单例模式
前言:最近看完<java多线程编程核心技术>一书后,对第六章的单例模式和多线程这章颇有兴趣,我知道我看完书还是记不住多少的,写篇博客记录自己所学的只是还是很有必要的,学习贵在坚持. 单例模 ...
- 原生JS实现-星级评分系统
今天我又写了个很酷的实例:星级评分系统(可自定义星星个数.显示信息) sufuStar.star();使用默认值5个星星,默认信息 var msg = [........]; sufuStar.sta ...