升级 iBATIS/MyBATIS 对处理 DuplicateKeyException 的影响

升级 iBATIS/MyBATIS 对处理 DuplicateKeyException 的影响

关于升级 Spring 等依赖的一些经验 中,分享了一些开源依赖的升级经验。部分小伙伴质疑升级 iBATIS/MyBATIS 会影响对 DuplicateKeyException 异常的处理。这篇文章就从源码分析/代码更新的就角度来分析一下升级相关依赖是否会对 DuplicateKeyException 异常的处理带来实质性的影响。

由于主要的技术栈涉及 MySQL 驱动、iBATIS、MyBATIS、Spring 周边等。所以,本文仅分析涉及的这些依赖。

D瓜哥使用 MySQL: Employees Sample Database 搭建了一个 Spring + MyBATIS + MySQL Connector/J 的测试环境。连续插入两条一样的数据,单步调试,在 com.mysql.jdbc.MysqlIO#sendCommand 方法中,就可以观察到如下异常:

MySQL Error 1062
Figure 1. MySQL Error 1062

从这里可以明显看出,MySQL 驱动返回的异常中, venderCode 编码是 1062

顺着这个线,往上走,到 org.apache.ibatis.session.defaults.DefaultSqlSession#update(java.lang.String, java.lang.Object) 方法中,可以看到,

MyBATIS wrap Exception
Figure 2. MyBATIS wrap Exception

在这里,会将 SQLException 包装成 PersistenceException,这也是 MyBATIS 对外暴露的统一的异常类。

继续往上走,就到了 org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke 方法:

MyBATIS translateException
Figure 3. MyBATIS translateException

SqlSessionInterceptor#invoke 方法的异常处理中,将 PersistenceException 异常通过 org.springframework.dao.support.PersistenceExceptionTranslator#translateExceptionIfPossible 方法,将异常转换成 DataAccessException 对象。 DataAccessException 类是 Spring 数据访问的异常类基类。

MyBATIS translate Exception
Figure 4. MyBATIS translate Exception

这里还会牵扯到 SQLExceptionTranslator 类。代码一路跟踪下去,最后,会发现是在 org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator#doTranslate 中完成了转换工作:

MySQL 1062 to DuplicateKeyException
Figure 5. MySQL 1062 to DuplicateKeyException

请注意这里的类名: SQLErrorCodeSQLExceptionTranslator,见名知意,类名明确地说明,是通过错误编码来确定具体异常类的。

这里再看一看异常信息的生成方法:

org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator#buildMessage
/**
 * Build a message {@code String} for the given {@link java.sql.SQLException}.
 * <p>To be called by translator subclasses when creating an instance of a generic
 * {@link org.springframework.dao.DataAccessException} class.
 * @param task readable text describing the task being attempted
 * @param sql the SQL statement that caused the problem
 * @param ex the offending {@code SQLException}
 * @return the message {@code String} to use
 */
protected String buildMessage(String task, @Nullable String sql, SQLException ex) {
    return task + "; " + (sql != null ? ("SQL [" + sql + "]; ") : "") + ex.getMessage();
}

这里一眼就可以看出,Spring 生成 DataAccessException 对象的错误信息时,是通过在 SQLException 错误信息基础上,在前面加上了 SQL 信息。

可以在 spring-jdbc.jar!/org/springframework/jdbc/support/sql-error-codes.xml 中查看到 Spring 内置支持的所有数据库类型以及对应的错误编码。关于 MySQL 的配置如下:

<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes">
    <property name="databaseProductNames">
        <list>
            <value>MySQL</value>
            <value>MariaDB</value>
        </list>
    </property>
    <property name="badSqlGrammarCodes">
        <value>1054,1064,1146</value>
    </property>
    <property name="duplicateKeyCodes">
        <value>1062</value>
    </property>
    <property name="dataIntegrityViolationCodes">
        <value>630,839,840,893,1169,1215,1216,1217,1364,1451,1452,1557</value>
    </property>
    <property name="dataAccessResourceFailureCodes">
        <value>1</value>
    </property>
    <property name="cannotAcquireLockCodes">
        <value>1205,3572</value>
    </property>
    <property name="deadlockLoserCodes">
        <value>1213</value>
    </property>
</bean>

MySQL Connector/J release/5.1 中可以下载到 MySQL 驱动的代码。其中,在 com.mysql.jdbc.MysqlErrorNumbers#ER_TRG_CORRUPTED_FILE 可以查看到 1602 错误的定义。查看代码变更历史,这个编码从 2011 年增加到这个文件中的。

再回过头来看 Spring 中 sql-error-codes.xml 的代码变更历史,其中可以看到 MySQL 1602 是在 2009-03-10 加入到配置文件中的。而 Spring 3.x 版的第一个版本 3.0.0.RELEASE 是在 2009年12月17日发布的。所以,从 Spring 3.0.0.RELEASE 开始,Spring 对 MySQL 数据库异常的处理,几乎保持不变。

最后,再看一下 iBATIS 的异常处理。 iBATIS 的异常处理比较简单,代码都集中在 org.springframework.orm.ibatis.SqlMapClientTemplate(该代码已经从 Spring 4 开始从 Spring 仓库中删除) 中:

org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator#buildMessage
/**
 * Execute the given data access action on a SqlMapExecutor.
 * @param action callback object that specifies the data access action
 * @return a result object returned by the action, or {@code null}
 * @throws DataAccessException in case of SQL Maps errors
 */
public <T> T execute(SqlMapClientCallback<T> action) throws DataAccessException {
    // 删除无用代码
    try {
        // 删除无用代码

        // Execute given callback...
        try {
            return action.doInSqlMapClient(session);
        }
        catch (SQLException ex) {
            // 这里是异常处理逻辑
            throw getExceptionTranslator().translate("SqlMapClient operation", null, ex);
        }
        finally {
            try {
                if (springCon != null) {
                    if (transactionAware) {
                        springCon.close();
                    }
                    else {
                        DataSourceUtils.doReleaseConnection(springCon, dataSource);
                    }
                }
            }
            catch (Throwable ex) {
                logger.debug("Could not close JDBC Connection", ex);
            }
        }

        // Processing finished - potentially session still to be closed.
    }
    finally {
        // Only close SqlMapSession if we know we've actually opened it
        // at the present level.
        if (ibatisCon == null) {
            session.close();
        }
    }
}

这里也是通过基类 org.springframework.jdbc.support.JdbcAccessorgetExceptionTranslator 方法,获取 SQLExceptionTranslator 对象,然后调用其 translate 方法来完成异常转换,和上面 MyBATIS 中的处理逻辑是一样的。

综上,升级 iBATIS/MyBATIS 不会对 DuplicateKeyException 异常的处理有任何影响,可以放心升级。