zoukankan      html  css  js  c++  java
  • Repository 仓储,你的归宿究竟在哪?(三)-SELECT 某某某。。。

    写在前面

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

    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 某某某”这个实际应用问题,以上只是我的个人实现方式,如果你有疑问或是有更好的实现,欢迎指教。。。

  • 相关阅读:
    Linux命令应用大词典-第11章 Shell编程
    Kubernetes 学习12 kubernetes 存储卷
    linux dd命令
    Kubernetes 学习11 kubernetes ingress及ingress controller
    Kubernetes 学习10 Service资源
    Kubernetes 学习9 Pod控制器
    Kubernetes 学习8 Pod控制器
    Kubernetes 学习7 Pod控制器应用进阶2
    Kubernetes 学习6 Pod控制器应用进阶
    Kubernetes 学习5 kubernetes资源清单定义入门
  • 原文地址:https://www.cnblogs.com/xishuai/p/3947897.html
Copyright © 2011-2022 走看看