设计模式-模板模式实际应用
背景描述:
最近在做个需求,将一个报表数据从Hadoop平台转移到TiDB,但是TiDB没有存储过程啥的,所以我们就打算用Java实现啦
具体实现思路: sql查询完数据之后将数据插入数据库临时表中,最后再转移到结果表中
使用模板设计模式是因为 整个报表数据使用了10+的临时表,我们对于临时表的处理步骤都一样,都是先查询出数据量,然后将数据分批插入(如果数据量过大的话)
抽象模板类设计:
package com.dfx.demo.service; import java.util.*; /** * 定义抽象类 模板方法 */ public abstract class OnePrimHaAbstract { /** * 得到需要插入数据库中的数据总量,用来判断数据是否过大,如果过多就分批插入数据 * @param methodName 被调用的方法名 * @return */ public abstract Integer getInsertDataNums(String methodName) throws Exception; /** * 将数据插入到数据库中 * @param methodName 被调用的方法名 * @param start 插入数据的开始位置 * @param end 插入数据的条数 */ public abstract void insertData2DB(String methodName,Integer start,Integer end)throws Exception; /** * 处理数据 * 1.查询数据量 * 2.(分批)将数据插入到数据库中 * @return */ public Integer HaProData(String methodName,Integer maxInsertNums ) throws Exception{ long starttime = System.currentTimeMillis(); //查询出数据量,再根据数据量分批插入数据 Integer sumdataNums =getInsertDataNums(methodName); List<Map<String,Integer>> limitList = getStartEndLimits(sumdataNums,maxInsertNums); if(limitList != null){ for(Map<String,Integer> limitListObj : limitList){ System.out.println(methodName+" - limitListObj:"+limitListObj); insertData2DB(methodName,limitListObj.get("start"),limitListObj.get("end")); } } System.out.println(methodName+" - 耗时:"+(System.currentTimeMillis()-starttime)/1000 +"s"); return sumdataNums; } /** * 临时表 or 结果表 数据清空,在重新插入数据到结果表中时需要先清空结果表的数据 * @param tablename 需要被清空的表名称 (eg: rs_Ha_Pro) */ public abstract void truncateConProTable(String tablename); /** * * 更新监控表的状态值 * @param tablename 被更新的表名称 作为入参条件(eg: rs_Ha_Pro) * @param tabledatanums 更新的表对应的数据量 * @param status 表名对应的状态值切换,正在更新 值为1,更新完毕可使用 值为0.(默认值为0) * @param remark 对应的表的备注信息 */ public abstract void updConProMonitor(String tablename,Integer tabledatanums,Integer status,String remark); /** * 返回数据分页结果信息(用于分批插入数据到数据库中-数据量过大时) * @param sumNumbers 总条数 * @param maxInsertNums 每次插入的最大的条数 * @return 分页结果list(seqid排序标识 从小到大,start 开始位置,end 插入条数) */ private List<Map<String,Integer>> getStartEndLimits(Integer sumNumbers,Integer maxInsertNums){ if(sumNumbers <= 0 ){return null;}//如果为0 则返回null int len = sumNumbers/maxInsertNums +1; int modlen = sumNumbers%maxInsertNums ; int start = 0; int end = maxInsertNums; List<Map<String,Integer>> limitList = new ArrayList<Map<String,Integer>>(); //因数据量过大,需要分批插入(每 maxInsertNums 条数据作为1批数据) len = sumNumbers/maxInsertNums +1; modlen = sumNumbers%maxInsertNums ; start = 0; end = maxInsertNums; if(sumNumbers < maxInsertNums){end=sumNumbers;} //如果总数量小于20w则不需要分批插入 for(int i=0;i<len;i++){ Map<String,Integer> limitMaps = new HashMap<String ,Integer>(); limitMaps.put("start",start); limitMaps.put("end",end); limitMaps.put("seqid", i); limitList.add(limitMaps); // System.out.println("第"+(i+1)+"次插入数据 limitMaps:"+limitMaps); // System.out.println("第"+(i+1)+"次插入数据 limitList:"+limitList); if(i==(len-2)){ end =modlen; } start +=maxInsertNums; } // 从小到大排序 Collections.sort(limitList,new Comparator<Map<String,Integer>>(){ @Override public int compare(Map<String,Integer> map1, Map<String,Integer> map2) { if( map1.get("seqid") > map2.get("seqid")){ return 1; } return -1; } }); return limitList; } }
抽象模板表的子类
(这里因为我的所有子类实现逻辑都几乎一致,只有最后调用的Dao不同,也就是执行的sql不同,所以我这里只定义了一个子类,在子类里面通过switch case 处理了,可根据需要定义不同的子类,分别实现抽象方法)package com.dfx.demo.service.impl;
import com.dfx.demo.dao.haProMapper; import com.dfx.demo.service.OnePrimHaAbstract; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * 定义抽象类 模板方法 */ @Service public class OnePrimHaAbstractImpl extends OnePrimHaAbstract { @Autowired private haProMapper haProMapper; /** * 得到需要插入数据库中的数据总量,用来判断数据是否过大,如果过多就分批插入数据 * @return */ public Integer getInsertDataNums(String methodName) throws Exception{ Integer resultNums = null; if(methodName != null ){ switch(methodName){ case "insertTmp1": resultNums = haProMapper.getTmp1nums(); break;
.........中间省略其它case ........ case "insertTmp20": resultNums = haProMapper.getTmp20nums(); break; } } return resultNums ; } /** * 将数据插入到数据库中 */ public void insertData2DB(String methodName,Integer start,Integer end)throws Exception{ if(methodName != null ){ switch(methodName){ case "insertHaProtmp1": haProMapper.insertTmp1(start,end); break;
.............中间省略其它case................. case "insertHaProtmp20": haProMapper.insertTmp20(start,end); break; } } } @Override public void truncateConProTable(String tablename) { haProMapper.truncateConProTable(tablename); } @Override public void updConProMonitor(String tablename, Integer tabledatanums, Integer status, String remark) { haProMapper.updConProMonitor(tablename,tabledatanums,status,remark); } }
Dao层的Mapper接口类
package com.dfx.demo.dao; import com.sun.org.glassfish.gmbal.ParameterNames; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; import java.util.Map; /** * 联络人数据接口 */ @Mapper public interface ContactAgentMapper { //获取临时表1 的数量 public Integer getTmp1nums(); //联络人临时表1 数据插入数据库 public void insertTmp1(@Param("start") Integer start, @Param("end") Integer end); ....... 中间省略临时表的数据量查询及数据插入方法定义......
/** * 临时表 or 结果表 数据清空,在重新插入数据到结果表中时需要先清空表的数据 * @param tablename 需要被清空的表名称 */ public void truncateConOnelifeTable(@Param("tablename") String tablename); }
Dao层的Mapper文件(sql)
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.dfx.demo.dao.ContactAgentMapper"> <!-- 临时表1 数据量查询 --> <select id="getTmp1nums" resultType="java.lang.Integer"> <![CDATA[ select count(*) from ( select distinct t.seqid ,t.name from rs_table_tmp4 t )tt ]]> </select> <!-- 临时表1 数据插入 --> <insert id="insertTmp1" > <![CDATA[ insert into rs_tmp1 ( `seqid`, `name`) select distinct t.seqid ,t.name from rs_table_tmp4 t order by t.seqid limit #{start},#{end} ]]> </insert> .........中间省略数据其它xml信息......... <!-- 清除联络人数据 临时表或结果表信息 --> <delete id="truncateConOnelifeTable" parameterType="java.lang.String"> <![CDATA[ truncate ${tablename} ]]> </delete> </mapper>
动态传入表名 执行sql
特别说明一下 truncateConProTable(String tablename) 这个方法,因为传到Dao层的是表明,在Mybatis里面记得用 ${} 去获取,而不是用 #{}去获取变量值。
原因:使用 #{} 获取的值经过了预编译,防止sql注入 会带单引号,整条sql执行会有语法问题。
使用示例(正确):
<delete id="truncateConProTable" parameterType="java.lang.String"> <![CDATA[ truncate ${tablename} ]]> </delete>
mybatis中的#和$的区别:
1、#将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。
如:where username=#{username},如果传入的值是111,那么解析成sql时的值为where username="111", 如果传入的值是id,则解析成的sql为where username="id".
2、$将传入的数据直接显示生成在sql中。
如:where username=${username},如果传入的值是111,那么解析成sql时的值为where username=111;
如果传入的值是;drop table user;,则解析成的sql为:select id, username, password, role from user where username=;drop table user;
3、#方式能够很大程度防止sql注入,$方式无法防止Sql注入。
4、$方式一般用于传入数据库对象,例如传入表名.
5、一般能用#的就别用$,若不得不使用“${xxx}”这样的参数,要手工地做好过滤工作,来防止sql注入攻击。
6、在MyBatis中,“${xxx}”这样格式的参数会直接参与SQL编译,从而不能避免注入攻击。但涉及到动态表名和列名时,只能使用“${xxx}”这样的参数格式。所以,这样的参数需要我们在代码中手工进行处理来防止注入。
【结论】在编写MyBatis的映射语句时,尽量采用“#{xxx}”这样的格式。若不得不使用“${xxx}”这样的参数,要手工地做好过滤工作,来防止SQL注入攻击。
最后在Controller层按照业务逻辑顺序调用处理即可。
Controller调用示例
package com.dfx.demo.web; import com.dfx.demo.dao.pojo.ContactAgentPojo; import com.dfx.demo.service.OnePrimContactsService; import com.dfx.demo.service.OnePrimContactsAbstract; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; /** * 联络人数据接口 */ @RestController public class OnePrimContacts { @Autowired private OnePrimHaAbstract OnePrimHaAbstract oneAbstract; @RequestMapping("/getOnePrimContactsTemplate") public Integer dealContactPro(){ Integer resultNums = null; try { resultNums = getContactPro(); } catch(Exception e){ System.out.println("数据获取失败,请注意检查,失败原因:"+ e); //在重新开始跑数据之前需要执行删除 清空所有临时表数据 dealContactPro();//失败了就重新开始跑数 } return resultNums; } private Integer getContactPro() throws Exception{ long starttime = System.currentTimeMillis(); //1.跑数据之前先清空所有的临时表信息 oneAbstract.truncateConProTable("rs_table_tmp1"); //2.开始执行获取临时表数据 Integer insertTmp1 = oneAbstract.HaProData("insertTmp1",200000); System.out.println("临时表1 insertTmp1"+insertTmp1); ....其它临时表的插入执行..... long endtime = System.currentTimeMillis(); System.out.println("整体方法耗时:"+(endtime-starttime)/1000 +"s"); return contactsPro; } }
整个模板模式主要应用在Service层,抽象类的定义,然后就是子类的实现。
模板父类:定义抽象方法(一个或多个),在非抽象方法中调用抽象方法 以及编写其它相同的业务逻辑
模板子类:继承抽象父类,实现抽象方法
调用: 声明父类对象的对象,调用父类的非抽象方法(即模板方法)即可。