DDD落地的思考–复杂SQL的查询问题

一、背景

在之前的文章中简单讨论了SQL中的写计算问题,重点将SQL中的技术因素与业务因素进行区分来更好的从代码层面控制复杂度,本篇文章将重点讨论复杂SQL中的查询问题,在DDD中如何控制复杂查询产生的复杂性。同时也将深入分析不同的复杂查询SQL的类型和场景,以便对症下药。 

另外在跟群友们讨论DDD下的复杂SQL解决方案的时候发现很多人对此方面的内容比较感兴趣,所以本篇文章很早就写好了标题,但是因为个人原因没有落笔,这里就着放假的时间补补作业。

二、复杂查询SQL在DDD中的复杂度

本小节将讨论不同场景的复杂SQL,通过对复杂SQL在项目中的展现场景来分析下不同SQL背后的复杂度和相应的解决方案。

2.1 单表多条件查询

这种情况是遇到的比较多的一种,我把此类场景归结为复杂SQL的入门级,从代码层面上看但表多条件查询有两种表现形式。

  1. 按不同参数开发DAO查询接口

    这里根据使用框架的不同开发的代码和SQL产物也不同,比如常规的select from where column=.

如果使用hibernate或者MP可能就直接在DAO层面就动态拼好了查询SQL。

  1. 定义多条件查询模型

这里一般又分为两种情况,一种是使用Java Entity表模型来当查询模型来构建动态SQL,另外一种则使用独立的查询对象模型(如XxxQueryDTO,XxxQueryObj,Map) 开发建议:

  1. 在表模型或者表结构有很多相对明确的具有业务语意的字段的时候尽量使用多条件查询模型来
  2. 如果查询模型中的查询属性与Java Entity严格一致则可以使用JavaEntity替代,外部传入可以正常复用对应dto,bo等模型
  3. 如果查询模型中的查询属性与Java Entity 不一致,则使用独立的查询对象模型。
  4. 可以根据使用框架的情况在基础设施层定义查询抽象类,这样的话查询对象模型可能不会太膨胀
  5. 另外的情况就是如果有很多类似属性的查询模型可以使用抽象类共同管理,在Java的包访问权限范围内使用。
  6. 多条件查询使用的过程中建议条件数按3来分,超过三个使用对象类的多条件查询模型。小于等于3,使用map,定义分别的参数都是可以的。
  7. 如果表是聚合根或者聚合对象对应的表的话,那么返回的数据内容建议只返回自己表内容,涉及到是否加载聚合内容则建议使用规格模式,后面会继续深入讨论。

2.2 单表分页查询

这里讲到的单表分页查询是很多CRUD的核心内容,一整张页面涵盖了很多以分页数据为基础的操作。所以大多数成熟的框架或者项目涉及到xx管理页面都会有一个分页查询对象,定义分页参数,查询参数,排序参数,结果集等。这里要引出的内容是对于查询的参数如何跟分页对象进行融合。我们看一下Mybatis-pageHelper的分页参数。

public class PageInfo<Timplements Serializable {
    private static final long serialVersionUID = 1L;
    private int pageNum;
    private int pageSize;
    private int size;
    private int startRow;
    private int endRow;
    private long total;
    private int pages;
    private List<T> list;
    private int prePage;
    private int nextPage;
    private boolean isFirstPage;
    private boolean isLastPage;
    private boolean hasPreviousPage;
    private boolean hasNextPage;
    private int navigatePages;
    private int[] navigatepageNums;
    private int navigateFirstPage;
    private int navigateLastPage;
    //.....
}

在分页查询的时候需要在查询的时候需要带查询条件的话可能每个查询接口都要带一个查询对象和一个分页对象,所以在天画-数据工厂2.0的实践中将分页对象模型和查询对象模型融合为一体,如下:

/**
 * Description:参考Mybatis_PageHelper的Page对象封装的DTO
 * date: 2021/10/26
 *
 * @author fanchunshuai
 * @version 1.0.0
 * @since JDK 1.8
 */

public class PageDTO<Eimplements Serializable {

    private static final long serialVersionUID = -2470832822882514457L;
    /**
     * 页码,从1开始
     */

    private int currentPageNum;

    /**
     * 当前页的下一页
     */

    private int nextPageNum;

    /**
     * 当前页的上一页
     */

    private int prePageNum;
    /**
     * 页面大小
     */

    private int pageSize;
    /**
     * 起始行
     */

    private int startRow;
    /**
     * 末行
     */

    private int endRow;
    /**
     * 总数
     */

    private long totalRows;
    /**
     * 总页数
     */

    private int totalPages;
    /**
     * 包含count查询
     */

    private boolean count = true;
    /**
     * 分页合理化
     */

    private Boolean reasonable;
    /**
     * 当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
     */

    private Boolean pageSizeZero;
    /**
     * 进行count查询的列名
     */

    private String countColumn;
    /**
     * 排序
     */

    private String orderBy;
    /**
     * 只增加排序
     */

    private boolean orderByOnly;

    private List<E> Items;


    /**
     * 针对简单场景 参数的查询map处理
     */

    private Map<String,Object> queryMap;

    /**
     * 针对复杂场景 参数的查询queryDTO对象处理
     */

    private Object queryDTO;
 
    //.....

}

可以看到整体模型差别不大,但是增加了两个查询参数,在DDD分层架构下或者MVC分层架构下查询模型不需要经过domain层感知到。现在看一下对于接口的定义和参数的使用:

/**
  *
  * @Description 分页获取api信息
  * @param apiQueryVO
  * @return PageVO<ApiVO>
  */

 @RequestMapping(value = "/api/pagelist")
 public ResultDataDto<PageVO<ApiVO>> getPageList(ApiQueryVO apiQueryVO){

  PageBean pageBean = apiQueryVO.getPageBean(); //1.构建PageBean
  pageBean = projectQueryRepository.queryApiPage(pageBean);
  List<ApiVO> apiVOList = ApiConverter.INSTANCE.BOs2VOs(projectQueryRepository.queryApiPage(pageBean).getRows());

  apiQueryVO.setRows(apiVOList);
  apiQueryVO.setCount(pageBean.getCount());
  return ResultDataDto.success(apiQueryVO);
 }

上述的PageBean模型是domain层定义的统一分页查询和结果集模型,这样到基础设施层基本不需要改什么代码,在MapperXml中可以直接拿到对象查询参数。这里分别看一下基础设施仓库Api queryApiPage的实现

 @Override
 public PageBean queryApiPage(PageBean pageBean ){
        List<ApiModelDO> apiModelDOList = apiModelMapper.getPageList(pageBean);
        List<ApiBO> apiBOList = ApiConvert.INSTANCE.doList2boList(apiModelDOList);
        apiBOList.stream().forEach(apiBO -> {
            apiBO.buildRequestParam();
        });
        pageBean.setRows(apiBOList);
        pageBean.setCount(apiModelMapper.getPageCount(pageBean));
        return pageBean;
    }

,但是这里也有一定的缺点,就是查询的时候没有在接口标明使用的是哪个查询对象模型,所以算是一种设计上的折中。对于查询模型,在数据工厂的adapter中的vo子包里进行了统一管理。

2.3 单表报表导出

这里讲到的单表报表导出也是比较常见的,经常与2.2中提到的单表分页查询一起出现,区别在于单表报表导出需要根据不同的查询条件导出全量数据。如果查询场景能与2.1相匹配那对整个DAO底层和业务代码层会少一半的代码量,开发效率也会提升。所以这里可能会对性能产生影响,尤其是不能使用*,不能使用全模糊匹配,尽量更多的使用最左匹配原则命中更多的索引。开发建议:

  1. 数据量比较大的话考虑性能和SQL
  2. 表数据需要转换的话,在导出之前进行数据预处理,使用CQRS模式专门对Q进行隔离。

2.4 多表多条件查询

现在讨论下多表多条件查询,相当于单表多条件查询的升级版,需要特别说明的是复杂度也会升级,这里就需要重点分析了,多表多条件查询的话返回的数据内容可能就是聚合内容或者是定制内容,那么开发者可能会陷入下面的陷阱中:

  1. 查询返回使用map
  2. 查询返回定制的javaEntity
  3. 多表多条件查询使用DAO框架的关联查询,比如一对多,多对多等。
  4. 重点是查询条件可能会比较多,比较散乱,动态性比较强。

那么针对以上查询陷阱中给出的开发建议如下:

  1. 使用查询规格模式,分析不同场景下的查询业务稳定性
  2. 比较稳定的话使用定制的JavaEntity,区别在于要与表模型对应的JavaEntity进行分包管理,控制复杂度
  3. 不稳定的话可以使用查询结果返回Map的形式,在dto层面或者业务领域层面定义map转模型的通用工具类,尽量让DAO底层更稳定,就是说修改更少的代码更好的满足需求的变化。
  4. 涉及到聚合查询的可以使用DAO框架的关联查询,在DAO层面可以使用定制的JavaEntity,或者JavaEntity中定义聚合模型。
  5. 查询条件比较多,比较散乱的话可以使用查询对象模型统一管控,另外借助自定义注解等实现机制来动态构建查询SQL。或者使用DAO提供的查询拼装API。但是需要特别说明的是尽量满足通用性。
  6. 数据量比较大,涉及模型比较多的话,可以使用ES查询,这里就建议查询条件和查询结果专门使用对象模型来构建了。

2.5 多表分页查询

多表分页查询其实会麻烦点,但是有个关键点可以控制复杂度,就是分页数据一定是以少数的某几张表提供大量的属性字段,这几张表是作为主表存在的。需要说明的是每条数据可能都是业务上的某种聚合形态,所以多表分页查询返回的数据应该就是聚合形态里的大部分数据了。这里给出的开发建议如上,需要考虑业务形态和性能。其他建议如下:

  1. 多表分页查询涉及到的其他表或者数据可以预处理提高速度
  2. 查询结果模型可以不在domain层定义结果模型,这里大多数情况下查询结果是直接返回的,所以可以在应用层使用dto来接收结果。

2.6 多表报表导出

多表报表导出与多表多条件查询可以做成一样的底层逻辑,但是也看业务特性,这里需要说明的是一般报表导出的数据不会过多,大部分都是同步导出的,或者会限制导出条件。另外就是走BI或者ES或者大数据的方式来实现,常规来讲,多表报表导出涉及数据量比较大的话其实更多的可能就是要考虑性能问题了。所以为了性能可以在模型上做一定的让步,或者直接提SQL导(无)出(可)工(耐)单(何)吧。也有些少数的xxx管理系统会存在这种情况,比如数据量大,存在多表导出,没有走其他存储中间件,那这种情况看上去只能硬磕了。不过硬磕也有一定的方法和技巧,比如异步化,并行和缓存。当然这里具体的实现的话可以放到应用层或者基础设施层去做。

2.7 单表多条件数据统计

单表多条件的数据统计有时候会让业务模型变得及其不稳定,或者说数据统计维度和口径又经常变化,所以在写需求代码中,关于技术统计相关的内容建议单独拎出一个模块,在基础设施层中实现。在查询入参的模型中可以复用上述场景中提到的查询模型,结果集模型建议用统一抽象的统计模型,比如构建如下Java Entity实体:DDD落地的思考--复杂SQL的查询问题不过另外一些场景中会出现查询业务字段的同时会带上一些统计数据,比如返回学生人数加上一些学生信息之类的,但是如果从统计模型的角度来看,这也非常容易解决,就是让Java实体模型继承上面的统计模型,在查询的时候即可复用。另外一个情况就是Java实体模型本身已经继承了某BaseEntity了,或者不太方便作为父类继承,此时可以单独抽象一个当前Java实体模型的子类来实现。

2.8 多表多条件数据统计

多表多条件数据统计本身应该包含两个维度,一是复杂查询,二是统计。所以此时面向对象的查询和结果集模型会更符合需求。另外也要看统计口径对应的结果集字段是否比较多,如果比较少的话那后期出现需求变化改动的可能性就越高。对于查询模型来说不同角度的查询构造出的查询SQ L也不一样,所以会经常遇到此类统计查询会经常变动,又需要经常改,对于此类场景,可以有如下开发建议:

  1. 如果可以的话建议使用查询对象模型和结果集对象模型,并且在单独的模块维护
  2. 如果可以的话充分利用动态SQL和动态结果集映射的ORM框架的技术潜力
  3. 如果需求中读写中存在此类统计需求且交织在一起,建议使用CQRS
  4. 数据量比较大的话,请看2.9

2.9 面向中间件的类SQL查询(ES,MONGO,REDIS)

此类查询可以说是面向NoSQL方面的,经过10年SQL与NewSql,NoSql的竞争,SQL还屹立不倒的原因在于业务模型和对象模型本身就是需要一种稳定的CURD协议来管理,业务模型和对象模型内部关系中更高级或者更离散的维度需要NoSql产品来解决。如果项目中用到了类SQL查询的话对于其查询模型和结果集模型也需要适应对应的NoSQL中间件。比如ES和 MongoDB,查询条件和查询结果集都可能是多属性的。所以更适用于对象模型。这里需要注意的是对于数据存储在ES或者MongoDB中的数据,其Java模型与领域模型或者数据库模型可能不是一一对应的,有可能是跨聚合的,或者按某个聚合维度存储的,又或者是对某些属性字段做了特殊处理等。需要说明的是Redis,由于Redis本身是内存数据库,同时数据结构也比较多,所以不同的场景需要存储到Redis中的数据内容也不一样,可能用JSON 序列化的字符串存到Redis里还不如字符串拼接性能更高。另外对于Redis而言,其查询模型本身就是比较简单的,所以在这里用查询对象模型和结果集模型不一定合适。但是对于Redis Value而言可以使用建造者模式来构建,从而避免字符串的硬拼接。既然是面向中间件的类SQL查询的话,有如下开发建议:

  1. 此类查询更多的偏向于非业务核心模型,所以可以单独立模块,或者接口
  2. 能用对象模型就用,不能用的话就简单封装到基础设施层中
  3. 查询需求和场景比较多的话建议使用CQRS,另外借助规格模式或者一定的设计模式保证查询模块的可扩展性和稳定性

三、复杂SQL治理

3.1 查询SQL的类型

类型 特性 说明
与业务强关联 对查询性能比较敏感,查询代码与底层存储框架耦合绑定 查询SQL对应的查询逻辑不能太复杂,或者需要保障稳定性和代码健壮性
报表统计型 对数据质量比较敏感,不一定与业务代码在一起,展示页面也不一定在对应的管理平台 如果直接查数据库搞不定可能就要借助于其他存储中间件,会涉及到一些代码逻辑的改动
实时性查询 对查询性能比较敏感,查询分析多借助于大数据框架,在大数据量下用的比较多 此类场景对数据规模,还有性能方面都有比较高的要求,比如报表导出,大数据量分页等,所以需要比较丰富的优化手段
非实时性查询 对查询性能不是很敏感,不同场景用的技术不太一样,在大数据量下用的比较多 非实时性查询出的内容会更丰富,对于各种维度的数据提取计算也更灵活,在大数据领域更容易处理这样的事情

通过上面的分析我们看一下如何对不同场景下的模型结构进行治理

3.2 单表治理

单表治理大多数情况下还是能控制住复杂度的,不过有一种情况可能需要注意,就是具有聚合性质的表,也叫主表,常规的CURD表是没什么问题的,但是对于主表对应的单表来说其业务属性会更重一些,所以尽量考虑主表业务功能的可扩展性,可复用性,另外注意好隔离边界。

3.3 多表治理

多表治理的一个切入点就是看业务模块或者功能模块的聚合程度,一般来说需要在基础设施层中对几张聚合表做复杂度管理,在领域层中定义好仓库接口或者聚合接口,底层的连表查询,或者统计查询或者查询请求对象,结果对象等都是可以控制住复杂度的。当然也看不同的ORM框架和SQ L的管理方式,有些SQL是手写的在代码里,有些sql在配置文件里,有些SQL是动态构建的。需要强调的是尽量不要过多的关联表,过多的创建结果集模型,尽可能的复用模型。另外的就是不要随别联与聚合模块不太相关的表。

3.4 聚合治理

这里的聚合治理跟多表治理差不多,但是也是需要结合领域层的模型也聚合的设计去控制复杂度,聚合设计有很多维度,不同的维度看到的数据结构不一样。所以如果业务上需要构建多个聚合模型来支持业务的话,那可能需要业务代码和底层SQL一起管控。当然针对查询的话此类场景更适合CQRS和读写分离,对于读模型来说复用领域模型的属性是比较简单的。如果要复用类的话,还是建议考虑跨层调用吧。对于复杂度比较低且聚合查询属性比较多的情况下,建议让app层直接引用基础设施层的查询实现,对于查询结果模型来说可能需要放到domain层,或者公共模块,尽量减少对象复制的链路长度。类似于查询结果直接返回到VO或者DTO层面。

四、总结

本文具体分析了不同场景下的SQL查询复杂度,并给出了一定的建议,当然不是所有的建议都适合,所以还需要在实践中探索更多管控复杂查询SQL在DDD工程架构下产生的复杂度。


原文始发于微信公众号(神帅的架构实战):DDD落地的思考–复杂SQL的查询问题

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/241370.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!