Java-Date类型被数据库四舍五入的问题
前言
在上下游数据对账时,发现存入数据库的时间跟发送给下游的时间上有差异,
比如上游存入 MySQL 数据库的时间为:2023-03-01 00:00:00,(数据库的字段类型为 datetime)
而下游接收到的时间为 2023-02-28 00:00:00. 执行顺序像下图这样:
伪代码如下:
// new 一个时间
Date createTime = new Date();
// insert 入库
Model model = new Model();
model.setCreateTime(createTime);
modelMapper.insert(model);
// 构造消息体,向下游发送消息
Map<String, Object> message = new HashMap<>();
message.put("bizMessage", "something");
message.put("model", model);
// 将消息体转换成字符串向下游发送消息
sender.send(JsonUtil.beanToJson(message));
下面是beantToJson的实现,可以看出采用 jackson来实现,并且其中的date格式化字符串采用的是
SimpleDateFormat, 而且格式是 yyyy-MM-dd HH:mm:ss, 到秒.
public class JsonUtil {
private static ObjectMapper mapper;
static {
mapper = new ObjectMapper();
mapper.setLocale(Locale.CHINA);
SimpleDateFormat smt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 设置默认时区
mapper.setTimeZone(TimeZone.getDefault());
// 时间类型格式化
mapper.setDateFormat(smt);
}
public static <T> String beanToJson(T obj) {
if (Objects.isNull(obj)) {
return null;
}
try {
return mapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new JsonSerializedException(e, "Bean转Json异常");
}
}
}
原因分析
先给出结果,然后我们再来证明
- Mysql datetime 类型如果没有指定精度的话,会将秒后面的毫秒等进行四舍五入。
比如:
2023-02-28 23:59:59.677 会四舍五入成 2023-03-01 00:00:00
2023-02-28 23:59:59.200 会四舍五入成 2023-02-28 00:00:00
_- SimpleDateFormat 将 date 类型进行字符串格式化时,是使用 Calendar 来处理的,不会进行四舍五入的操作
接下来我们来用下面的程序测试一下数据库的四舍五入及date字符串格式化
Mysql datetime 类型如果没有指定精度的话,会将秒后面的毫秒等进行四舍五入。
比如:
2023-02-28 23:59:59.677 会四舍五入成 2023-03-01 00:00:00
2023-02-28 23:59:59.200 会四舍五入成 2023-02-28 00:00:00
SimpleDateFormat 将 date 类型进行字符串格式化时,是使用 Calendar 来处理的,不会进行四舍五入的操作。
由于Calendar 存在线程安全问题,所以需要用 Java 8 提供的java.time包下的 API 来替换 Calendar.
LocalDateTime now = LocalDateTime.now();
now = now.withNano(0);
Date nowDate = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());
或者直接使用 joda 的 LocalDateTime (org.joda.time).LocalDateTime now = new LocalDateTime();
now = now.withMillisOfSecond(0);
Date nowDate = now.toDate();
@Test
public void Test() {
ModelMapper modelMapper = new ModelMapper();
Sender sender = new Sender();
// 创建一个 calender 对象,来构造确定时分秒及毫秒的时间
// 2023-04-01 00:00:00.677
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.set(Calendar.MONTH, 3);
calendar.set(Calendar.DATE, 1);
calendar.set(Calendar.MINUTE,0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 677);
Date createTime = calendar.getTime();
// 构造 model
Model model = new Model();
model.setCreateTime(createTime);
// insert model
modelMapper.insert(model);
// 构造消息,向下游发送消息
Map<String, Object> message1 = new HashMap<>();
message1.put("bizMessage", "message1");
message1.put("model", model);
// 将消息体转换成字符串向下游发送消息
sender.send(JsonUtil.beanToJson(message1));
// 更改calendar毫秒为 200
// 2023-04-01 00:00:00.677
calendar.set(Calendar.MILLISECOND, 200);
createTime = calendar.getTime();
// 更新 model 的时间
model.setCreateTime(createTime);
// insert model
modelMapper.insert(model);
// 构造消息,向下游发送消息
Map<String, Object> message2 = new HashMap<>();
message2.put("bizMessage", "message2");
message2.put("model", model);
// 将消息体转换成字符串向下游发送消息
sender.send(JsonUtil.beanToJson(message2));
}
在 sender.send() 方法中对消息进行打印,运行上面的代码后,控制台输出如下:
{"bizMessage":"message1","model":{"createTime":"2023-04-01 22:00:00"}}
{"bizMessage":"message2","model":{"createTime":"2023-04-01 22:00:00"}}
mysql 数据库中的 create_time 字段两条分别为:
可以看到 date 转字符串的结果是相同的,而数据库中对毫秒部分进行了四舍五入到秒.
MySQL的DATETIME类型
MySQL 官方对 DATETIME 类型的精度作了详细的说明 (DATE、TIMESTAMP类型的精度定义也一样),详情可参见官方文档
我总结了下,一共有以下几点:
- DATETIME 可以指定精度(秒的后面还有几位),从 0-6,也就是最大精度可以到微妙级别(6位). 指定精度的方式为 datetime(6).
- 如果不指定精度,默认为0,也就是秒级精度
- 如果给的值的精度比字段设定的精度要高,则采取四舍五入(rounding),
- 如果不想四舍五入,要直接舍弃掉后面的位,则需要开启 sqlmodel: TIME_TRUNCATE_FRACTIONAL.
对于第三点,官法的例子很清晰,我摘录了过来:
CREATE TABLE fractest( c1 TIME(2), c2 DATETIME(2), c3 TIMESTAMP(2) );
INSERT INTO fractest VALUES
('17:51:04.777', '2018-09-08 17:51:04.777', '2018-09-08 17:51:04.777');
mysql> SELECT * FROM fractest;
+-------------+------------------------+------------------------+
| c1 | c2 | c3 |
+-------------+------------------------+------------------------+
| 17:51:04.78 | 2018-09-08 17:51:04.78 | 2018-09-08 17:51:04.78 |
+-------------+------------------------+------------------------+
需要开启 sqlmodel: TIME_TRUNCATE_FRACTIONAL 的方法为:
SET @@sql_mode = sys.list_add(@@sql_mode, 'TIME_TRUNCATE_FRACTIONAL');
开启后:
mysql> SELECT * FROM fractest;
+-------------+------------------------+------------------------+
| c1 | c2 | c3 |
+-------------+------------------------+------------------------+
| 17:51:04.77 | 2018-09-08 17:51:04.77 | 2018-09-08 17:51:04.77 |
+-------------+------------------------+------------------------+
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。