专栏名称: 逸言
文学与软件,诗意地想念。
目录
相关文章推荐
神秘的程序员们  ·  文生图模型进化简史和生成能力比较——艺术肖像篇 ·  1 年前  
神秘的程序员们  ·  KALOS.art AI 作品每周精选 021 ·  1 年前  
程序员的那些事  ·  人人影视:将 20 ... ·  4 天前  
程序员小灰  ·  真心建议大家拿下软考证(风口) ·  6 天前  
51好读  ›  专栏  ›  逸言

项目札记006:数据引擎模块的设计

逸言  · 公众号  · 程序员  · 2024-11-04 08:08

正文

数据引擎(Data Engine)模块属于EISaaS产品架构的框架层,它是元数据与业务数据之间的桥梁,可以将配置的元数据生成SQL语句,然后通过JDBC执行SQL语句获得报表需要的数据。

在设计上,数据引擎需要支持多种SQL语句的生成。为更灵活的支持SQL语句生成的功能,需要提供SQL语法树,以及语法树解析器。不过,在当初的详细设计方案中,只具备了计划设计的初步功能。

数据引擎模块的命名空间为:en.com.chengjun.eisaas.framework.engine.data,项目名称为eisaas-framework-engine-data。它主要由service、executor、assembler、statement、resource以及datasource等子模块构成。如前所述,我的建议是为模块定义门面类以提升API的易用性,在数据引擎模块扮演门面类的是service子模块。下图为当初对数据引擎模块的设计图:

datasource子模块

首先介绍datasource子模块,它包含的类可以认为是数据引擎的模型类,是对数据库数据的抽象,数据引擎访问数据库得到的结果就以下图所示的Table对象来呈现:

由于它是我们自行定义的模型对象,故而可以与基础设施彻底解耦,而它又具有规范的数据结构,可以将其传递给实体引擎,作为映射实体的数据值。Table相当于数据表,Record是其中的一行,Field是数据表的列:

public class Table { private List records = new ArrayList();
public List getRecords() { return records; }
public void addRecord(Record record) { records.add(record); }}
public class Record { private List fields = new ArrayList();
public List getFields() { return fields; }
public void addField(Field field) { fields.add(field); }
public Object getValue(String fieldName) { for (Field field : fields) { if (field.getName().equalsIgnoreCase(fieldName)) { return field.getValue(); } } return null; }}
public class Field { private String name; private Object value; private String className;
public Field(String name, Object value) { this.name = name; this.value = value; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Object getValue() { return value; }
public void setValue(Object value) { this.value = value; }
public String getClassName() { return className; }
public void setClassName(String className) { this.className = className; }}

resource子模块

接着介绍resource子模块,它负责完成从数据集元数据到SQL语句各个组成部分的解析,解析的结果封装为Variable(变量),例如组成SQL的数据表名是一个Variable,Where条件表达式也是一个变量,解析后的各个变量最终存储到SqlResource中,具体的解析工作则由SqlResourceParser完成。如下图所示:

SqlResourceParser的作用会在asembler模块中有所体现。

statement子模块

如果说resource模块是完成元数据到SQL语句各个组成部分的转换,则statement模块就是要把这些组成部分塞到实现准备好的模板,最终形成一个完整的SQL语句。设计类图如下所示:

模块主要接口为SqlStatment。它继承了SqlResourceProvider接口可以设置SqlResource对象。该对象存储的信息在装配SQL语句的时候需要使用。SqlResource类在datasource子模块。

SqlStatement提供了getSql()方法,用以获得数据引擎想要执行的SQL语句。在其内部,会调用SqlTemplate的evaluate()方法对模板字符串进行解析。执行不同操作的SQL语句需要与之对应的模板,故而可以将SqlStatement与SqlTemplate看作是两个结构和变化方向都一致的继承体系。

这两个继承体系都运用了模板方法模式(Template Method Pattern)。

由于SQL语句遵循通用的格式,SqlStatement就为各种CRUD操作定义通用的SQL模板。它们的差异主要体现在包含变量占位符的SQL模板值和各自对应的变量,一旦统一这些差异后,解析模板的逻辑都是一样的。与之有关的主要代码如下所示:

public interface VariableUtil { String FUNCTION = "${function}"; String DISTINCT = "${distinct}"; String COLUMNS = "${columns}"; String TABLE_NAME = "${tableName}"; String LEFT_JOIN = "${leftJoin}"; String WHERE = "${where}"; String ORDER_BY = "${orderBy}"; String GROUP_BY = "${groupBy}"; String PARAMETER = "${parameter}"; String SET = "${set}";}
public interface SqlTemplate { String evaluate(); void addVariable(Variable variable);}public abstract class SqlTemplateBase implements SqlTemplate { private List variables; private String replacement;
public String evaluate() { String template = getTemplate(); for (Variable variable : variables) { String regex = "\\$\\{" + variable.getName() + "\\}"; template.replaceAll(regex,variable.getValue()); } return template; } public void addVariable(Variable variable) { variables.add(variable); } protected abstract String getTemplate();}public class QuerySqlTemplate extends SqlTemplateBase { // 这里以最简单的查询SQL为例 @Override protected String getTemplate() { return "select " + VariableUtil.COLUMNS + " from " + VariableUtil.TABLE_NAME + " where " + VariableUtil.WHERE; }}public final class Variable { public Variable(String name, String value) {} public String getName() {} public String getValue() {}}

表面上,SqlStatement才是生成SQL语句的对象,但实际上,解析变量与模板获得SQL语句的职责都委派给了SqlTemplate,如下代码所示,getSql()方法的内部调用了SqlTemplate的evaluate()方法。SqlStatement更像是一个指导者,它允许调用者根据自己的选择添加对应的变量,同时还要确保它关联的是正确的模板类,这两个工作分别由addVariable()和createTemplate()方法完成。只有createTemplate()存在差异,因此,调用该方法的构造函数算是一个模板方法,如下示意代码所示:

public interface SqlStatement extends SqlResourceProvider { public String getSql(); public void addVariable(Variable variable);}
public abstract class SqlStatementBase implements SqlStatement { private SqlResource sqlResource; protected SqlTemplate template;
public SqlStatementBase() { template = createTemplate(); }
public void addVariable(Variable variable) { template.addVariable(variable); }
public String getSql() { return template.evaluate(); }
protected abstract SqlTemplate createTemplate();}
public class QuerySqlStatement extends SqlStatementBase { @Override protected SqlTemplate createTemplate() { return new QuerySqlTemplate(); }}

虽说都运用了模板方法模式,我在设计时,却为模板方法的抽象同时定义了接口和抽象类,如SqlStatement与SqlStatementBase,SqlTemplate与SqlTemplateBase。现在看来,根本没有必要,完全可以将二者合并为一个抽象类,分别命名为SqlStatement与SqlTemplate这一冗余设计应该是受到自己对“面向接口设计”之误解所限。

该设计还存在一个问题,即它具有Martin Fowler所说的“平行继承体系(Parallel Inheritance Hierarchies)”坏味道。这个坏味道的症状为:当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。让我们换一个角度来看看这一设计: