大千世界,茫茫人海,相识就是一种缘分。若此篇文章对您有帮助,点个赞或点个关注呗!
一、数据库异常的五层封装
下图左边展示的是分库分表前的数据访问层(3层),右边是在引入了分库分表(如Mycat)之后的数据访问层(5层)。本文我们将以分库分表之后的5层结构为例,由下至上逐层分析数据库的异常是如何返回到业务的DAO层。
第1层:Oracle数据库驱动层
Oracle驱动是对JDBC接口的标准实现,通过Statement和PreparedStatement这两个接口我们可以看出执行SQL时抛出的异常类型都是SQLException。
而SQLException中有几个关键的属性,了解这几个属性对我们全盘理解异常的传递过程有很大帮助。
reason:a description of the exception
SQLState:an XOPEN or SQL:2003 code identifying the exception
vendorCode:a database vendor-specific exception code
cause:the cause of this throwable
第2层:分库分表层(以Mycat为例)
将调用JDBC执行SQL过程中抛出的SQLException Catch住,根据errno(vendorCode),sqlState(SQLState,Mycat默认的sqlState是HY000),message(reason)创建一个ErrorPacket对象,并序列号成一个MySQL协议发送出去。
private void handleSqlExceptionError(SQLException e) {
String msg = e.getMessage();
ErrorPacket error = new ErrorPacket();
error.packetId = ++packetId;
error.errno = e.getErrorCode();
error.sqlState = e.getSQLState().getBytes();
error.message = msg.getBytes();
// 发送给mysql driver
this.respHandler.errorResponseWithNoInterrupt(error.writeToBytes(), this);
}
第3层:MySQL数据库驱动层
MySQL Driver解析返回的mysql协议byte[],获取errno,sqlState,message根据sqlState判断是那种类型的SQLException,如下图索引:
此时mysql驱动根据sqlState把SQLException封装成了特定的MySQL的异常(SQLException的子类)抛出
第4层:MyBatis
MyBatis抛出的异常都是PersistenceException类型,将调用mysql驱动执行SQL过程中抛出的SQLException或其子类 Catch住,
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
// 封装SQLException
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
// 将SQLException作为PersistenceException的一个属性
public static RuntimeException wrapException(String message, Exception e) {
return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
}
第5层:MyBatis-Spring:MyBatis代码和Spring的整合
MyBatis-Spring针对Mybatis抛出的异常封装的都是DataAccessException类型
public DataAccessException translateExceptionIfPossible(RuntimeException e) {
// Mybatis抛出异常
if (e instanceof PersistenceException) {
// Batch exceptions come inside another PersistenceException
// recursion has a risk of infinite loop so better make another if
if (e.getCause() instanceof PersistenceException) {
e = (PersistenceException) e.getCause();
}
// MySQL驱动抛出的异常
if (e.getCause() instanceof SQLException) {
this.initExceptionTranslator();
// 封装成DataAccessException
return this.exceptionTranslator.translate(e.getMessage() + "\n", null, (SQLException) e.getCause());
} else if (e.getCause() instanceof TransactionException) {
throw (TransactionException) e.getCause();
}
return new MyBatisSystemException(e);
}
return null;
}
sql-error-codes.xml文件片段
<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes">
<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</value>
</property>
<property name="deadlockLoserCodes">
<value>1213</value>
</property>
</bean>
根据SQLException中的错误码(db特定异常码)和sql-error-codes.xml文件中的定义,封装成具体的DataAccessException异常。
思考:
JDBC抛出的都是SQLException类型
MyBatis抛出的都是PersistenceException类型
MyBatis-Spring抛出的都是DataAccessException类型,我们在设计一个系统或框架的时候,可以统一我们的异常类型。
二、数据库异常的定制
详见笔者之前的博客
1、数据库驱动层
https://my.oschina.net/u/3787772/blog/2980222
2、MyBatis层
https://my.oschina.net/u/3787772/blog/2962941
3、MyBatis-Spring层