在之前的博文中,我写过一篇思考设计数据库管理平台的文章,当时在设计时,没有提及到一个点,那就是如果说我的系统要支持一个N多种数据库类型,并且当前市面上每一种数据库之间的表字段,大小写区分,字符长度运算可能都完全不同.
就比如Oracle数据库中,在utf8编码下,一个汉字占用2个字节,4000长度的varchar2可以存储2000汉字没有问题,但是在国产达梦数据库中,utf8编码下,一个汉字占用3个字符或者经过配置后占用1个字符,这样的话做数据迁移不经过处理,就必然会报错.
还有就是导出导入功能,如果单纯使用insert语句肯定没有问题,简单语句是通用的,但是数百万数千万数亿数据用insert怕是要跑一天.在oralce中,就有dmpdp这种装载器,在mysql,达梦数据库这些数据库中也存在不同的各自数据库适配的装载器.
这种多对多需要互相匹配的情况,代码很容易就会变成:
if("oralce".equals(dbtype)){
if("varchar2".equals(column)){
}
}else if("dm".equals(dbtype)){
if("varchar2".equals(column)){
}
}else if("mysql".equals(dbtype)){
if("varchar2".equals(column)){
}
}else if("db2".equals(dbtype)){
if("varchar2".equals(column)){
}
}else if("mongo".equals(dbtype)){
if("varchar2".equals(column)){
}
}else if("redis".equals(dbtype)){
if("varchar2".equals(column)){
}
}
如上,如果说以后多一种数据库类型,那么在这一个大if块里面需要再去加一个else if,如果说库的字段对比不一致,那么又可能增加内层中无数的if,如果我们再去考虑一下,oracle是可以给mysql转化数据的,也是可以给达梦数据库转换数据的!!!那么就炸了,需要在if中再次嵌套,一个一个去比对,一对多关系需要一个个重写.这种代码相信很多人写过,并且一个if块及其简单就能写到5000行以上,万一新人接手,基本上就可以准备写辞职信了.
所以,问题就来了,这种结构怎么优化?有没有办法?
在程序设计的23种常见的设计模式中,有一个模式叫做:"策略模式",我们可以看看百度的解释:
策略模式
在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。
介绍
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。
关键代码:实现同一个接口。
应用实例: 1、诸葛亮的锦囊妙计,每一个锦囊就是一个策略。 2、旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。 3、JAVA AWT 中的 LayoutManager。
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。
使用场景: 1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。 2、一个系统需要动态地在几种算法中选择一种。 3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
看起来很大上的样子,也有点云里雾里的,但是我们可以看到策略模式主要解决的其实就是if else带来的难以维护和复杂性的问题,
在最后一行,百度也有一句话:如果说一个系统的策略多于四个,那么需要考虑使用混合模式,解决策略类膨胀问题!
那么思考一下,我们现在的需求,单单说数据库类型,就不可能是4个以内吧.所以说单纯的策略模式对于我们来说,可能还不够用,这里我找到了一个可以用于搭配的另外一个设计模式:工厂模式
相信大家对于工厂模式应该不陌生吧.太多地方用了,就不介绍了,那么我们看看如果说策略模式和工厂模式整合在一块,会有什么好处呢?
如果说我们单纯的使用策略模式,那么会产生大量的策略类,每次要使用,都需要new一个策略类来进行实现,并无法达到我们想象中的优雅的效果.在结合工厂模式之后,我们由工厂去生成策略类,我们只需要专注于去实现我们内部的处理逻辑即可.
来,开始上代码(仅为自己写的一些简单的解决方案,大佬勿喷):
首先,我们创建我们自己的一个工厂类,用于生产策略:
package com.zach.factory;
import java.util.HashMap;
import java.util.Map;
/**
* 功能描述:
* 〈策略工厂类〉
*
* @return :
* @author : zach
*/
public class EtlDbFactory {
public static final String MYSQL = "MYSQL";
public static final String ORACLE = "ORACLE";
public static final String VERTICA = "VERTICA";
public static final String DM = "DM";
public static final String SQLSERVER = "SQLSERVER";
public static final String SYBASE = "SYBASE";
public static final String DB2 = "DB2";
public static final String POSTGRESQL = "POSTGRESQL";
public static final String GBASE = "GBASE";
private static EtlDbFactory factory = new EtlDbFactory();
private EtlDbFactory(){}
private static Map etlMakeDbEinvoiceMap = new HashMap<>();
static {
etlMakeDbEinvoiceMap.put(MYSQL,new MysqlEtlService());
etlMakeDbEinvoiceMap.put(ORACLE,new OracleEtlService());
etlMakeDbEinvoiceMap.put(VERTICA,new VerticaEtlService());
etlMakeDbEinvoiceMap.put(DM,new DmEtlService());
etlMakeDbEinvoiceMap.put(SQLSERVER,new SqlServerEtlService());
etlMakeDbEinvoiceMap.put(SYBASE,new SybaseEtlService());
etlMakeDbEinvoiceMap.put(DB2,new DB2EtlService());
etlMakeDbEinvoiceMap.put(POSTGRESQL,new PostgreSqlEtlService());
etlMakeDbEinvoiceMap.put(GBASE,new GbaseEtlService());
}
public EtlMakeDbEinvoice creator(String type){
return etlMakeDbEinvoiceMap.get(type);
}
public static EtlDbFactory getInstance(){
return factory;
}
}
这个类中,我们引入了我们常用的一些数据库类型,在数据库之间的数据交换业务中,我们通常是会遇到源库以及目标库两种数据库的情况的,那么这里的工厂类,针对的仅仅是对源库的封装.
然后定义我们的策略分发接口:
package com.zach.factory;
import java.util.Map;
/**
* 功能描述:
* 〈策略工厂模式分发处理etl多dbType类型〉
*
* @return :
* @author : zach
*/
public interface EtlMakeDbEinvoice {
//数据装载接口
boolean makeDbLoading(Map paramMap);
//建表语句装载接口
boolean createDbTable(Map paramMap);
}
因为这个功能暂时只是完成了从源库中获取数据集,然后数据处理后调用各种不同的装载器去给不同的目标库执行装载,所以就暂时写了两个方法,以后看需求可以增加.
当以上两个类完成后,我们的策略工厂以及分发接口就完成了,那么剩下的,我们就需要根据不同的库,去创建不同的实现类去集成分发接口,然后实现去除每一个数据库类型一个ifelse的情况.
例如我们写一个mysql为源库的实现:
package com.zach.factory;
import com.zach.domain.ReturnMsgInfo;
import com.zach.timeTask.etlLogsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class MysqlEtlService extends CommonLoadingService implements EtlMakeDbEinvoice {
Logger logger = LoggerFactory.getLogger(MysqlEtlService.class);
private ReturnMsgInfo returnMsgInfo;
@Override
public boolean makeDbLoading(Map paramMap) {
InitData(paramMap);
returnMsgInfo = super.switchDb();
//根据返回值,开始写入日志
PubLogService(paramMap,returnMsgInfo);
return true;
}
@Override
public boolean createDbTable(Map paramMap) {
InitData(paramMap);
//调用公共的建表语句列数据生成方法
returnMsgInfo = CommonCreateTableService.createTableStr(paramMap);
if(0 == returnMsgInfo.getCode()){
returnMsgInfo = super.switchDb();
//根据返回值,开始写入日志
PubLogService(paramMap,returnMsgInfo);
}else{
paramMap.put("log",returnMsgInfo.getMsg());
etlLogsService.startTaskLogSuccess(paramMap);//修改任务表
etlLogsService.taskItemLogAndPath(paramMap);//记录日志表+记录装载错误log的位置
}
return true;
}
}
那么我们是通过为什么方式来调用到具体的实现方法呢?
我们可以通过调用我们预定的一些配置,或者说从数据库中查询,我们当前源库到底是什么类型的库?然后将类型传输给我们工厂类,然后由我们的策略工厂分发到我们每个源库类型的实现类中,比如:
public static void insertExec(Map paramMap){
Map rtnMap = new HashMap<>();
//策略工厂处理源数据类型情况
String SourceDbType = DictUtil.returnDbTypeDBTECHNOLOGYCLASSID(((etlDsaDbVO) paramMap.get("etlDsaDbVOSource")).getDbServerType());//源表数据库类型
EtlMakeDbEinvoice etlMakeDbEinvoice = EtlDbFactory.getInstance().creator(SourceDbType);
etlMakeDbEinvoice.makeDbLoading(paramMap);
}
以上这段代码,我通过查询数据库,获取到了SourceDbType,也就是我的源库类型,然后通过EtlDbFactory这个工厂类解析后,我们调用makeDbLoading的时候,自动分发到我们的MysqlEtlService类中处理
这样的话,我们就解决了我们第一层ifelse的问题,那么第二层呢?不能忘了我们源库有七八种,那么对应的,我们一个源库肯定也是要对照6-7中目标库的,每个目标库的装载器都不同,我们没有一种通用的装载器去实现不同的数据类型.
既然我们已经使用了策略工厂这种形式去优化代码,那么不可能说我们又在每一个源库的实现类中去写入大量的:
if("mysql".equals(目标库) && "mysql".equals(源库)){
使用mysql装载器
}else if("Oralce".equals(目标库) && "mysql".equals(源库)){
使用Oracle装载器
}else if(){
XXXXXX
}else if(){
XXXXXX
}else if(){
XXXXXX
}else if(){
XXXXXX
}
这样相当于没有完成我们的目标,但是如果说这个时候再使用一层策略工厂去初始化目标库?我是觉得太繁杂.有点得不偿失.所以我选择了一种折中的封装方案,因为所有的装载器他都一个可以定义的数据格式,那比如说,达梦数据库的装载器要求一行数据,每个列之间使用|||分隔,多行数据中使用@@来区别行,在所有的装载器中,这个分隔符格式是可以由我们自定义的,所以我们完全可以将所有的数据的获取封装到一起,只去处理装载问题,
这样的话,相当于把之前的mysql,oralce,达梦等数据库的实现类中每个实现类都要写一遍针对各个数据库的查询和装载,压缩到一个公共方法中,在装载器规则可变的情况下,只需要实现公共处理方法,如果说目标库不支持自定义分隔符,那么可以自己再去重写,节约大量代码,
如:
@Override
public boolean makeDbLoading(Map paramMap) {
InitData(paramMap);
returnMsgInfo = super.switchDb();
//根据返回值,开始写入日志
PubLogService(paramMap,returnMsgInfo);
return true;
}
public void InitData(Map paramMap){
paramMapData = paramMap;
etlList = (etlTaskConfigVO) paramMap.get("etlTaskConfigVO");//etl_starttasklog/etl_startconfig/etl_data_extract_config 配置信息
etlDsaDbVOSource = (etlDsaDbVO) paramMap.get("etlDsaDbVOSource");// etl_data_extract_config/etl-dsa-info/etl-db-info 源数据库信息
etlDsaDbVOTarget = (etlDsaDbVO) paramMap.get("etlDsaDbVOTarget");// etl_data_extract_config/etl-dsa-info/etl-db-info 目标数据库信息
execSql = SQLUtil.trimEnterAndSpace(SQLUtil.removeCommnetFromSQL(etlList.getStartconfigExtractcode()));//取出原始sql中的注释,空格,换行符,前置空格等
TargetDbType = DictUtil.returnDbTypeDBTECHNOLOGYCLASSID(((etlDsaDbVO) paramMap.get("etlDsaDbVOTarget")).getDbTechnologyClassId());//目标表数据库类型
TargetTableName = etlList.getStartconfigTargettablename().toUpperCase();//大写目标表名
fileExtractDirPath = ETLGlobal.getfileExtractDirPath() + etlList.getTasklogCode();//初始日志/数据文件存储路径
fileExtractBakDir = ETLGlobal.getFileExtractBakDir() + etlList.getTasklogCode();//错误日志备份目录存储路径
FileUtils.createFolder(fileExtractDirPath);//创建数据文件存储路径
}
public ReturnMsgInfo switchDb(){
switch (TargetDbType.toUpperCase()){
case "MYSQL":
returnMsgInfo = MYSQL();
break;
case "ORACLE":
returnMsgInfo = ORACLE();
break;
case "DM":
returnMsgInfo = DM();
break;
default:
logger.error("匹配不到目标数据库类型,请检查类型: " + TargetTableName.toUpperCase());
returnMsgInfo.error();
break;
}
return returnMsgInfo;
}
/**
* 功能描述:
* 〈当目标数据库为mysql时的处理〉
*
* @return : com.zach.domain.ReturnMsgInfo
* @author : zach
*/
public ReturnMsgInfo MYSQL(){
logger.info("进入目标数据为mysql情况");
returnMsgInfo = new ReturnMsgInfo();
returnMsgInfo.success("任务执行完成,执行的sql语句是:" + execSql);
try(Connection conn = DBConnection.getConnection(etlDsaDbVOSource.getDbDriver(),etlDsaDbVOSource.getDbUrl(),etlDsaDbVOSource.getDsaInfoUsername(),etlDsaDbVOSource.getDsaInfoPassword()); Statement stat = conn.createStatement(); ResultSet rs = stat.executeQuery(execSql)) {
Map rtnMap = etlExecMYSQLToolService.createDataFile(TargetTableName,rs,fileExtractDirPath);//生成数据文件
if("0".equals(rtnMap.get("code").toString())){
String dataFilePath = fileExtractDirPath + File.separator + TargetTableName + ".txt";
String batStr = "LOAD DATA LOCAL INFILE '" + dataFilePath + "' INTO TABLE " + TargetTableName + " FIELDS TERMINATED BY ','OPTIONALLY ENCLOSED BY '\"'LINES TERMINATED BY '\r\n'".replaceAll("\\\\","/");
//执行命令语句(MYSQL的LOADDATA语句,是通过mysql指令语句执行的,而不是bat或者sh的命令语句和组件)
stat.executeQuery(batStr);
}else{
//数据文件生成失败,记录错误
returnMsgInfo.error("数据文件生成失败");
}
return returnMsgInfo;
}catch (Exception e){
e.printStackTrace();
returnMsgInfo.error("任务执行失败,执行的sql语句是:" + execSql + "失败的原因是:" + e.getMessage());
return returnMsgInfo;
}
}
这样就可以最大程度减少代码的重复性.
写到最后,给我的感觉就是,设计模式的使用,可以在一些情况下,给我们的代码和设计带来很神奇的改变和升级,但是设计模式的引入,肯定是要在一些确定需求,并且不引入会给代码可读性或者性能带来很恶心的影响的时候再去引入,会很好用,需要好好评审.
不能说觉得这个地方也许可能用设计模式就二话不说直接上设计模式,看着高端,但可能会反其道而行之,大大降低代码阅读性.