mysql在存储过程异常处理方面有个很大的缺点,就是在异常处理块中不能打印出错误信息和错误号等信息。这给我们记录错误日志带来了很大的不便。
《MySQL Stored Procedure Programming》里提到了jdbc的异常处理可以捕捉到mysql的错误,代码如下:
try {
Class.forName("com.mysql.jdbc.Driver").newInstance( );
String ConnectionString="jdbc:mysql://" + hostname + "/" + database + "?user=" +
username + "&password=" + password;
System.out.println(ConnectionString);
Connection conn = DriverManager.getConnection(ConnectionString);
Statement stmt=conn.createStatement( );
stmt.execute("call error_test_proc(1)");
}
catch(SQLException SQLEx) {
System.out.println("MySQL error: "+SQLEx.getErrorCode( )+
" SQLSTATE:" +SQLEx.getSQLState( ));
System.out.println(SQLEx.getMessage( ));
可不可以借用jdbc的处理方式呢?看一下mysql jdbc在异常处理这块的code, 却发现是用"show errors"发送数据库命令,然后解析返回数据集的方式。
显然,我们在mysql的存储过程中,是无法直接用"show errors"的方式来记录错误信息的。
到这里,似乎已经无路可走了。其实mysql在走自己路的同时,也留了路给开发者,比如udf。
udf以动态链接库的方式注入到数据库连接环境中,这就给我们提供了一个可以进入mysql server的后门,从而获取mysql server的各种运行时信息。并且很重要的,udf是用较温和的方式实现了mysql接口的扩展。
以下是我用用udf来返回mysql错误信息。主体代码如下:
extern "C" my_bool showerr_udf_init(UDF_INIT *initid, UDF_ARGS *args, char *message)
{
int msgLen = 0;
char errCode[50];
THD* thd = current_thd;
MYSQL_ERROR *err;
err = (MYSQL_ERROR*)(thd->warn_list.first_node()->info);
if (NULL != err)
{
sprintf(errCode,"error code: %d ",err->code);
msgLen = strlen(err->msg) + strlen(errCode) + 1;
if (!(initid->ptr=(char*) malloc(msgLen)))
{
strmov(message,"Couldn't allocate memory");
return 1;
}
bzero(initid->ptr,msgLen);
strcpy(initid->ptr,errCode);
strcat(initid->ptr,err->msg);
initid->max_length = msgLen;
}
initid->maybe_null = 1;
return 0;
}
编译后,注册udf:
mysql> CREATE FUNCTION showerr_udf RETURNS STRING SONAME "udf_example.dll";
Query OK, 0 rows affected (0.02 sec)
再做个简单的测试:
mysql> select k from tab_not_exists;
ERROR 1146 (42S02): Table 'test.tab_not_exists' doesn't exist
mysql> select showerr_udf();
+------------------------------------------------------------+
| showerr_udf() |
+------------------------------------------------------------+
| error code: 1146 Table 'test.tab_not_exists' doesn't exist |
+------------------------------------------------------------+
1 row in set (0.00 sec)
看起来不错。
再把showerr_udf()放到存储过程里试试看。
1: 创建测试表:
create table department(id int, name varchar(100)) engine innodb;
2:创建测试存储过程:
DELIMITER $$
DROP PROCEDURE IF EXISTS sp_test$$
CREATE PROCEDURE sp_test()
READS SQL DATA
BEGIN
DECLARE l_last_row INT DEFAULT 0;
DECLARE l_dept_id INT;
DECLARE l_err varchar(1000);
DECLARE c_dept CURSOR FOR
SELECT name FROM department where id=1;
/* handler to set l_last_row=1 if a cursor returns no more rows */
DECLARE CONTINUE HANDLER FOR NOT FOUND
BEGIN
SET l_last_row=1;
SET l_err = showerr_udf();
END;
OPEN c_dept;
dept_cursor: LOOP
FETCH c_dept INTO l_dept_id;
IF (l_last_row=1) THEN
LEAVE dept_cursor;
END IF;
END LOOP dept_cursor;
CLOSE c_dept;
SELECT l_err;
END$$
DELIMITER ;
3:调用测试
mysql> call sp_test();
+----------------------------------------------------------------------+
| l_err |
+----------------------------------------------------------------------+
| NULL
|
+----------------------------------------------------------------------+
1 row in set (0.00 sec)
返回的结果居然是空字符串。于是做了个猜测:一旦在存储过程中定义了异常处理,那么mysql会把错误静默掉,
且不会把错误信息保存到thd线程的错误列表里。(这个猜测可以用"show errors"命令同样不返回错误信息来侧面印证。)
然后果真在mysql的代码里看到了这样的注解(mysqld.cc的my_message_sql函数):
/*
If a continue handler is found, the error message will be cleared
by the stored procedures code.
*/
if (thd->spcont &&
! (MyFlags & ME_NO_SP_HANDLER) &&
thd->spcont->handle_error(error, MYSQL_ERROR::WARN_LEVEL_ERROR, thd))
{
/*
Do not push any warnings, a handled error must be completely
silenced.
*/
DBUG_RETURN(0);
}
找到了原因,就可以尝试去改变。在上面的代码中加入push_warning方法(mysql自己的方法),如下:
/*
If a continue handler is found, the error message will be cleared
by the stored procedures code.
*/
if (thd->spcont &&
! (MyFlags & ME_NO_SP_HANDLER) &&
thd->spcont->handle_error(error, MYSQL_ERROR::WARN_LEVEL_ERROR, thd))
{
/*
Do not push any warnings, a handled error must be completely
silenced.
*/
push_warning(thd, MYSQL_ERROR::WARN_LEVEL_ERROR, error, str);
DBUG_RETURN(0);
}
还需要在mysql错误处理模块中,注释掉吞异常的代码:
if (thd->handle_error(code, msg, level))
DBUG_RETURN(NULL);
if (thd->spcont &&
thd->spcont->handle_error(code, level, thd))
{
DBUG_RETURN(NULL);
}
重新构建,重启mysql server。再做一次测试:
mysql> call sp_test();
+----------------------------------------------------------------------+
| l_err |
+----------------------------------------------------------------------+
| error code: 1329 No data - zero rows fetched, selected, or processed |
+----------------------------------------------------------------------+
1 row in set (0.00 sec)
Query OK, 0 rows affected, 1 warning (0.02 sec)
终于可以在mysql的存储过程中,记录详细的错误信息了。虽然部分解决了问题(因为没做全面测试),但却修改了mysql的服务器代码,偏离了一开始的期望。希望mysql能在后续的版本中修正这样的问题,毕竟对于异常处理来说,打印错误信息和错误号,以及重抛异常等是一个很基本的要求。