ORA-01861 日期格式错误的根治方案:从 SQL 层到 Java 层的标准化治理
问题现象
系统运行中,某个查询接口突然报错:
org.springframework.jdbc.BadSqlGrammarException:
### Error querying database. Cause: java.sql.SQLException: ORA-01861:
文字与格式字符串不匹配
错误指向 MyBatis XML 中的一段日期比较 SQL:
sql
WHERE sc.ZPSJ >= TO_DATE(TO_CHAR(#{startDate}, 'yyyy-mm-dd') || ' 00:00:00',
'yyyy-mm-dd hh24:mi:ss')
AND sc.ZPSJ <= TO_DATE(TO_CHAR(#{endDate}, 'yyyy-mm-dd') || ' 23:59:59',
'yyyy-mm-dd hh24:mi:ss')
但同样的 SQL 在另一个环境却正常执行。这是什么原因?
根因分析:双重 TO_DATE 的脆弱性
这段 SQL 的意图很明确:将传入的 startDate/endDate 参数格式化为当天 00:00:00 / 23:59:59 后进行日期范围比较。
问题 1:多余的 TO_DATE + TO_CHAR 转换
当 MyBatis 传入的 #{startDate} 是 Java Date 类型 时,MyBatis 的 JDBC 驱动会自动将 Date 对象转换为 Oracle 可识别的日期类型。
此时再做 TO_CHAR(date, 'format') → 字符串拼接 → TO_DATE(string, 'format'),不仅多此一举,还引入了不稳定因素:
sql
-- 元凶:双重转换链
Java Date → TO_CHAR → 字符串拼接 → TO_DATE → Oracle Date
问题 2:MyBatis 参数绑定的隐式行为
MyBatis 在将 #{startDate} 传递给 Oracle 时,JDBC 驱动的 PreparedStatement.setDate() 会使用会话的 NLS_DATE_FORMAT 格式化日期。当不同数据库环境的 NLS_DATE_FORMAT 不一致时:
| 环境 | NLS_DATE_FORMAT | 问题 |
|---|---|---|
| 开发库 | YYYY-MM-DD HH24:MI:SS |
✅ TO_CHAR 正常 |
| 测试库 | YYYY-MM-DD |
✅ 但 TO_CHAR 后拼接 ' 00:00:00' 再 TO_DATE 仍可能失败 |
| 生产库 | DD-MON-YY |
❌ TO_CHAR 输出 06-MAY-26,TO_DATE 解析失败 |
问题 3:参数可能携带时间分量
更隐蔽的问题是:startDate 参数传入了 "2026-05-15 14:30:00" 这种带时间的值,而 TO_CHAR 中指定的格式是 'yyyy-mm-dd',导致 TRUNC 隐式截断不生效。
修复方案一:SQL 层简化(最推荐)
既然 MyBatis 可以直接传 Date 类型给 Oracle,那就去掉所有多余的 TO_DATE/TO_CHAR 转换,直接比较:
xml
<!-- ❌ 原写法 -->
WHERE sc.ZPSJ >= TO_DATE(TO_CHAR(#{startDate}, 'yyyy-mm-dd') || ' 00:00:00',
'yyyy-mm-dd hh24:mi:ss')
AND sc.ZPSJ <= TO_DATE(TO_CHAR(#{endDate}, 'yyyy-mm-dd') || ' 23:59:59',
'yyyy-mm-dd hh24:mi:ss')
<!-- ✅ 修复后 -->
WHERE sc.ZPSJ >= #{startDate}
AND sc.ZPSJ <= #{endDate}
就这么简单。MyBatis 会自动将 Date 对象转换为 Oracle 的 DATE 类型进行比较。
保留 LIKE 语句的拼接
需要注意:给 DATE 类型做 || 字符串拼接才需要 TO_CHAR,但如果只是 LIKE 语句的字符串拼接,Oracle 默认就是可行的:
xml
<!-- 以下写法无需改成 CONCAT,Oracle 原生支持 -->
AND sc.SCBC LIKE #{scbc} || '%'
修复方案二:Java 层标准化(防御性加固)
如果业务逻辑需要确保「开始时间归零、结束时间归满」,可以在 Service 层对日期参数做标准化处理,作为第二道防线:
java
public class OrderShqyServiceImpl extends ServiceImpl<...> {
/**
* 标准化日期查询参数
* 开始时间:归零到当天 00:00:00.000
* 结束时间:归满到当天 23:59:59.999
*/
private Map<String, Date> normalizeDateRange(Date startDate, Date endDate) {
Map<String, Date> result = new HashMap<>();
Calendar cal = Calendar.getInstance();
if (startDate != null) {
cal.setTime(startDate);
// 归零:00:00:00.000
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
result.put("startDate", cal.getTime());
}
if (endDate != null) {
cal.setTime(endDate);
// 归满:23:59:59.999
cal.set(Calendar.HOUR_OF_DAY, 23);
cal.set(Calendar.MINUTE, 59);
cal.set(Calendar.SECOND, 59);
cal.set(Calendar.MILLISECOND, 999);
result.put("endDate", cal.getTime());
}
return result;
}
}
在调用 Mapper 之前,先对参数做标准化:
java
public List<SomeVO> queryByDateRange(Date startDate, Date endDate) {
Map<String, Date> normalized = normalizeDateRange(startDate, endDate);
// 传入标准化后的日期
return productionPlanPrintMapper.selectProductionPlanPrintList(
normalized.get("startDate"), normalized.get("endDate"), ...);
}
优点:
- 从源头消除参数携带时间分量的问题
- 不修改 SQL,不影响现有索引
- 后续即使调整时区或精度,只需改这一个地方
两种方案的决策矩阵
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 新写 SQL | 方案一(去掉 TO_DATE) | 简洁、高效,JDBC 原生支持 |
| 现有 SQL 出问题 | 方案一 | 定位问题后直接修改 XML |
| 担心其他调用方传错参数 | 方案一 + 方案二 | SQL 简化 + 调用层防御 |
| 无法修改 Mapper XML | 方案二 | 只能通过修改 Service 规避 |
| 多系统共享 SQL | 方案二 | 保护性编程,不依赖外部传参 |
经验教训总结
1. MyBatis 的 #{param} 会自动处理类型转换
这是很多 Java 开发者的认知盲区:
java
// 传入 Date 类型
mapper.selectByDate(startDate);
// MyBatis 自动调用 PreparedStatement.setDate() 传给 Oracle
// Oracle 直接识别为 DATE 类型,无需 TO_DATE
所以 TO_DATE(#{param}, 'format') 的做法,99% 的情况下都是多余的。正确做法:
| 参数类型 | XML 写法 | 说明 |
|---|---|---|
Date |
#{param} |
直接比较,Oracle 自动识别 |
String |
TO_DATE(#{param}, 'yyyy-mm-dd') |
字符串转日期才需要 TO_DATE |
Date + 格式化 |
#{param, jdbcType=DATE} |
明确指定 JDBC 类型 |
2. 不要在 SQL 中对 Date 类型做字符串拼接
sql
-- ❌ 三无产品:无意义 + 无效率 + 不稳定
TO_DATE(TO_CHAR(#{startDate}, 'yyyy-mm-dd') || ' 00:00:00', 'yyyy-mm-dd hh24:mi:ss')
-- ✅ 直接在 Java 层标准化日期后传入
>= #{startDate}
-- 或使用 Oracle 原生函数(如果确实需要在 SQL 层处理)
>= TRUNC(#{startDate})
3. 区分两种场景的日期处理
| 场景 | 做法 | 示例 |
|---|---|---|
| 查询条件 | Java 层归一化后直接传 #{date} |
zpsj >= #{startDate} |
| LIKE/拼接 | Oracle 原生 ` | |
| 导入/外部数据 | 字符串转日期 | TO_DATE(#{str}, 'yyyy-mm-dd') |
写在最后
ORA-01861 的核心解决思路其实很简单:不要替 Oracle 做它擅长的事。
Oracle 的 JDBC 驱动本身就能正确处理 Java Date 类型的参数绑定,开发者在 SQL 中手动 TO_DATE/TO_CHAR 反而破坏了这一机制,引入了格式不一致的风险。
我们最终采取的是 "SQL 简化 + Java 层防御" 的组合方案:
- 将 Mapper XML 中的
TO_DATE(TO_CHAR(...))替换为直接#{param}比较 - 在 Service 层通过
Calendar对日期进行标准化(归零/归满) - 保留 LIKE 语句的
||拼接不变,仅修改日期比较部分
修改量很小,但彻底消除了 ORA-01861 这个旧病复发的隐患。