ORA-01861 日期格式错误的根治方案:从 SQL 层到 Java 层的标准化治理

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 层防御" 的组合方案:

  1. 将 Mapper XML 中的 TO_DATE(TO_CHAR(...)) 替换为直接 #{param} 比较
  2. 在 Service 层通过 Calendar 对日期进行标准化(归零/归满)
  3. 保留 LIKE 语句的 || 拼接不变,仅修改日期比较部分

修改量很小,但彻底消除了 ORA-01861 这个旧病复发的隐患。

相关推荐
半只小闲鱼1 小时前
合并多个excel文件到一个文件中
前端·python·数据分析
雪宫街道1 小时前
SpringBoot 静态资源映射规则与定制
java·spring boot·后端·spring
lg_cool_1 小时前
使用conda管理python运行环境并关联vscode
vscode·python·conda
宸津-代码粉碎机1 小时前
Spring AI企业级实战|智能记忆摘要+自动遗忘机制落地,彻底解决上下文爆炸与Token冗余
java·大数据·人工智能·后端·python·spring
南极企鹅1 小时前
springboot项目不退出的原因
java·spring boot·后端
乘浪初心1 小时前
python调用API接口,免费API调取,学习如何调取API接口并反馈你输入的内容
开发语言·python·api·免费
AI玫瑰助手1 小时前
Python模块:import导入模块与模块的搜索路径
android·开发语言·python
傻啦嘿哟1 小时前
一篇文章讲清楚Python的变量作用域
开发语言·python
devilnumber1 小时前
Java 二分查找(二分算法)详解 + 实战运用 + 核心坑点
java·开发语言·算法