现世界中,根本不存在完美的程序,不管多么完美的程序,总会抛出这样或那样的异常。MySQL 为了处理程序的错误,提供了错误处理,避免由于错误而导致程序崩溃,这将避免一些灾难性事故的发生。
例如,对于航天飞机的某类应用软件来说,这种的完美程度实际上是需求的一部分,因为软件的任何意外故障都将是灾难性的。然而,在商业应用程序的世界里,我们通常会对执行环境做出某些假设,我们假设 MySQL 服务器正在运行,我们的表没有被删除,主机没有着火等等。如果出现这些情况中的任何一种,我们就认为应用程序会失败。在许多其他情况下,我们应该预测潜在的故障,并编写代码来管理这些情况,这就是异常处理的作用所在。
在 MySQL 中,当存储程序遇到错误条件时,会停止执行并向调用程序返回错误信息。这是 MySQL 的默认行为。如果我们需要一种不同的行为呢?例如,我们想捕获错误、记录错误或报告错误,然后继续执行应用程序,该怎么办?为了实现这种控制,我们需要在程序中定义异常处理程序。
下面我们将介绍如何创建各种类型的异常处理程序,以及如何通过使用 “命名” 条件来提高错误处理的可读性。
首先,我们来看一个存储程序错误处理的示例,下面将通过 MySQL 客户端创建一个根据参数插入用户信息的简单存储过程:
-- 准备一张用户表
mysql> select * from user;
+----+-------+-----+-----+----------------------+-------------+
| id | name | age | sex | email | phone |
+----+-------+-----+-----+----------------------+-------------+
| 1 | 王五 | 23 | 1 | wangwu@sina.com | 15810202050 |
| 2 | Helen | 26 | 0 | helen@outlook.com | 15814402560 |
| 3 | Jemo | 27 | 1 | jemo@163.com | 15105603501 |
| 4 | Bill | 33 | 1 | bill@gmail.com | 15100055760 |
| 5 | 张三 | 21 | 0 | zhangsan@sina.com.cn | 15810232022 |
| 6 | 李四 | 19 | 1 | lisi@qq.com | 15810122441 |
+----+-------+-----+-----+----------------------+-------------+
6 rows in set (0.07 sec)
-- 设置分隔符
mysql> delimiter //
-- 创建存储过程,根据参数插入一条记录到用户表
mysql> create procedure insert_user(
-> in in_id int,
-> in in_name varchar(50),
-> in in_age int,
-> in in_sex varchar(1)
-> )
-> begin
-> INSERT INTO `user`(`id`, `name`, `age`, `sex`) values (in_id, in_name, in_age, in_sex);
-> end
-> //
Query OK, 0 rows affected (0.00 sec)
-- 设置分隔符
mysql> delimiter ;
-- 调用存储过程,插入用户ID不存在的记录到数据库
mysql> call insert_user(7, 'Test', 30, 0);
Query OK, 1 row affected (0.01 sec)
-- 重复插入一条记录,抛出了错误
mysql> call insert_user(7, 'Test', 30, 0);
ERROR 1062 (23000): Duplicate entry '7' for key 'PRIMARY'上面的示例中,当我们第二次调用 insert_user 存储过程插入 ID 为 7 的用户时,出现了 ERROR 1062 主键重复错误。
如果上面存储过程是由外部程序(如 Java)调用的,我们也许可以 “保持原样”,不会产生什么不好的影响。Java 和其他外部程序可以检测到此类错误,然后采取适当的措施进行处理。
如果上面存储过程是从另一个存储过程中调用,就有可能导致整个存储过程调用栈中止,这可能不是我们想要的结果。
既然我们可以预见到这个存储过程可能会引发 MySQL ERROR 1062 错误,那么我们就可以编写代码来处理这个特定的错误代码。
下面示例演示了这种技术。存储过程不允许异常在未处理的情况下从存储过程中传播出去(导致调用程序出现故障),而是捕获异常,设置状态标志,并将状态信息返回给调用程序。然后,调用程序就可以决定是终止该故障,还是继续执行:
CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_user`(
IN in_id INT,
IN in_name VARCHAR(50),
IN in_age INT,
IN in_sex VARCHAR(1),
OUT out_status VARCHAR(200)
)
BEGIN
-- 定义错误处理器
-- 当执行 SQL 代码抛出 1062 错误时,触发该错误处理器
-- 然后,将 'Duplicate Entry' 赋值给 out_status 变量
DECLARE CONTINUE HANDLER FOR 1062 SET out_status='Duplicate Entry';
-- 插入用户信息
INSERT INTO `user`(`id`, `name`, `age`, `sex`) values (in_id, in_name, in_age, in_sex);
END使用 MySQL 客户端调用上面存储过程,如下:
-- 定义一个用户变量,用来存放输出的错误信息
mysql> set @out_state='';
Query OK, 0 rows affected (0.00 sec)
-- 调用存储过程
mysql> call insert_user(7, 'Test', 30, 0, @out_state);
Query OK, 0 rows affected (0.00 sec)
-- 查看 @out_state 变量的值
mysql> select @out_state;
+-----------------+
| @out_state |
+-----------------+
| Duplicate Entry |
+-----------------+
1 row in set (0.07 sec)注意,我们将在后续章节详细介绍 HANDLER 子句的语法。现在,我们只需要理解 DECLARE CONTINUE HANDLER 语句告诉 MySQL:“如果遇到 ERROR 1062 (23000): Duplicate entry 错误,那么继续执行,但将变量 out_status 的值设置为 'Duplicate Entry'"。
根据调用结果可知,这种实现不会向调用程序返回错误,我们可以检查状态变量,看看存储过程是否执行成功。
下面示例展示另一个插入用户信息的存储过程,该过程调用上面过程插入用户基本信息,然后判断调用结果。如果调用失败,则输出错误信息,终止继续执行。如果调用成功,则修改用户的电话和邮件信息:
CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_user_ext`(
IN in_id INT,
IN in_name VARCHAR(50),
IN in_age INT,
IN in_sex VARCHAR(1),
IN in_phone varchar(200),
IN in_email varchar(200))
myBegin:BEGIN
DECLARE call_state VARCHAR(200) DEFAULT '';
-- 调用 insert_user 过程插入用户基本信息
CALL insert_user(in_id, in_name, in_age, in_sex, call_state);
IF call_state = 'Duplicate Entry' THEN
-- 直接输出错误消息
SELECT '插入用户信息失败,ID冲突' as Error;
-- 离开 BEGIN 语句块
LEAVE myBegin;
END IF;
-- 更新,添加电话和邮件
UPDATE `user` SET phont=in_phone, email=in_email WHERE id=in_id;
END myBegin使用 MySQL 客户端调用上面存储过程,如下:
mysql> call insert_user_ext(7, 'Test', 30, 0, '15100223311', 'test@gmail.com');
+--------------------------+
| Error |
+--------------------------+
| 插入用户信息失败,ID冲突 |
+--------------------------+
1 row in set (0.03 sec)
Query OK, 0 rows affected (0.00 sec)