写在前面

首先,本篇博文主要包含两个主题:

  1. 领域服务中使用仓储
  2. SELECT 某某某(有点晕?请看下面。)

上一篇:Repository 仓储,你的归宿究竟在哪?(二)-这样的应用层代码,你能接受吗?

关于仓储这个系列,很多园友问我:为什么纠结仓储?我觉得需要再次说明下(请不要再“纠结”了),引用上一篇博文中某一段评论的回复:

关于“纠结于仓储”这个问题,其实博文中我就有说明,不是说我纠结或是陷入这个问题,而是我觉得在实践领域驱动设计中,仓储的调用是一个很重要的东西,如果使用的不恰当,也许就像上面我所贴出来的应用层代码一样,我个人觉得,这是很多人在实践领域驱动设计中,很容易踩的一个坑,我只是希望可以把这个过程分享出来,给有相同困惑的人,可以借鉴一下。

领域服务和仓储的两种“微妙关系”

这边的“领域服务”和仓储的关系,可以理解为在领域中调用仓储,具体表现为在领域服务中使用。

在很久之前,我为了保持所谓的“领域纯洁”,在领域服务设计的时候,没有参杂仓储任何的调用,但是随着应用程序的复杂,很多业务添加进来,一个单纯的“业务描述”并不能真正去实现业务用例,所以这时候的领域服务就被“架空”了,一些业务实现“迫不得已”放在了应用层,也就是上一篇我所贴出的应用层代码,不知道你能不能接受?反正我是接受不了,所以我做了一些优化,领域服务中调用了仓储。

关于领域服务中调用仓储,在上一篇博文讨论中(czcz1024、Jesse Liu、netfocus、刘标才...),主要得出两种实现方式,这边我再大致总结下:

  1. 传统方式:仓储接口定义在领域层,实现在基础层,通过规约来约束查询,一般返回类型为聚合根集合对象,如果领域对象的查询逻辑比较多,具体体现就是仓储接口变多。
  2. IQueryable 方式:和上面不同的是接口的设计变少了,因为返回类型为 IQueryable,具体查询表达式的组合放在了调用层,也就是领域服务中,比如:xxxRepository.GetAll().Where(x=>....)

其实这两种方式都是一把双刃剑,关键在于自己根据具体的业务场景进行选择了,我说一下我的一些理解,比如现实生活中车库的场景,我们可以把车库看作是仓储,取车的过程看作是仓储的调用,车子的摆放根据汽车的规格,也就是仓储中的规约概念,比如我今天要开一辆德系、红色、敞篷、双门的跑车(条件有点多哈),然后我就去车库取车,在车库的“调度系统“(在仓储的具体表现,可以看作是 EF)中输入这些命令,然后一辆兰博基尼就出现在我的眼前了。

在上面描述的现实场景中,如果是第一种传统方式,“我要开一辆德系、红色、敞篷、双门的跑车”这个就可以设计为仓储的一个接口,为什么?因为车库可以换掉,而这些业务用例一般不会进行更改,车库中的“调度系统”根据命令是如何寻找汽车的呢?答案是规格的组合,也就是仓储中规约的组合,我们在针对具体业务场景设计的时候,一般会提炼出这个业务场景中的规约,这个也是不可变的,根据命令来进行对这些规约的组合,这个过车的具体体现就是仓储的实现,约束的是聚合根对象。这种方式中,我个人认为好处是可以充分利用规约,仓储的具体调用统一管理,让调用者感觉不到它是如何工作的,因为它只需要传一个命令过去,就可以得到想要的结果,唯一不好的地方就是:我心情不好,每天开的汽车都不一样,这个就要死人了,因为我要设计不同的仓储接口来进行对规约的组合。

如果是第二种方式,也就是把“调度系统”的使用权交到自己手里(第一种的这个过程可以看作是通过秘书),这种方式的好与坏,我就不多说了,我现在使用的是第一种方式,主要有两个原因:

  1. 防止 IQueryable 的滥用(领域服务非常像 DAL)。
  2. 现在应用场景中的查询比较少,没必要。

上一篇博文中贴出的是,发送短消息的应用层代码,发送的业务验证放在了应用层,以致于 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 表达式,详细过程就不回首了,我贴一下当时在搜索时的一些资料:

在 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 某某某。。。的更多相关文章

  1. Repository 仓储,你的归宿究竟在哪?(二)-这样的应用层代码,你能接受吗?

    写在前面 关于"Repository 仓储,你的归宿究竟在哪?"这个系列,本来是想写个上下篇,但是现在觉得,很有多东西需要明确,我也不知道接下来会写多少篇,所以上一篇的标题就改成了 ...

  2. Repository 仓储,你的归宿究竟在哪?(一)-仓储的概念

    写在前面 写这篇博文的灵感来自<如何开始DDD(完)>,很感谢young.han兄这几天的坚持,陆陆续续写了几篇有关于领域驱动设计的博文,让园中再次刮了一阵"DDD探讨风&quo ...

  3. Repository 仓储,你的归宿究竟在哪?(上)

    Repository 仓储,你的归宿究竟在哪?(上) 写在前面 写这篇博文的灵感来自<如何开始DDD(完)>,很感谢young.han兄这几天的坚持,陆陆续续写了几篇有关于领域驱动设计的博 ...

  4. Repository 仓储

    Repository 仓储 写在前面 首先,本篇博文主要包含两个主题: 领域服务中使用仓储 SELECT 某某某(有点晕?请看下面.) 上一篇:Repository 仓储,你的归宿究竟在哪?(二)-这 ...

  5. Repository仓储 UnitofWork

    Repository仓储 UnitofWork 目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 本章我们来创建仓储类Repository 并且引入 UnitOfWork 我对 ...

  6. 【无私分享:ASP.NET CORE 项目实战(第五章)】Repository仓储 UnitofWork

    目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 本章我们来创建仓储类Repository 并且引入 UnitOfWork 我对UnitOfWork的一些理解  UnitOfW ...

  7. MVC+Ef项目(2) 如何更改项目的生成顺序;数据库访问层Repository仓储层的实现

    我们现在先来看看数据库的生成顺序   居然是 Idal层排在第一,而 web层在第二,model层反而在第三 了   我们需要把 coomon 公用层放在第一,Model层放在第二,接下来是 Idal ...

  8. 从Entity Framework的实现方式来看DDD中的repository仓储模式运用

    一:最普通的数据库操作 static void Main(string[] args) { using (SchoolDBEntities db = new SchoolDBEntities()) { ...

  9. DDD之:Repository仓储模式

    在DDD设计中大家都会使用Repository pattern来获取domain model所需要的数据. 1.什么事Repository? "A Repository mediates b ...

随机推荐

  1. Cocos2d Android 环境搭建

    1.在开始之前,需要先准备好资源如下,如果安卓开发环境有了直接装第3.4. 1.JDK      点击下载 (1.6) 2.ADT(已经自带Android SDK)点击下载 3.NDK 点击下载 4. ...

  2. 神技!微信小程序(应用号)抢先入门教程(附最新案例DEMO-豆瓣电影)持续更新

    微信小程序 Demo(豆瓣电影) 由于时间的关系,没有办法写一个完整的说明,后续配合一些视频资料,请持续关注 官方文档:https://mp.weixin.qq.com/debug/wxadoc/de ...

  3. VS2015在创建项目时的一些注意事项

    一.下面是在创建一个新的项目是我最常用的,现在对他们一一做一个详细的介绍: 1.Win32控制台应用程序我平时编写小的C/C++程序都用它,它应该是用的最多的. 2.名称和解决方案名称的区别:名称是项 ...

  4. Tomcat启动报错org.springframework.web.context.ContextLoaderListener类配置错误——SHH框架

    SHH框架工程,Tomcat启动报错org.springframework.web.context.ContextLoaderListener类配置错误 1.查看配置文件web.xml中是否配置.or ...

  5. canvas快速绘制圆形、三角形、矩形、多边形

    想看前面整理的canvas常用API的同学可以点下面: canvas学习之API整理笔记(一) canvas学习之API整理笔记(二) 本系列文章涉及的所有代码都将上传至:项目代码github地址,喜 ...

  6. Git(1)

    安装Git 完毕 (在开始菜单打开的话,打开的不是你想要的路径,切换路径很麻烦) 1.D盘新建 GitTest 文件夹 2.打开GitTest , 在空白的地方右键, 3.单击 Git Bash He ...

  7. docker – 你应该知道的10件事

      容器并不是一个全新的技术,但这并不妨碍Docker如风暴一样席卷整个世界. 如果你在IT圈里,你一定听说过Docker.就算与其他热门技术,如:Puppet/Chef,Hadoop或者MongoD ...

  8. Linux基础介绍【第三篇】

    更改SSH服务端远程登录的配置 windows服务端的默认远程管理端口是3389,管理员用户是administrator,普通用户是guest.Linux的管理用户是root,普通用户默认有很多个,远 ...

  9. Android快乐贪吃蛇游戏实战项目开发教程-04虚拟方向键(三)三角形按钮效果

    该系列教程概述与目录:http://www.cnblogs.com/chengyujia/p/5787111.html 一.知识点讲解 当我们点击系统自带的按钮时,按钮的外观会发生变化.上篇博文中我们 ...

  10. python安装BeautifulSoup注意事项

    好久没有写爬虫了,最近用Python的BeautifulSoup4.Scrapy分别对以前写的spider进行优化,发现python3.5后这些库变化了很多,遇到了许多问题,在这里做一下总结. 切换环 ...