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