庞大的建造者模式:以组装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对象作为基本的信息源,贯穿整个构造过程,如果有哪个部分需要做定制处理,则仅仅需要在对应类对应步骤处进行特殊处理,不影响整个建造过程。

原文地址:https://www.cnblogs.com/yinyunmoyi/p/14313217.html