现世界中,根本不存在完美的程序,不管多么完美的程序,总会抛出这样或那样的异常。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)