TOC
KINA

KINA-0

Start having fun with KINA right now!

MySQL高级:存储过程/函数、高级控制语句

MySQL高级语法:存储过程详解

1 存储过程

存储过程(Stored Procedure)是一种存储在数据库中的程序。它可以包含多个SQL语句,提供许多过程语言的功能,例如变量定义、条件语句、循环语句、游标以及异常处理等。存储过程没有返回值,但是它可以通过输出参数实现数据的返回,同时还可以产生一个查询结果返回到客户端。

存储过程的优点:

  • 实现代码的重用和集中管理
  • 实现业务的封装和隔离
  • 减少了应用与数据库之间的网络流量
  • 存储过程可以提高安全性

存储过程的缺点:

  • MySQL存储过程的语法和其他数据库之间不完全兼容,无法直接移植
  • 存储过程需要占用数据库服务器的资源,包括CPU、内存等
  • 存储过程的开发和维护需要专业的技能

1.1 创建存储过程

CREATE PROCEDURE sp_name ( [ IN | OUT | INOUT ] param_name data_type, ... )
[COMMENT 'string’
| LANGUAGE SQL
| [NOT] DETERMINISTIC
| { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }]
routine_body;
  • sp_name:存储过程名称
  • param_name:参数名称。IN表示输入参数(默认模式),OUT表示输出参数,INOUT表示输入输出参数
  • data_type:参数的数据类型
  • COMMENT:为存储过程添加注释
  • LANGUAGE:编写存储过程的语言,目前SQL是唯一可用值
  • DETERMINISTIC:该属性表示这是一个确定性的存储函数,对于相同的输入参数一定会返回相同的结果;MySQL默认创建非确定性函数(NOT DETERMINISTIC)
  • CONTAINS:SQL表示程序中不包含读取或者写入数据表的语句(默认设置)
  • routine_body:存储过程的具体实现(begin……end;

【例1】创建无参存储过程

CREATE PROCEDURE GetAllCourse()
BEGIN
    SELECT CNO,CNAME,CCREDIT
    FROM COURSE;
END;

-- 调用存储过程
CALL GetAllCourse

【例2】创建有参存储过程

CREATE PROCEDURE GetAvgScoreByCno(
    IN pCno VARCHAR(5),
    OUT pScore INT
)
BEGIN
    SELECT AVG(grade) INTO pScore
    FROM SC
    WHERE CNO = pCno;
END;

-- 调用存储过程
CALL GetAvgScoreByCno('1',@score);
SELECT @score;

1.2 查看存储过程

SHOW PROCEDURE STATUS [LIKE 'pattern' | WHERE expr]
  • LIKE:用于匹配存储过程的名称
  • WHERE:指定更多的过滤条件

【例1】查看存储过程信息

SHOW PROCEDURE STATUS where Name like 'Get%' and db = 'course'

查看存储过程信息

【例2】查看存储过程的定义

SHOW CREATE PROCEDURE GetAvgScoreByCno;

查看存储过程的定义

1.3 修改和删除存储过程

如果想要为存储过程增加或删除某个参数,或者修改存储过程的定义,只能通过下文中的DROP PROCEDURE语句删除该存储过程,然后再次使用CREATE PROCEDURE语句创建存储过程。

修改存储过程:

ALTER PROCEDURE proc_name
COMMENT 'string’
| LANGUAGE SQL
| { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }

删除存储过程:

DROP PROCEDURE [IF EXISTS] sp_name;

2 存储函数

存储函数(Stored Function)和存储过程类似,也是存储在数据库中的程序,但是它会返回一个计算结果。存储函数可以和内置函数或者表达式一样用于SQL语句,可以提高代码的可读性以及可维护性。
MySQL 存储过程和存储函数统称为存储例程(Stored Routine)

2.1 创建存储函数

CREATE FUNCTION sp_name (
  param_name data_type,
  ...
)
RETURNS data_type
  routine_body;
  • sp_name:存储函数名称
  • param_name:参数名称,所有的参数都是输入参数
  • data_type:参数或者返回值的数据类型
  • RETURNS:定义返回值的类型
  • routine_body:函数的具体实现(同存储过程)

其他参数详见存储过程

【例1】创建函数add2

CREATE FUNCTION add2(
    p1 int,
    p2 int
)
RETURNS int
DETERMINISTIC --表示这是一个确定性函数,对于相同的输入参数一定会返回相同的结果
BEGIN
    RETURN p1 + p2;
END;

-- 调用函数
SELECT ADD2(3,5)

【例2】创建带参函数

CREATE FUNCTION funcGetAvgScoreByCno(
     pCno VARCHAR(5)
)
RETURNS INT
DETERMINISTIC
BEGIN
    set @pScore=0;
    SELECT AVG(GRADE) INTO @pScore
    FROM SC
    WHERE CNO = pCno;
    return @pScore;
END;

2.2 查看存储函数

同存储过程

SHOW FUNCTION STATUS [LIKE 'pattern' | WHERE expr]
  • LIKE:用于匹配存储过程的名称
  • WHERE:指定更多的过滤条件

2.3 修改和删除存储函数

与存储过程相同,修改存储函数的定义也只能通过DROP FUNCTION语句先删除,然后再次使用CREATE FUNCTION语句创建存储函数。

修改存储函数:

ALTER FUNCTION proc_name
COMMENT 'string’
| LANGUAGE SQL
| { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }

删除存储函数:

DROP FUNCTION [IF EXISTS] sp_name;

3 触发器

MySQL触发器(Trigger)是一种存储程序,它和一个指定的表相关联,当该表中的数据发生变化(增加、更新、删除)时自动执行。 这些修改数据行的操作被称为触发器事件,例如INSERT或者LOAD DATA等插入数据的语句可以激活插入触发器。

按照SQL标准,触发器可以分为行级触发器(Row-level Trigger)和语句级触发器(Statement-level Trigger)。行级触发器对于修改的每一行数据都会激活一次,如果一个语句插入了100行数据,将会调用触发器100次;语句级触发器针对每个语句激活一次,一个插入100行数据的语句只会调用一次触发器。MySQL只支持行级触发器,不支持语句级触发器。

触发器的优点:

  • 记录并审核用户对表中数据的修改操作,实现审计功能
  • 实现比检查约束更复杂的完整性约束,例如禁止非业务时间的数据操作
  • 实现某种业务逻辑,例如增加或删除员工时自动更新部门中的人数
  • 同步实时地复制表中的数据

触发器的缺点:

  • 触发器会增加数据库结构的复杂度,而且触发器对应用程序不可见,难以调试
  • 触发器需要占用更多的数据库服务器资源,尽量使用数据库提供的非空、唯一、检查约束等
  • 触发器不能接收参数,只能基于当前的触发对象进行操作

3.1 创建触发器

CREATE TRIGGER trigger_name
{ BEFORE | AFTER } { INSERT | UPDATE | DELETE } ON table_name
FOR EACH ROW
trigger_body;
  • trigger_name:触发器名称
  • BEFORE、AFTER:指定触发器的触发时机
  • INSERT、UPDATE、DELETE:用于定义触发事件的类型
  • table_name:触发器关联的表名,不能是临时表或者视图
  • FOR EACH ROW:表明这是一个行级触发器
  • trigger_body:触发器执行的具体语句

触发体中,使用NEWOLD关键字用于指示变量改变前后,如下例所示。

【例1】零件重量变化触发器

-- 首先创建审计表
CREATE TABLE p_weight_audit (
    audit_id    INTEGER NOT NULL AUTO_INCREMENT,
    p_id      VARCHAR(20) NOT NULL,
    old_weight  INT NULL,
    new_weight  INT NULL,
    change_date TIMESTAMP NOT NULL,
    change_by   VARCHAR(50) NOT NULL,
    CONSTRAINT pk_p_weight_audit PRIMARY KEY (audit_id)
);

-- 创建触发器
CREATE TRIGGER tri_audit_weight
AFTER UPDATE ON p
FOR EACH ROW
BEGIN
    -- 当重量改变时,记录审计数据
    IF (NEW.weight <> OLD.weight) THEN
        INSERT INTO p_weight_audit (p_id, old_weight, new_weight, change_date, change_by)
        VALUES(OLD.pno, OLD.weight, NEW.weight, CURRENT_TIMESTAMP, USER());
    END IF;
END;

-- 测试
UPDATE P
SET WEIGHT = WEIGHT*1.2
WHERE PNO = 'P2’
mysql> SELECT * FROM P_WEIGHT_AUDIT WHERE P_id='P2';
+----------+------+------------+------------+---------------------+----------------+
| audit_id | p_id | old_weight | new_weight | change_date         | change_by      |
+----------+------+------------+------------+---------------------+----------------+
|        2 | P2   |         17 |         20 | 2023-10-11 16:34:04 | root@localhost |
+----------+------+------------+------------+---------------------+----------------+

3.2 查看触发器

SHOW TRIGGERS
[{FROM | IN} db_name]
[LIKE 'pattern' | WHERE expr]
  • db_name:用于查看指定数据库中的触发器,默认为当前数据库
  • LIKE:用于匹配存储过程的名称
  • WHERE:指定更多的过滤条件

3.3 删除触发器

DROP TRIGGER IF EXISTS trigger_name;

IF EXISTS可以避免触发器trigger_name不存在时产生错误。


4 存储过程变量

变量(Variable)是存储过程和函数内部用于存储中间结果的对象。这些变量属于局部变量,作用域限定在存储过程中。

4.1 变量的定义

在使用存储过程变量之前需要使用DECLARE语句声明变量:

DECLARE variable_name datatype(size) [DEFAULT default_value];
  • DECLARE:表示定义变量,必须位于BEGIN和END之间并且为第一个语句
  • variable_name:变量名
  • datatype(size):变量的类型和长度
  • DEFAULT default_value:用于为变量指定一个默认值,否则默认值为 NULL

【例】定义不同数值类型的变量

DECLARE salary DECIMAL(8, 2) DEFAULT 0.0;
DECLARE x, y INTEGER DEFAULT 0;

salary是DECIMAL(8, 2)类型的变量,默认值为0.0;x和y是INTEGER类型变量,默认值为0。

4.2 变量的赋值

声明了变量之后,可以使用SET语句进行赋值:

SET variable_name = value;

或者使用SELECT INTO语句将查询的结果赋值给变量:

SELECT expression1, ...
INTO variable1, ...
FROM ...;

SELECT expression1, ...
FROM ...
INTO variable1, ...;

SELECT返回的表达式个数和变量的个数相同。查询语句最多只能返回一行数据,可以使用LIMIT 1进行限定。

【例】创建存储过程TestProc

CREATE PROCEDURE TestProc()
BEGIN
  DECLARE sal decimal(8, 2) DEFAULT 0.0;
  DECLARE x, y integer DEFAULT 0;

  SELECT ccredit
  FROM course
  WHERE cname like '数据库'
  INTO sal;

  SET x = 6, y = 9;

  SELECT sal, x, y;
END;

-- 调用该存储过程查看变量的赋值结果
CALL TestProc();

testProc


5 条件控制语句

MySQL提供了两种条件控制语句:IF语句和CASE语句。

5.1 IF语句

IF 语句可以实现基本的条件判断结构,语法如下:

IF search_condition THEN statement_list
    [ELSEIF other_condition THEN other_statement_list] 
    ...
    [ELSE else_statement_list]
END IF;

如果search_condition的结果为true,指定对应THEN之后的statement_list语句列表;否则,如果存在可选的ELSEIF并且other_condition结果为true,执行对应的other_statement_list语句列表;依次类推;否则,如果存在可选的ELSE,执行对应的else_statement_list语句列表。

【例】调用如下含IF语句的存储过程

DROP PROCEDURE TestProc;

CREATE PROCEDURE TestProc()
BEGIN
  DECLARE x, y integer DEFAULT 0; 
  SET x = 5, y = 10;

  IF x = 5 THEN
    SELECT 'x = 5';
  END IF;

  IF y < 10 THEN
    SELECT 'y < 10';
  ELSEIF y > 10 THEN
    SELECT 'y > 10';
  ELSE
    SELECT 'y = 10';
  END IF;
END

-- 调用该存储过程
CALL TestProc();
x = 5
x = 5

y = 10
y = 10

5.2 CASE语句

CASE语句可以用于构造复杂的条件判断结构,支持两种形式的语法。

  1. 简单CASE语句:
CASE case_value
    WHEN when_value1 THEN statement_list1
    [WHEN when_value2 THEN statement_list2] 
    ...
    [ELSE else_statement_list]
END CASE;

其中,case_value是一个表达式;首先使用该表达式的值和when_value1进行比较,如果相等则执行statement_list1语句列表并结束CASE语句;否则,如果表达式等于可选的when_value2,则执行statement_list2并结束CASE语句;依次类推;最后,如果存在可选的ELSE,执行else_statement_list语句列表;如果此时没有定义ELSE子句,将会返回“Case not found for CASE statement”错误。

  1. 搜索CASE语句
CASE
    WHEN search_condition1 THEN statement_list1
    [WHEN search_condition2 THEN statement_list2] 
    ...
    [ELSE else_statement_list]
END CASE;

首先,判断search_condition1是否为true;如果是则执行statement_list1语句列表;否则,判断search_condition2是否为true,如果是则执行statement_list2语句列表;依此类推;最后,如果存在可选的ELSE,执行else_statement_list语句列表;如果此时没有定义ELSE子句,将会返回“Case not found for CASE statement”错误。

【例】调用如下含有搜索CASE语句的存储过程

DROP PROCEDURE TestProc;

DELIMITER $$

CREATE PROCEDURE TestProc()
BEGIN
  DECLARE x integer DEFAULT 0;
  SET x = 5;

  CASE
    WHEN x < 0 THEN
      SELECT 'x < 0';
    WHEN x = 0 THEN
      SELECT 'x = 0';
    ELSE
      SELECT 'x > 0';
  END CASE;

END$$

DELIMITER;
-- 调用该存储过程
CALL TestProc();
x > 0
x > 0

6 循环控制语句

MySQL支持三种循环控制语句:LOOP、REPEAT以及WHILE语句。

6.1 LOOP语句

LOOP语句用于实现简单的循环结构,语法如下:

[begin_label:] LOOP
    statement_list
END LOOP [end_label]

重复执行statement_list直到循环被终止,通常使用LEAVE label语句退出循环,或者使用函数的RETURN语句退出整个函数。begin_label和end_label是该循环语句的标签。

【例】调用如下含有LOOP语句的存储过程

DELIMITER $$

CREATE PROCEDURE TestLoop()
BEGIN
  DECLARE x, sumx integer DEFAULT 0;

  label1: LOOP
    SET x = x + 1;

    IF x > 10 THEN
      LEAVE label1;
    END IF;
    IF x mod 2 THEN
      ITERATE label1;
    ELSE
      SET sumx = sumx + x;
    END IF;

  END LOOP label1;

  SELECT sumx;

END$$

DELIMITER;

-- 调用存储过程
CALL TestLoop();
sumx
30

其中LEAVE表示x大于10时退出循环label1;ITERATE类似于C++或者Java中的continue,表示x为奇数则进入下一次循环,否则将其增加到变量sumx。

6.2 REPEAT语句

REPAET语句用于重复执行指定语句列表直到某个条件为真,语法如下:

[begin_label:] REPEAT
    statement_list
UNTIL search_condition
END REPEAT [end_label];

REPEAT语句首先执行statement_list,然后判断search_condition是否为true;如果是则终止循环,否则再次循环。REPAET语句至少会执行一次。

【例】6.1中的LOOP语句示例使用REPAET实现如下

DELIMITER $$

CREATE PROCEDURE TestRepeat()
BEGIN
  DECLARE x, sumx integer DEFAULT 0;

  label1: REPEAT
    SET x = x + 1;

    IF x mod 2 THEN
      ITERATE label1;
    ELSE
      SET sumx = sumx + x;
    END IF;

  UNTIL x = 10 END REPEAT label1;

  SELECT sumx;

END$$

DELIMITER;

UNTIL 指定了x = 10时退出循环,执行该存储过程的结果和上一节相同。

6.3 WHILE语句

WHILE语句基于某个条件为真时重复执行指定的语句列表,语法如下:

[begin_label:] WHILE search_condition DO
    statement_list
END WHILE [end_label];

首先,判断search_condition是否为true;如果是则执行statement_list语句列表,否则退出循环语句。WHILE语句可能一次也不执行。

【例】6.1中的LOOP语句示例使用WHILE实现如下

DELIMITER $$
CREATE PROCEDURE TestWhile()
BEGIN
  DECLARE x, sumx integer DEFAULT 0;

  label1: WHILE x < 10 DO
    SET x = x + 1;

    IF x mod 2 THEN
      ITERATE label1;
    ELSE
      SET sumx = sumx + x;
    END IF;

  END WHILE label1;

  SELECT sumx;

END$$

DELIMITER;

7 错误处理

当存储过程或者函数在执行过程中出现某种错误条件(例如警告或者异常)时,需要进行特殊处理,例如退出当前程序模块或者继续执行,同时返回一个有用的错误信息。

7.1 定义错误条件

MySQL提供了定义错误条件的DECLARE ... CONDITION语句:

DECLARE condition_name CONDITION 
FOR { mysql_error_code | SQLSTATE [VALUE] sqlstate_value }
  • condition_name:错误条件的名称,可以用于随后的错误处理器声明
  • mysql_error_code:MySQL错误码,例如1062代表了唯一键重复错误
  • SQLSTATE:使用5位字符串代表的SQLSTATE值,例如“42000”和错误码1062一样代表了唯一键重复错误。

7.2 定义错误处理器

定义了错误条件之后,可以使用DECLARE ... HANDLER语句定义处理一个或多个错误条件的错误处理器:

DECLARE { CONTINUE | EXIT } HANDLER
    FOR condition_value [, condition_value] ...
    statement;

condition_value: {
    mysql_error_code
  | SQLSTATE [VALUE] sqlstate_value
  | condition_name
  | SQLWARNING
  | NOT FOUND
  | SQLEXCEPTION
}

该语句定义了一个错误处理器——当某个错误条件condition_value发生时,执行指定的statement语句,可以是单个语句或者BEGIN/END代码块;然后执行相应的操作,CONTINUE表示继续执行当前程序,EXIT表示终止执行错误处理器所在的BEGIN/END模块。

指定错误条件时,mysql_error_code表示MySQL错误码,SQLSTATE表示使用5位字符串代表的SQLSTATE值,condition_name是使用该定义错误处理器语句定义的错误条件名称,SQLWARNING代表了以“01”开头的一类SQLSTATE值,NOT FOUND代表了以“02”开头的一类SQLSTATE值,SQLEXCEPTION代表了以“00”、“01”或者“02”开头的一类SQLSTATE值。

【例】如下所示代码定义了一个错误条件 duplicate_employee,表示员工信息重复错误;然后定义了一个错误处理器,发生该错误时回滚数据修改并且输出一个消息,使用命名的错误条件可以提高代码的可读性。

DROP PROCEDURE IF EXISTS TestError;
DELIMITER $$

CREATE PROCEDURE TestError()
BEGIN
  DECLARE stuid varchar(10) DEFAULT '201215121';
  DECLARE duplicate_student CONDITION FOR 1062;

  DECLARE EXIT HANDLER
  FOR duplicate_student
  BEGIN
    ROLLBACK;
    SELECT CONCAT('重复的学生信息!', 'stuid:', stuid) Message;
  END;    

  INSERT INTO student 
  VALUES (stuid, '张三三', '男', 20, 'CS');
END$$

DELIMITER ;

-- 调用示例存储过程
CALL TestError();
Message
重复的学生信息!stuid:201215121

7.3 获取诊断信息

MySQL 在发生错误时会生成诊断信息,应用程序可以通过GET DIAGNOSTICS语句获取这些信息(也可以使用 SHOW WARNINGS 或者 SHOW ERRORS 查看相关的错误信息)。

mysql> DROP TABLE no_such_table;
1051 - Unknown table 'course.no_such_table'
mysql> GET DIAGNOSTICS CONDITION 1
    -> @p1 = RETURNED_SQLSTATE, @p2 = MESSAGE_TEXT;
Query OK, 0 rows affected

mysql> SELECT @p1, @p2;
+-------+--------------------------------------+
| @p1   | @p2                                  |
+-------+--------------------------------------+
| 42S02 | Unknown table 'course.no_such_table' |
+-------+--------------------------------------+
1 row in set

GET DIAGNOSTICS语句的完整语法如下:

GET [CURRENT | STACKED] DIAGNOSTICS {
    target = NUMBER | target = ROW_COUNT,
    ...
} 

GET [CURRENT | STACKED] DIAGNOSTICS 
CONDITION condition_number {
    target = condition_information_item_name,
    ...
} 

condition_information_item_name: {
    CLASS_ORIGIN
  | SUBCLASS_ORIGIN
  | RETURNED_SQLSTATE
  | MESSAGE_TEXT
  | MYSQL_ERRNO
  | CONSTRAINT_CATALOG
  | CONSTRAINT_SCHEMA
  | CONSTRAINT_NAME
  | CATALOG_NAME
  | SCHEMA_NAME
  | TABLE_NAME
  | COLUMN_NAME
  | CURSOR_NAME
}

其中,CURRENT表示返回当前诊断区域中的信息,STACKED表示返回第二诊断区域的信息(只能用于 DECLARE HANDLER语句之中)。默认返回当前诊断区域中的信息。

简单来说,诊断区域中存储了两类信息:

  • 语句信息,包括错误条件的编号(NUMBER)和影响的行数(ROW_COUNT),对应上面的第一种语法。
  • 条件信息,例如错误码(MYSQL_ERRNO)和错误信息(MESSAGE_TEXT)等,对应上面的第二种语法。如果一个语句触发了多个错误条件,每个错误条件都提供了一个条件区域,编号从1到NUMBER。

【例】在存储过程中获取诊断信息(其中cno是返回的错误条件编号,code和msg是错误编码和信息,nrows是成功插入的行数)

CREATE TABLE t(id int NOT NULL);

DELIMITER $$

CREATE PROCEDURE do_insert(value INT)
BEGIN
  -- 声明保存诊断信息的变量
  DECLARE cno INT;
  DECLARE code CHAR(5) DEFAULT '00000';
  DECLARE msg TEXT;
  DECLARE nrows INT;
  DECLARE result TEXT;
  -- 声明异常错误处理器
  DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
    BEGIN
      GET DIAGNOSTICS cno = NUMBER;
      GET DIAGNOSTICS CONDITION cno
        code = RETURNED_SQLSTATE, msg = MESSAGE_TEXT;
    END;

  -- 执行插入语句
  INSERT INTO t(id) VALUES(value);
  -- 检查插入是否成功
  IF code = '00000' THEN
    GET DIAGNOSTICS nrows = ROW_COUNT;
    SET result = CONCAT('insert succeeded, row count = ',nrows);
  ELSE
    SET result = CONCAT('insert failed, error = ',code,', message = ',msg);
  END IF;
  -- 返回错误信息
  SELECT result;
END$$

DELIMITER ;
mysql> CALL do_insert(1);
+---------------------------------+
| result                          |
+---------------------------------+
| insert succeeded, row count = 1 |
+---------------------------------+
1 row in set
mysql> CALL do_insert(NULL);
+--------------------------------------------------------------------+
| result                                                             |
+--------------------------------------------------------------------+
| insert failed, error = 23000, message = Column 'id' cannot be null |
+--------------------------------------------------------------------+
1 row in set

7.4 抛出错误条件

除了捕获和处理错误之外,MySQL还提供了抛出错误的SIGNALRESIGNAL语句。

SIGNAL { SQLSTATE [VALUE] sqlstate_value | condition_name }
    [SET condition_information_item_name = simple_value, ...]

condition_information_item_name: {
    CLASS_ORIGIN
  | SUBCLASS_ORIGIN
  | MESSAGE_TEXT
  | MYSQL_ERRNO
  | CONSTRAINT_CATALOG
  | CONSTRAINT_SCHEMA
  | CONSTRAINT_NAME
  | CATALOG_NAME
  | SCHEMA_NAME
  | TABLE_NAME
  | COLUMN_NAME
  | CURSOR_NAME
}

SIGNAL可以抛出一个SQLSTATE值或者命名的错误条件,SET子句可以设置不同的信息,它们可以在调用程序中使用 GET DIAGNOSTICS语句捕获。

【例】抛出错误

DELIMITER $$

CREATE PROCEDURE p(divisor INT)
BEGIN
  IF divisor = 0 THEN
    SIGNAL SQLSTATE '22012'
    SET MESSAGE_TEXT = 'devided by zero!';
  END IF;
END;

DELIMITER ;

-- 调用示例存储过程
CALL p(0);

存储过程 p 抛出了一个自定义的错误条件,指定了 SQLSTATE 值和错误消息。传入参数0后抛出错误的结果为:

ERROR 1644 (22012): devided by zero!

RESIGNAL语句也可以抛出错误,语法和SIGNAL相同,但RESIGNAL和SIGNAL存在一些差异:

  • RESIGNAL语句只能用于错误处理器的处理语句中,可以修改某些错误信息后再次抛出
  • RESIGNAL语句的属性可以忽略,意味着将接收到的错误不经修改再次抛出

8 游标

游标(Cursor)可以支持对查询结果的遍历,从而实现数据集的逐行处理。MySQL存储过程和函数支持游标,它们具有以下特性:

  • 灵敏性(Asensitive):服务器可能直接遍历基础表的数据,也可能复制一份额外的临时数据
  • 只读性(Read only):不能通过游标修改基础表中的数据
  • 不可滚动性(Nonscrollable):只能按照查询结果的顺序访问数据,不能反向遍历,也不能跳跃访问数据行

使用MySQL游标的过程如下:

  1. 通过DECLARE语句声明游标
  2. 使用OPEN语句打开游标
  3. 循环使用FETCH语句获取游标中的数据行
  4. 使用CLOSE语句关闭游标并释放资源

8.1 声明游标

MySQL使用DECLARE语句声明游标:

DECLARE cursor_name CURSOR FOR select_statement;

其中,cursor_name是游标名称,select_statement定义了与游标关联的查询语句。游标声明必须在变量和错误条件(CONDITION)声明之后,以及错误处理器(HANDLER)声明之前。

【例】声明一个游标cur1

DECLARE cur1 CURSOR FOR
    SELECT sno, sname FROM STUDENT WHERE ssex = '女';

8.2 打开游标

声明之后,使用OPEN语句打开游标,即执行查询初始化结果集:

OPEN cur1;

8.3 遍历游标

循环使用FETCH语句获取下一行数据,并移动游标指针:

FETCH cur1 INTO id, name;

同时,在循环中检查是否还有更多数据行;如果没有则退出循环。变量done在预定义的错误处理器中进行设置,没有找到数据(NOT FOUND)时设置为true。

8.4 关闭游标

最后使用 CLOSE 语句关闭游标,关闭游标可以释放相关的资源。

CLOSE cur1;

【例】一个完整的使用游标的示例

DELIMITER $$

CREATE PROCEDURE TestCurosr()
BEGIN
  DECLARE done INT DEFAULT FALSE;
  DECLARE id VARCHAR(10);
  DECLARE name varchar(50);
  DECLARE namelist varchar(500) default '';
  DECLARE cur1 CURSOR FOR 
    SELECT sno, sname FROM STUDENT WHERE ssex = '女';
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

  OPEN cur1;

  getStu: LOOP
    FETCH cur1 INTO id, name;
    IF done THEN
      LEAVE getStu;
    ELSE
      SET namelist = concat( name, ';', namelist);
    END IF;
  END LOOP;

  CLOSE cur1;

  SELECT namelist;
END$$

DELIMITER ;

-- 调用示例存储过程
CALL TestCurosr();
+----------+
| namelist |
+----------+
| 刘%;     |
+----------+

9 访问权限控制

在存储过程和函数的定义中,可以使用SQL SECURITY属性指定定义者权限(DEFINER)或者调用者权限(INVOKER)。

对于SQL SECURITY DEFINER,存储过程使用定义者的权限执行,定义者可以通过DEFINER属性进行指定,默认为当前用户。

对于SQL SECURITY INVOKER,存储过程将会使用调用者的权限执行,此时DEFINER属性不会对执行产生任何作用。如果调用者的权限较低,定义者的权限较高,调用者仍然能够获得存储过程内部操作所需的权限。

发表评论