写在前面
首先,本篇博文主要包含两个主题:
- 领域服务中使用仓储
- 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 某某某”这个实际应用问题,以上只是我的个人实现方式,如果你有疑问或是有更好的实现,欢迎指教。。。