利用下班时间在办公室忙里偷闲写的,和大家分享一下可能会用到的一种解决问题的技术思路,措辞有不严谨的地方,请广大网友轻点喷。
大家在做项目时候是否遇到动态分表查询需求?
报表通过数据集查询非常复杂以及数据展示慢的问题?
是否需要分表分页查询需求以及排序问题?
是否查询的数据是通过海量数据筛选而来?
如果您在做项目时也有以上需求问题,那么请耐心看完本篇文章,也许能给你提供一种不一样的思路,一种不一样的解决方案,以及你需要的答案。本篇文章所有涉及的思路以及方案都是基于实际项目需求以及所遇到的问题,以结果为导向进行抽丝剥茧分而治之逐步解决的,下面首先介绍一下项目背景。
#1. 项目背景
我所在项目是一个物联网项目,他其中的一个功能就是在终端分布着上千万的采集设备,每天定时或者实时采集数据,数据采集后上传到kafka,然后kafka消费者将原始数据存储到oracle数据库,根据不同的数据属性将数据分别存储在三张表,我们暂且叫record记录表,reading读数表和parameter表吧,到目前为止,每个表的数据量在5亿左右,随着终端设备的增加后期每天产生的数据在2000万左右。
在数据库中涉及的主要表结构中,每个终端设备都有一个唯一的标识SNID, 以及标识当前SNID所属的区域码,多个区域码组成一个大区,数据采集后,需要在每个公司进行不定期计费,在计费的过程中需要对当前设备历史所有的数据进行汇总,然后通过一些算法计算出当前设备关联的用户费率信息。
在数据分析和统计方面,根据项目需要,动态查询某一个大区下某一个时间段内所有设备数据采集情况,以及统计设备某一时间段通信情况等一系列复制的统计需要分析,在数据统计和展示方面,采用的是帆软报表为工具。
下面分表通过思维导图简单的勾勒出公司组织架构以及终端设备数据采集流程和报表方面的需求,方便大家直观了解整个项目需求背景,为后续产生的问题以及解决方案提供思路。
1.1组织架构图:
所有的数据采集终端设备都在挂载到大区级别所属组织机构下,而每个省公司下面有N多个大区,他们都有自己的组织机构编码ouid。
1.2数据采集流程图:
上千万个数据采集终端设备将数据上传到kafka中,kafka通过多个消费者将数据插入到指定的数据库对应的表。
1.3报表需求展示图:
在终端用户需求方面,会通过查询条件,查询省公司下面指定大区下挂载的所有数据采集终端设备,查询时间不固定,查询的大区不固定,以及查询的省公司不固定。
比如查询:2021-03-01到2021-05-30号期间,江苏省公司下大区1、大区2、大区3的终端设备数据采集情况。
2.问题需求
基于上述项目大体架构图,在实际生产应用中,产生一个问题,就是数据存储在record、 reading 、parameter 三张业务表,每张表业务数据5亿多,现在每天增量是200万,后续每天增量是2000万,组织机构信息存储在orgunit表中,省级组织机构与大区机构以及其他的下级机构数据大概在1万左后,涉及的终端设备信息存储在device表中,现有设备信息1000万,而每个终端设备所属用户信息存放在customer表中,与设备信息数据大致相同,现有用户信息为1000万左右。
由于项目处于建设阶段,数据需要实时查询,并且数据查询集成到业务系统里面,没有单独采用分析相关工具。现有的项目采用springmvc+dubbo +mybatis+oracle+redis技术架构,项目中存在大量手写sql语句,基于oracle数据的好多特性函数,已有的报表展示采用的也是通过数据库sql查询,然后将结果展示到报表中,sql结果比较复杂,最多的sql连接涉及到23张表,少者5张表,其中涉及到一些oracle专用统计分析函数,在使用查询过程中,只能查询一天的数据,并且查询时间在2分钟左右,经常出现查询奔溃问题。
以查询江苏公司大区1、大区2、大区3的数据为例:
```
SELECT *
FROM RECORD R
INNER JOIN READING RE
ON R.OU_ID = RE.OU_ID
AND R.SNID = RE.SNID
INNER JOIN PARAMETER P
ON R.SNID = P.SNID
AND R.OU_ID = P.OU_ID
INNER JOIN ORGUNIT O
ON O.OU_ID = R.OU_ID
INNER JOIN CUSTOMER C
ON C.OU_ID = R.OU_ID
AND C.SNID = R.SNID
WHERE R.CREATE_DATE BETWEEN TO_DATE('yyyy-mm-dd','2021-02-01') AND TO_DATE('yyyy-mm-dd','2021-03-01')
and R.OU_ID IN('10002','100003','100004','100005');
如上面的一个简单查询,从三个上亿表的数据中查询几十万数据,速度非常慢,经常听到下面的用户哀声哉道,吐槽查询问题,基于上述问题,我们决定忙里偷闲抽出一定时间来进行一个简单的技术改造,在技术改造的过程中,要求的一个原则是劲量少动现有的数据逻辑下,满足用户的实际需要。
毕竟,作为一个程序员,编程的第一法则还要是遵守的,
#3.解决方案
基于上述问题,我们找到了第一种解决方案,那就是对表进行分表操作,根据实际业务需求、组织机构架构图,终端数据采集设备增加速度、以及数据存储方式,首要任务就是对表进行分表操作,分表后可以满足现有十年数据内业务的需求,并且是对现有程序花最小代价即可进行改造,就能满足基本的需要,如果后期业务扩展超出超出当前逾期,我们可以在进一步做分库,暂且不讨论分库的问题。
根据组织机构架构图与数据存在的方式(每一个业务表中,会会有俩个字段,一个代码省公司字段,一个代表当前省公司下大区字段,也就是实际终端采集设备绑定的直属机构)下面以实际示例说明:
组织机构数据存储结构:
业务数据存储结构:
从业务数据可以看到,里面有俩个字段,一个是省公司编码,一个大区公司编码,所有我们着手从这里下手,进行分表逻辑处理,分表的逻辑为具体的表名省公司编码大区编码取模,具体取模的因子是根据当前省公司下大区公司数量决定定,示例如下:
江苏省公司ou_id 为,100001, 江苏省公司下有五个大区,编码分表为500001、500002、500003、500004、500005.那么分表后根据大区编码数量与大区编码值,将原表分成5个,并且通过r%n的方式进行分表,下面以伪代码示例:
public void test(){
String tableName="record";
Integer ouId=100001;
List subOuId = Arrays.asList(500001,500002,500003,500004,500005);
for(int i=0;i
System.out.println(tableName+"_"+ouId+"_"+i%subOuId.size());
}
}
最后分表后如下:
record_100001_0
record_100001_1
record_100001_2
record_100001_3
record_100001_4
通过这种方法,我们将原来一张表进行拆分后,分成了大约400多张表,每张表只存储当前省公司下当前大区的数据,数据拆分后,每张表的数据控制在几百万,将原来无法查询的数据控制在查询结果在3秒之内,查询慢的数据控制在几十毫秒。
在进行分表操作后,面临的第二问题,就是就是如何利用拆分后的表,以及兼容现有sql问题,一开始我们在网上找了一个分表插件,在实际应用中,发现不兼容oracle的一些统计分析函数,以及在多表关联的时候,查询有问题,在尝试了一段时间,发现改造工程比较浩大,果断放弃。然后结合项目现有实际应该,自己写了一个分表查询插件,下面先说一下逻辑:
思路1:
项目查询数据库使用的是mybatis,所有首先想到是的在mybatis的基础上进行改造,通过mybatis的Interceptor 拦截器在原有sql与执行执行进行拦截,将涉及到需要分表的表名进行替换。
思路2:
由于不同省公司下所挂载的大区公司数量不同,导致有的省公司分5张表,有的省公司需要分10张表,多着需要分30张表,所以想到通过配置文件,将各省公司需要进行分表的数量提前配置。
思路3:
那些表需要进行分表操作?分表操作的依据是什么?分表的一级前缀和二级后缀是什么?这是最后需要考虑的,结合mybatis,我们通过自定注解方式标注那些sql对应的表需要分表,那些不要。
思路4:
分表后,关于分页操作,之前采用的一个分页插件,不适合分表操作,所有涉及到分页问题,我们的思路是在原有的sql上执行返回具体的总数,然后通过计算分页相关的参数,如:第几页,起始行数,终止行数等。
思路5:
关于性能问题,如果我们分表后,涉及到查询10个表的数据,按照原有逻辑循环查询每个表数据,然后汇总的话,分表执行的时间可能不会比原来小多少,所有我们通过多线程问题,根据传入的大区数量决定执行几个线程进行同时查询,然后将结果返回给用户
下面言归正传,直接上代码:
首先定义一个注解,通过注解辨识那些表需要进行分表,分表后的前缀、后缀以及分表符号。
import java.lang.annotation.*;
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SqlInterceptor {
/**
* 涉及分表的名称
* @return
*/
String[]tableName()default "";
/**
* 分表规则
* @return
*/
Stringrule()default "_";
/**
* 分表后前缀
* @return
*/
StringtablePrefix()default "";
/**
* 分表后后缀
* @return
*/
StringtableSuffix()default "";
}
接着,定义一个SubMeterRole.properties文件,用来存放每个省公司下需要分多少张表,以及在取模运算过程中的分母值。
#分表规则配置
141800 = 30
128400 = 30
110000 = 15
133100 = 5
131700 = 5
114600 = 5
118900 = 5
131800 = 5
126200 = 5
其次在创建公共类用来解析properties文件:
public enum SubMeterRolePropertiesUtil {
INSTRANCE {
@Override
public String getValue(String key) {
Properties properties = new Properties();
try {
properties = PropertiesLoaderUtils.loadProperties(new ClassPathResource("config/SubMeterRole.properties"));
} catch (IOException e) {
LogUtil.error(e);
}
return properties.getProperty(key);
}
};
public abstract String getValue(String key);
public static void main(String[] args) {
System.out.println(INSTRANCE.getValue("141800"));
}
};
接着,我们实现一个mybatis的Interceptor的拦截器,然后对请求进入的sql进行拦截,拦截过程中只拦截SELECT类型的sql。然后根据我们前面定义的注解,判断是否需要分表,如果需要分表,就根据注解和properties配置文件组装sql,然后将组装好的sql传给session即可。
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
public class MybiatisInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
String id = mappedStatement.getId();
String sqlCommandType = mappedStatement.getSqlCommandType().toString();
Object object = boundSql.getParameterObject();
String mSql = analysisSql(id,sqlCommandType,sql,object);
System.out.println(mSql);
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, mSql);
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o,this);
}
@Override
public void setProperties(Properties properties) {
}
public String analysisSql(String id,String sqlType,String sql,Object objParameter) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
String mysql = sql;
try {
Class<?> classType = Class.forName(id.substring(0,id.lastIndexOf(".")));
//拦截select 的语句
if("SELECT".equals(sqlType)){
SqlInterceptor sqlInterceptor = classType.getAnnotation(SqlInterceptor.class);
String []tableNames= sqlInterceptor.tableName();
String pre = sqlInterceptor.tablePrefix();
String suf = sqlInterceptor.tableSuffix();
String rus = sqlInterceptor.rule();
String prefix =null;
String suffix=null;
StringBuffer sqlfag = new StringBuffer();
if(tableNames.length>0 && StringUtil.isNotBlank(pre) && StringUtil.isNotBlank(suf)){
if(objParameter instanceof Map){
Map<String,Object> map = (Map<String, Object>) objParameter;
prefix = map.containsKey(pre)?map.get(pre).toString():null;
suffix = map.containsKey(suf)?map.get(suf).toString():null;
}else{
Class c =objParameter.getClass();
Field field[] = c.getDeclaredFields();
Optional<Field> field1 = Arrays.stream(field).distinct().filter(e->e.getName().equals(pre)).findFirst();
Optional<Field> field2 = Arrays.stream(field).distinct().filter(e->e.getName().equals(suf)).findFirst();
if (field1.isPresent() && field2.isPresent()){
prefix =BeanUtils.getProperty(objParameter,pre);
suffix = BeanUtils.getProperty(objParameter,suf);
}
}
for(String str:tableNames){
sqlfag.append(str).append(rus).append(prefix).append(rus).append(suffix);
if(sql.contains(str)){
mysql= mysql.replace(str,sqlfag);
}
sqlfag.setLength(0);
}
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return mysql;
}
}
同时将拦截器配置到mybatis-conf.xml中,
<typeHandlers>
<typeHandler handler="org.apache.ibatis.type.InstantTypeHandler" />
<typeHandler handler="org.apache.ibatis.type.LocalDateTimeTypeHandler" />
<typeHandler handler="org.apache.ibatis.type.LocalDateTypeHandler" />
<typeHandler handler="org.apache.ibatis.type.LocalTimeTypeHandler" />
<typeHandler handler="org.apache.ibatis.type.OffsetDateTimeTypeHandler" />
<typeHandler handler="org.apache.ibatis.type.OffsetTimeTypeHandler" />
<typeHandler handler="org.apache.ibatis.type.ZonedDateTimeTypeHandler" />
</typeHandlers>
<!--分表插件-->
<plugins>
<plugin interceptor="com.petrochina.gss.report.interceptor.MybiatisInterceptor">
</plugin>
</plugins>
到此为止,我们可以拦截sql,并且根据配置需要进行组装sql以及查询数据库了,接下来我们优化查询速度,根据配置通过线程池技术通过多线程进行查询,提高查询效率,首先需要我们在applicationContext.xml文件中配置一个全局的异步线程池。
<!-- 自定义异步线程池 -->
<bean id="queryThreadPoolTaskExecutor"
class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 核心线程数 -->
<property name="corePoolSize" value="16" />
<!-- 最大线程数 -->
<property name="maxPoolSize" value="100" />
<!-- 队列最大长度 >=mainExecutor.maxSize -->
<property name="queueCapacity" value="10000" />
<!-- 线程池维护线程所允许的空闲时间 -->
<property name="keepAliveSeconds" value="60" />
<!-- 线程池对拒绝任务(无线程可用)的处理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
</property>
<property name="threadNamePrefix" value="batch-openaccount" />
</bean>
接着改造mybatis的mapper接口,通过集成基础类的方法,对查询接口增加多线程技术。
public class BaseReportDao<T> {
@Resource(name="dynamicSqlSessionTemplate")
private SqlSession sqlSessionTemplate;
protected Class<T> entityClass;
protected String className;
private Future future;
@Autowired
private ThreadPoolTaskExecutor batchOpenAccountThreadPoolTaskExecutor;
public BaseReportDao()
{
Type genType = getClass().getGenericSuperclass();
Type[] params = ((ParameterizedType)genType).getActualTypeArguments();
this.entityClass = ((Class)params[0]);
this.className = entityClass.getName();
}
public SqlSession getSqlSessionTemplate() {
return this.sqlSessionTemplate;
}
public String getClassName()
{
return this.className;
}
public <E> List<E> query(String mapperMethod, Object params,List<Integer> list) {
String className = getClass().getGenericSuperclass().getTypeName();
int startindex = className.indexOf("<");
int endindex = className.indexOf(">");
String classNameType = className.substring(startindex+1,endindex);
List<Callable<List<T>>> callables = new ArrayList<>();
List<Future<List<T>>> futures = new ArrayList<>();
try {
Class<?> classType = Class.forName(classNameType);
SqlInterceptor sqlInterceptor = classType.getAnnotation(SqlInterceptor.class);
String pre = sqlInterceptor.tablePrefix();
String suf = sqlInterceptor.tableSuffix();
if(StringUtil.isNotBlank(pre) && StringUtil.isNotBlank(suf)){
if(params instanceof Map){
//如果入参是map
Map<String,Object> map = (Map) params;
if(map.containsKey(pre) && map.containsKey(suf)){
int modify = getModify(map.get(pre).toString());
if(list.size()>1){
list.forEach(e->{
Map<String,Object> map1 = new HashMap<>();
map1.putAll((Map<String,Object>) params);
if(modify==999) {
map1.put(suf,0);
}else{
map1.put(suf,e%modify);
}
callables.add(new Callable<List<T>>() {
@Override
public List<T> call() throws Exception {
List<T> list = getSqlSessionTemplate().selectList(getMapperMethod(mapperMethod), map1);
return list;
}
});
});
futures= batchOpenAccountThreadPoolTaskExecutor.getThreadPoolExecutor().invokeAll(callables);
}else{
((Map<String, Object>) params).put(suf,0);
return getSqlSessionTemplate().selectList(getMapperMethod(mapperMethod), params);
}
}else{
throw new Exception("传入的参数中没有包含@SqlInterceptor 注解中tablePrefix的值 或者tableSuffix 的值!");
}
}else{
String ouId = org.apache.commons.beanutils.BeanUtils.getProperty(params,pre);
int modify = getModify(ouId);
//如果入参数po对象
if(list.size()>1){
for(Integer i :list){
Object obj = params.getClass().newInstance();
BeanUtils.copyProperties(params,obj);
Field field = params.getClass().getDeclaredField(suf);
String suff = null;
if(modify==999){
suff = 0+"";
}else{
suff = i%modify+"";
}
field.setAccessible(true);
field.set(obj,suff);
MyThread myThread = new MyThread(mapperMethod,obj);
callables.add(myThread);
futures.add(future);
}
futures= batchOpenAccountThreadPoolTaskExecutor.getThreadPoolExecutor().invokeAll(callables);
}else{
Field field = params.getClass().getDeclaredField(suf);
field.setAccessible(true);
field.set(suf,0);
return getSqlSessionTemplate().selectList(getMapperMethod(mapperMethod), params);
}
}
}else {
return getSqlSessionTemplate().selectList(getMapperMethod(mapperMethod), params);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
List<T> tList = new ArrayList<>();
try {
for (Future<List<T>> future :futures){
tList.addAll(future.get());
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return (List<E>) tList;
}
public String getMapperMethod(String methodName) {
return new StringBuilder(className).append(".").append(methodName).toString();
}
class MyThread implements Callable<List<T>>{
private Object params;
private String mapperMethod;
public MyThread(String mapperMethod,Object params){
this.params = params;
this.mapperMethod = mapperMethod;
}
@Override
public List<T> call() throws Exception {
List<T> list = getSqlSessionTemplate().selectList(getMapperMethod(mapperMethod), params);
return list;
}
}
public int getModify(String ouId){
String val = SubMeterRolePropertiesUtil.INSTRANCE.getValue(ouId);
if(StringUtil.isNotBlank(val)){
return Integer.parseInt(SubMeterRolePropertiesUtil.INSTRANCE.getValue(ouId));
}else{
return 999;
}
}
public int queryCount(String mapperMethod, Object params,List<Integer> list) {
Integer count = 0;
String className = getClass().getGenericSuperclass().getTypeName();
int startindex = className.indexOf("<");
int endindex = className.indexOf(">");
String classNameType = className.substring(startindex+1,endindex);
List<Callable<Integer>> callables = new ArrayList<>();
List<Future<Integer>> futures = new ArrayList<>();
try {
Class<?> classType = Class.forName(classNameType);
SqlInterceptor sqlInterceptor = classType.getAnnotation(SqlInterceptor.class);
String pre = sqlInterceptor.tablePrefix();
String suf = sqlInterceptor.tableSuffix();
if(StringUtil.isNotBlank(pre) && StringUtil.isNotBlank(suf)){
if(params instanceof Map){
//如果入参是map
Map<String,Object> map = (Map) params;
if(map.containsKey(pre) && map.containsKey(suf)){
int modify = getModify(map.get(pre).toString());
if(list.size()>1){
list.forEach(e->{
Map<String,Object> map1 = new HashMap<>();
map1.putAll((Map<String,Object>) params);
if(modify==999) {
map1.put(suf,0);
}else{
map1.put(suf,e%modify);
}
callables.add(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Integer integer = getSqlSessionTemplate().selectOne(getMapperMethod(mapperMethod), map1);
return integer;
}
});
});
futures= batchOpenAccountThreadPoolTaskExecutor.getThreadPoolExecutor().invokeAll(callables);
}else{
((Map<String, Object>) params).put(suf,0);
return getSqlSessionTemplate().selectOne(getMapperMethod(mapperMethod), params);
}
}else{
throw new Exception("传入的参数中没有包含@SqlInterceptor 注解中tablePrefix的值 或者tableSuffix 的值!");
}
}else{
String ouId = org.apache.commons.beanutils.BeanUtils.getProperty(params,pre);
int modify = getModify(ouId);
//如果入参数po对象
if(list.size()>1){
for(Integer i :list){
Object obj = params.getClass().newInstance();
BeanUtils.copyProperties(params,obj);
Field field = params.getClass().getDeclaredField(suf);
String suff = null;
if(modify==999){
suff = 0+"";
}else{
suff = i%modify+"";
}
field.setAccessible(true);
field.set(obj,suff);
callables.add(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Integer integer = getSqlSessionTemplate().selectOne(getMapperMethod(mapperMethod), obj);
return integer;
}
});
}
futures= batchOpenAccountThreadPoolTaskExecutor.getThreadPoolExecutor().invokeAll(callables);
}else{
Field field = params.getClass().getDeclaredField(suf);
field.setAccessible(true);
field.set(suf,0);
return getSqlSessionTemplate().selectOne(getMapperMethod(mapperMethod), params);
}
}
}else {
return getSqlSessionTemplate().selectOne(getMapperMethod(mapperMethod), params);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
try {
for (Future<Integer> future :futures){
count+= future.get();
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return count;
}
}
到此为此,集成mybatis插件就写完了,下面我们通过几个示例测试一下,首先需要我们自定义的到类继承BaseReportDao 类,然后通过BaseReportDao类的query方法查询数据集,queryCount方法查询数据总数用于分页。
public class SmChargeRecordDao extends BaseReportDao<SmChargeRecord> {
}
接口定义。
public interface SmChargeRecordService {
List<SmChargeRecord> querySmChargeRecord(String ouId, Integer areaId,List<Integer> list);
List<SmChargeRecord> querySmChargeRecordByDto(SmChargeRecordDto smChargeRecordDto,List<Integer> list);
Integer querySmChargeRecordByDtoCount(SmChargeRecordDto smChargeRecordDto,List<Integer> list);
}
接口的实现。
@Service
public class SmChargeRecordServiceImpl implements SmChargeRecordService {
@Autowired
private SmChargeRecordDao smChargeRecordDao;
@Override
public List<SmChargeRecord> querySmChargeRecord(String ouId, Integer areaId,List<Integer> list) {
Map map = new HashMap();
map.put("ouId",ouId);
map.put("areaId",areaId);
return smChargeRecordDao.query("querySmChargeRecord",map,list);
}
@Override
public List<SmChargeRecord> querySmChargeRecordByDto(SmChargeRecordDto smChargeRecordDto, List<Integer> list) {
return smChargeRecordDao.query("querySmChargeRecordByDto",smChargeRecordDto,list);
}
@Override
public Integer querySmChargeRecordByDtoCount(SmChargeRecordDto smChargeRecordDto,List<Integer> list){
return smChargeRecordDao.queryCount("querySmChargeRecordByDtoCount",smChargeRecordDto,list);
}
}
Mapper.xml文件设置。
<select id="querySmChargeRecordByDto" resultMap="BaseResultMap">
SELECT * FROM (
SELECT R.OU_ID,R.IN_PARAMATERS,ROWNUM AS NUMID FROM SM_CHARGE_RECORD R WHERE R.CHARGE_TIME <![CDATA[ >= ]]>TO_DATE('2021-07-6','yyyy-mm-dd'))
SM_CHARGE WHERE NUMID<![CDATA[ < ]]>1000
</select>
<select id="querySmChargeRecordByDtoCount" resultType="java.lang.Integer">
SELECT count(*) FROM SM_CHARGE_RECORD R WHERE R.CHARGE_TIME <![CDATA[ >= ]]>TO_DATE('2021-07-6','yyyy-mm-dd')
</select>
测试用例,即支持通过对象传值,也支持通过Map等传值。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:applicationContext.xml"})
public class TestSmChargeRecordService {
@Resource
private SmChargeRecordService smChargeRecordService;
//测试开始
@Before
public void setUp() throws Exception {
System.out.println("测试开始");
}
//测试结束
@After
public void tearDown() throws Exception {
System.out.println("测试结束");
}
@Test
public void testQuerySmChargeRecord(){
List<Integer> list =Arrays.asList(31,32,33);
List<SmChargeRecord> smChargeRecordList = smChargeRecordService.querySmChargeRecord("141800", 2,list);
System.out.println(smChargeRecordList);
}
@Test
public void testQuerySmChargeRecordByDto(){
SmChargeRecordDto smChargeRecordDto = new SmChargeRecordDto();
smChargeRecordDto.setOuId("141800");
smChargeRecordDto.setComouId("141800");
List<Integer> list =Arrays.asList(504630,504421,504602,504603);
Integer integer = smChargeRecordService.querySmChargeRecordByDtoCount(smChargeRecordDto,list);
List<SmChargeRecord> smChargeRecordList = smChargeRecordService.querySmChargeRecordByDto(smChargeRecordDto,list);
System.out.println(smChargeRecordList);
}
}
最后通过postman测试,发现可以在3到5秒内可以返回十几万数据。
4.归纳总结
在实际项目应该中,并是不什么好拿来用什么,需要结合自己项目的实际情况,可以利用已有的一些功能可以达到事半功倍的效果,其次在大的项目中已有的功能逻辑复杂,涉及几百万代码,并不是我们每一个人都了解项目的全部功能,如果改动的话可能牵一发而动全身,那么在此情况下就需要我们另辟奇径在不影响现有功能情况下如何满足新的需求提供一种新的思路。
最后国庆节祝福大家吃的开心,玩的快乐!!!!