• 庞大的建造者模式:以组装SQL为例


    庞大的建造者模式:以组装SQL为例

    某微服务,作用是生成一条SQL语句,供其他服务调用,这条sql语句可能非常长,拼接过程中涉及和其他服务复杂的交互和解析,这种涉及复杂对象构建的情况一般要用建造者模式。

    常规的建造者模式涉及指导者和构造者,现实应用时一般更为简洁和直接,我们要达到的目的很简单:就是将巨大SQL的每一部分都标准化、模块化,最后达到代码优雅,可读性好的效果。

    总体思路

    整理一下可能进行整合的部分:

    一条SQL语句常常由以下部分组成:select部分、from部分、where部分、join部分、order部分、limit部分、group部分等。每一个部分拼装的规则不同,它们都应该有对应的类来完成其拼装时的处理。

    一条SQL语句常常由多个子句组成,有的是join部分用到的语句,有的是from部分用到的语句等,这些子句各自的拼装规则不同,所以它们也必须有对应的类完成拼装。

    总结一下,我们需要建立两个层次的建造器,一个层次较高,是和业务强相关的子句拼接构造器,一个是较为通用的底层能力,是sql中各元素的拼接构造器。

    我们最终想要的结果是,经过前期的设置,构造器对象调用构造方法直接生成sql语句,在本例中应该就是:

    SqlBuilder sqlBuilder = new SqlBuilderFactory.getSqlBuilder(context)
    String sql = sqlBuilder.build();
    

    其中context是由请求request生成的上下文对象。

    工厂类调用不同的构造器

    在业务中,可能对应多种截然不同的构造模式,这些模式相互之间没有重合的部分,此时就需要用工厂类来完成这个分支选择:

    public class SqlBuilderFactory {
        public static SqlBuilder getSqlBuilder(Context context) {
            
            switch (??) {
                case x1:
                    return new Pattern1SqlBuilder(context);
                case x2:
                    return new Pattern2SqlBuilder(context);
                case x3:
                    return new Pattern3SqlBuilder(context);
                default:
                    throw new NoSuchPatternException();
            }
        }
    }
    

    子句拼接构造器

    贯穿创建builder的对象是context,在SqlBuilder的构造方法中按顺序构造字句的各个部分:

    public Pattern1SqlBuilder(Context context) {
        buildSelect(context);
        buildFrom(context);
        buildJoin(context);
        bulidOrder(context);
        buildLimit(context);
    }
    

    在每个方法中完成各部分需要部分的数据查询和拼装,如buildSelect中要查到select部分用到的数据,如我们要完成用户某篇博客的活跃数据查询,不同博客的活跃字段有一部分是随机生成的,因为用户可以自定义博客,所以要查询的字段名并不是固定的,此时必须与数据库交互取到这个字段值,而子句拼接构造器的主要职责则在于此。

    将数据拿到后,拼接的职责传递给更底层的select构造器:

    private void buildSelect(Context context) {
        // 1、这里用context完成与数据库的交互,得到要查询的字段field1、field2
        
        // 2、建立selectBuilder,链式调用构造器
        SelectBuilder selectBuilder = new SelectBuilder.addField(field1).addField(field2);
        
        // 3、将设置好的构造器传出,准备使用build方法构造
        addBuilder(selectBuilder);
    }
    

    更复杂的情况,如下例,在拼接时涉及其他子句,此时需要在第一步这里将子句拼装好拿到,然后再进行后续处理,至于是怎样拿到子句的,相当于一种模式上的递归,流程大致与此类似:

    private void buildFrom(Context context) {
        // 1、这里用context完成与数据库的交互,得到要查询表table1
        // 或者是递归调用这个过程,拿到table2(当然是不同的子句构造器):
        String table2 = new Pattern2SqlBuilder(context).build();
        
        // 2、建立fromBuilder,链式调用构造器
        FromBuilder fromBuilder = new FromBuilder.addTable(table1).addTable(table2);
        
        // 3、将设置好的构造器传出,准备使用build方法构造
        addBuilder(fromBuilder);
    }
    

    语句元素拼接构造器

    以SelectBuilder为例,addField方法将要查询的字段放入内部一个集合中,并返回当前对象:

    public class SelectBuilder extends AbstractSqlBuilder {
        private List<String> selectFields = new ArrayList<SelectItem>();
        
        public SelectBuilder addField(String field) {
            this.selectFields.add(field);
            return this;
        }
    }
    

    剩下的问题就是addBuilder这个方法要做什么,思考这个问题之前,我们先要思考如何使用这些构造器。回到我们一开始想要的结果,我们想直接获取构造器,然后调用建造方法直接得到结果:

    SqlBuilder sqlBuilder = new SqlBuilderFactory.getSqlBuilder(context)
    String sql = sqlBuilder.build();
    

    build方法内部其实就是将之前我们设置好的组成sql的元素(如select部分、from部分)拼接起来,为了能模块化实现这个过程,在上面的addBuilder方法负责将这些组成数据汇总起来,而build负责将汇总后的数据拼接在一起,这样设计的好处在于在建造模式的整个过程中,都可以向select部分添加元素,而不仅仅局限于一处,这就将构造和生成两部分分开,解耦性好,SQL组装过程就像搭积木一样简单便捷。

    在addBuilder方法中,将拼接元素统一放入一个集合中,因为任何的子句构造器都需要调用该方法,统一用一个集合,所以这个方法写在它们共同的父类中:

    public abstract class AbstractSqlBuilder implements SqlBuild {
        private List<SqlBuild> sqlBuilds = new ArrayList<>();
        
        public AbstractSqlBuilder addBuilder(SqlBuilder builder) {
            SqlBuilder.add(builder);
            return this;
        }
    }
    

    这样当所有addBuilder方法都执行完毕,组成SQL的所有元素都已经填充完毕,最后只待调用build方法了。

    生成SQL

    所有模块都在sqlBuilds集合后,我们还需要一个载体来收集拼接信息,因为sqlBuilds中的build类型和数量都是不确定的,这个载体被命名为SqlStatement,在这个类中,sql的各组成部分就对应它的各成员变量:

    public class SqlStatement {
        private List<String> select;
        private List<String> from;
        ...
        public toString() {
            // 在这里将各成员变量拼接,然后返回结果,这个结果就是sql
            return sql;
        }
    }
    

    回到一开始我们设想的调用方法:

    SqlBuilder sqlBuilder = new SqlBuilderFactory.getSqlBuilder(context)
    String sql = sqlBuilder.build();
    

    这个build方法就是将其中的sqlBuilds集合汇总,将所有信息注入SqlStatement中:

    public abstract class AbstractSqlBuilder implements SqlBuild {
        private List<SqlBuild> sqlBuilds = new ArrayList<>();
        ...
        public String build() {
            SqlStatement sqlStatement = buildStatement();
            return sqlStatement.toString();
        }
        
        public SqlStatement buildStatement() {
            SqlStatement sqlStatement = new SqlStatement();
            for (SqlBuild sqlBuild : sqlBuilds) {
                sqlBuild.build(context, sqlStatement);
            }
            return sqlStatement;
        }
    }
    

    获取每个在sqlBuilds集合中的构造器,然后调用它们各自的build方法,向sqlStatement中注入数据,以SelectBuilder为例,在build方法中它自己的集合中抽取数据,然后设置到sqlStatement中:

    public class SelectBuilder extends AbstractSqlBuilder {
        private List<String> selectFields = new ArrayList<SelectItem>();
        
        ...
        public void builder(Context context, SqlStatement sqlStatement) {
            List<String> select = new ArrayList<>(selectFields);
            sqlStatement.getSelect().addAll(select);
        }
    }
    

    context对象作为基本的信息源,贯穿整个构造过程,如果有哪个部分需要做定制处理,则仅仅需要在对应类对应步骤处进行特殊处理,不影响整个建造过程。

  • 相关阅读:
    grafana邮箱配置
    grafana集群配置
    CentOS7 配置OOM监控报警
    Mycat使用配置实践
    CentOS7安装JAVA环境
    CentOS7安装MYCAT中间件
    CentOS7安装MySQL5.6
    Mockingbird
    堆的建立与功能实现
    Matlab解决线性规划问题
  • 原文地址:https://www.cnblogs.com/yinyunmoyi/p/14313217.html
Copyright © 2020-2023  润新知