SQL函数TIMEDIFF在Java程序中使用报错的问题分析
需求背景
(读者可略过)
司机每天从早到晚都会去到不同的自动售货机上补货,而且补货次数和路线等也是因人而异,补货依据是由系统优化并指派。但是目前系统还无法实施有效指挥和优良的补货策略,司机的补货活动因此变得较为随意和散漫。为了有效跟踪司机补货,计算司机补货效率,也为了便于HR月底计算司机绩效,因此提出需求需要根据司机补货记录数据阶段性地计算出补货情况统计值。
需求中指出,按天按司机手机号为统计维度,需要统计司机当天总共补货的机器数(相同机器补多次也只能算一台机器);统计司机当天总共补货的次数(自然小时内补了货只算一次);统计司机当天补货时长,即统计从司机第一次补货开始时间至当天最后一次补货结束时间为止之间的时长。
本次需求背景来自于售货机供应链系统。
需求实现
项目实现基于SpringBoot基础框架,搭配BeetlSql实现ORM,以下SQL均是基于此的MarkDown格式语句,另基于JDK 1.8,不过针对当前主题,以上环境参数均可忽略。
1、统计司机补货总机器数
1 SELECT 2 t.count_date, 3 t.phone_num, 4 count(1) AS 'totalCount' 5 FROM 6 ( 7 SELECT 8 DATE_FORMAT(supplement_time, "%Y-%m-%d") AS count_date, 9 phone_num, 10 device_id 11 FROM 12 cardslot_supplement_record 13 WHERE 1 = 1 14 @if(!isEmpty(startDate)){ 15 AND supplement_time > #startDate# 16 @} 17 @if(!isEmpty(endDate)){ 18 AND supplement_time < CONCAT(#endDate#, " 23:59:59") 19 @} 20 GROUP BY 21 count_date, 22 phone_num, 23 device_id 24 ) t 25 GROUP BY 26 t.count_date, 27 t.phone_num
2、统计司机补货总次数
1 SELECT 2 t.count_date, 3 t.phone_num, 4 count(1) 'totalCount' 5 FROM 6 ( 7 SELECT 8 DATE_FORMAT(supplement_time, "%Y-%m-%d") AS count_date, 9 phone_num, 10 DATE_FORMAT( 11 supplement_time, 12 "%Y-%m-%d %H" 13 ) AS 'count_date_hour' 14 FROM 15 cardslot_supplement_record 16 WHERE 1 = 1 17 @if(!isEmpty(startDate)){ 18 AND supplement_time > #startDate# 19 @} 20 @if(!isEmpty(endDate)){ 21 AND supplement_time < CONCAT(#endDate#, " 23:59:59") 22 @} 23 GROUP BY 24 count_date, 25 phone_num, 26 count_date_hour 27 ) t 28 GROUP BY 29 t.count_date, 30 t.phone_num
3、统计司机补货总时长
1 SELECT 2 DATE_FORMAT(supplement_time, "%Y-%m-%d") AS 'count_date', 3 phone_num, 4 SEC_TO_TIME( 5 UNIX_TIMESTAMP(max(supplement_end_time)) - UNIX_TIMESTAMP(min(supplement_start_time)) 6 ) AS 'diffTime' 7 FROM 8 cardslot_supplement_record 9 WHERE 1 = 1 10 @if(!isEmpty(startDate)){ 11 AND supplement_time > #startDate# 12 @} 13 @if(!isEmpty(endDate)){ 14 AND supplement_time < CONCAT(#endDate#, " 23:59:59") 15 @} 16 GROUP BY 17 count_date, 18 phone_num
接收查询结果的POJO类如下:
1 // 司机手机号码 2 private String phoneNum; 3 4 // 统计日期 5 private String countDate; 6 7 // 统计数量 8 private Integer totalCount; 9 10 // 时间差 11 private String diffTime;
在做测试执行查询时,前两个SQL执行都正常,最后的SQL在IDE工具中执行也是OK的,但是在Java程序中运行时,出现了如下异常:
1 Sql异常┏━━━━━ Debug [cardslotSupplementRecord.queryDriverReplenishmentD...] ━━━ 2 ┣ SQL: SELECT DATE_FORMAT(supplement_time, "%Y-%m-%d") AS 'count_date', phone_num, SEC_TO_TIME( UNIX_TIMESTAMP(max(supplement_end_time)) - UNIX_TIMESTAMP(min(supplement_start_time)) ) AS 'diffTime' FROM cardslot_supplement_record WHERE 1 = 1 AND supplement_time > ? AND supplement_time < CONCAT(?, " 23:59:59") GROUP BY count_date, phone_num 3 ┣ 参数: [2018-08-01 00:00:00, 2018-09-06 23:59:59] 4 ┣ 位置: com.bluepay.operation.service.impl.DriverReplenishmentSummaryExportServiceImpl.executeQueryAndExport(DriverReplenishmentSummaryExportServiceImpl.java:46) 5 ┗━━━━━ Debug [ ERROR:Bad format for Time '162:30:34' in column 3] ━━━ 6 7 [ERROR] 2018-09-06 16:22:56,856 -2592- [pool-3-thread-1] [com.bluepay.operation.service.export.impl.ExportBlockingQueueServiceImpl] 下载任务出现异常 8 org.beetl.sql.core.BeetlSQLException: java.sql.SQLException: Bad format for Time '162:30:34' in column 3 9 at org.beetl.sql.core.SQLScript.select(SQLScript.java:403) 10 at org.beetl.sql.core.SQLManager.select(SQLManager.java:475) 11 at org.beetl.sql.core.SQLManager.select(SQLManager.java:461) 12 at org.beetl.sql.core.mapper.SelectMapperInvoke.call(SelectMapperInvoke.java:29) 13 at org.beetl.sql.core.mapper.MapperJavaProxy.invoke(MapperJavaProxy.java:162) 14 at org.beetl.sql.core.mapper.MapperJava8Proxy.invoke(MapperJava8Proxy.java:92) 15 at com.sun.proxy.$Proxy109.queryDriverReplenishmentDuration(Unknown Source) 16 at com.bluepay.operation.service.impl.DriverReplenishmentSummaryExportServiceImpl.executeQueryAndExport(DriverReplenishmentSummaryExportServiceImpl.java:46) 17 at com.bluepay.operation.service.export.impl.ExportBlockingQueueServiceImpl$1$1.run(ExportBlockingQueueServiceImpl.java:75) 18 at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) 19 at java.util.concurrent.FutureTask.run(FutureTask.java:266) 20 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) 21 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) 22 at java.lang.Thread.run(Thread.java:748) 23 Caused by: java.sql.SQLException: Bad format for Time '162:30:34' in column 3 24 at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:965) 25 at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:898) 26 at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:887) 27 at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:861) 28 at com.mysql.jdbc.ResultSetImpl.getTimeFromString(ResultSetImpl.java:5511) 29 at com.mysql.jdbc.ResultSetImpl.getStringInternal(ResultSetImpl.java:5284) 30 at com.mysql.jdbc.ResultSetImpl.getString(ResultSetImpl.java:5138) 31 at com.alibaba.druid.filter.FilterChainImpl.resultSet_getString(FilterChainImpl.java:951) 32 at com.alibaba.druid.wall.WallFilter.resultSet_getString(WallFilter.java:1014) 33 at com.alibaba.druid.filter.FilterChainImpl.resultSet_getString(FilterChainImpl.java:947) 34 at com.alibaba.druid.filter.stat.StatFilter.resultSet_getString(StatFilter.java:938) 35 at com.alibaba.druid.filter.FilterChainImpl.resultSet_getString(FilterChainImpl.java:947) 36 at com.alibaba.druid.proxy.jdbc.ResultSetProxyImpl.getString(ResultSetProxyImpl.java:685) 37 at com.alibaba.druid.pool.DruidPooledResultSet.getString(DruidPooledResultSet.java:111) 38 at org.beetl.sql.core.mapping.type.StringTypeHandler.getValue(StringTypeHandler.java:46) 39 at org.beetl.sql.core.mapping.BeanProcessor.createBean(BeanProcessor.java:320) 40 at org.beetl.sql.core.mapping.BeanProcessor.toBeanList(BeanProcessor.java:177) 41 at org.beetl.sql.core.SQLScript.mappingSelect(SQLScript.java:438) 42 at org.beetl.sql.core.SQLScript.select(SQLScript.java:385) 43 ... 13 common frames omitted
出现这个问题之后,很疑惑,确定在SQL IDE(Navicat)中执行没有任何异常,从异常日志中得出是第三列也就是时间差计算中出现了不符合时间格式的异常,接下来Google一下。
问题解答
Google之后立马找到了答案,MySQL针对此问题专门开了CASE。有外国网友遇到了同样的问题:基于Java开发,在MySQL数据库上使用TIMEDIFF计算时间差,然后使用String类型接收结果值,报了如我一般的异常。当时给出的解决建议是,在计算到时间差异之后,转换为字符串格式,这样程序执行就不会报错了。
这里就有一个疑问了,为何直接这样输出就不行呢?
这其中隐藏着一个类型转换的问题。MySQL中的TIMEDIFF(时间差计算那一类函数)函数计算的结果是一个“xx:xx:xx”(xx小时:xx分钟:xx秒钟)时间格式的值,是一个时间长度(或者说跨度)的且以“%H:%m:%d”格式表现出来的值,以此输出到Java程序中之后,JDK自做聪明将其转换为了“java.sql.Time”类型,很明显这是一个时间类型,时间格式中小时的最大值是23,一旦我们的结果超过这个数字,自然就报错了,在这个类的底层,我们发现它就是通过“xx:xx:xx”格式的长度来进行判断和处理的,一旦超出这个长度就会抛出异常,因为格式显然不符合了。
所以针对这个问题比较恰当的处理方式,就是在SQL结果输出之前就将其转换为字符串格式,这样Java程序获取到结果之后就不会隐式对其进行转换操作了,最后程序能够得以正常执行。
解决方案参考: https://bugs.mysql.com/bug.php?id=70892
最后根据如上描述,我将SQL进行了如下修改,执行通过:
1 SELECT 2 DATE_FORMAT(supplement_time, "%Y-%m-%d") AS 'count_date', 3 phone_num, 4 CONCAT(SEC_TO_TIME( 5 UNIX_TIMESTAMP(max(supplement_end_time)) - UNIX_TIMESTAMP(min(supplement_start_time)) 6 ), '') AS 'diffTime' 7 FROM 8 cardslot_supplement_record 9 WHERE 1 = 1 10 @if(!isEmpty(startDate)){ 11 AND supplement_time > #startDate# 12 @} 13 @if(!isEmpty(endDate)){ 14 AND supplement_time < CONCAT(#endDate#, " 23:59:59") 15 @} 16 GROUP BY 17 count_date, 18 phone_num
问题总结
MySQL中诸如TIMEDIFF(时间差计算那一类函数)一类函数计算出的结果可能都是“hh:MM:ss”格式的值,如果想在Java程序中以String类型来接受处理,那么一定要事先确定这个以“时间格式”来表示的值,是否超过了时间表达的最大范围值,如果不能确定的,都最好在输出到程序中执行之前,对其进行字符串化再输出,以免在后续程序执行时抛出异常。