Repository 仓储

写在前面

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

  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 仓储的更多相关文章

  1. Repository 仓储,你的归宿究竟在哪?(三)-SELECT 某某某。。。

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

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

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

  3. Repository仓储 UnitofWork

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

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

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

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

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

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

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

  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. POJ1080 Human Gene Functions 动态规划 LCS的变形

    题意读了半年,唉,给你两串字符,然后长度不同,你能够用'-'把它们补成同样长度,补在哪里取决于得分,它会给你一个得分表,问你最大得分 跟LCS非常像的DP数组 dp[i][j]表示第一个字符串取第i个 ...

  2. 使用方便git命令检查记录的版本号

    现在开始git大多数用户都经历过subversion,对于这两种开关的版本控制系统需要一段时间去适应.本文旨在帮助恢复一些,这些用户都熟悉的日志记录买家的习惯. 我们要熟悉一个详细的例子git中log ...

  3. Online网站集

    http://tool.oschina.net/apidocs/    在线工具(IT技术工具)

  4. jQuery整理笔记文件夹

    jQuery整理笔记文件夹 jQuery整理笔记一----jQuery開始 jQuery整理笔记二----jQuery选择器整理 jQuery整理笔记三----jQuery过滤函数 jQuery整理笔 ...

  5. Hadoop之——HBase注意事项

    转载请注明出处:http://blog.csdn.net/l1028386804/article/details/46447573 1.HBase(NoSQL)的数据模型 1.1 表(table) 存 ...

  6. asp.net学习之扩展GridView

    原文:asp.net学习之扩展GridView 本节讨论如何从现有的控件,进而扩展成强大的,更定制的GridView控件 1.扩展BoundField 默认的BoundField不能显示多文本,文字一 ...

  7. Git提交代码的处理流程(转)

    Jerry 工作在wchar_support分支.他改变了名称的功能和测试后,他提交他的变化. [jerry@CentOS src]$ git branch master * wchar_suppor ...

  8. /proc/mtd 各参数的含义 -- linux内核

    经/proc虚拟文件系统读取MTD分区表:cat /proc/mtd mtd .name = raspi, .size = 0x00400000 (4M) .erasesize = 0x0001000 ...

  9. MP4文件格式具体解释——结构概述

    MP4文件格式具体解释(ISO-14496-12/14) Author:Pirate Leo Email:codeevoship@gmail.com 一.基本概念 1. 文件,由很多Box和FullB ...

  10. 贪心算法(Greedy Algorithm)最小生成树 克鲁斯卡尔算法(Kruskal&#39;s algorithm)

    克鲁斯卡尔算法(Kruskal's algorithm)它既是古典最低的一个简单的了解生成树算法. 这充分反映了这一点贪心算法的精髓.该方法可以通常的图被表示.图选择这里借用Wikipedia在.非常 ...