备注:测试数据库版本为MySQL 8.0
这个blog我们来聊聊MySQL存储过程,MySQL的存储过程比较其它如Oracle、SqlServer、PostgreSQL会弱很多,但是也能实现一些单纯sql语句不能实现,或者是实现起来比较复杂业务场景,有总比没有强,而且Oracle公司在加强对MySQL各方面的提升,也许后面MySQL的存储过程会有很大的提升。
一.语法相关
语法:
CREATE
[DEFINER = user]
PROCEDURE sp_name ([proc_parameter[,...]])
[characteristic ...] routine_body
proc_parameter:
[ IN | OUT | INOUT ] param_name type
type:
Any valid MySQL data type
characteristic: {
COMMENT 'string'
| LANGUAGE SQL
| [NOT] DETERMINISTIC
| { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }
}
routine_body:
Valid SQL routine statement
1.DEFINER:
DEFINER模式下,默认DEFINER=CURRENT_USER,在存储过程执行时,mysql会检查DEFINER定义的用户'user_name'@'host_name'的权限;
2.proc_parameter:
参数列表:不同于函数的参数列表,需要指明参数类型
IN,表示输入型
OUT,表示输出型
INOUT,表示混合型,即可以输入,在存储过程里面也可以修改,最后输出
3.characteristic:
3.1 LANGUAGE SQL
存储过程语言,默认是sql,说明存储过程中使用的是sql语言编写的,暂时只支持sql,后续可能会支持其他语言
3.2 NOT DETERMINISTIC
是否确定性的输入就是确定性的输出,默认是NOT DETERMINISTIC,只对于同样的输入,输出也是一样的,当前这个值还没有使用
3.3 CONTAINS SQL
提供子程序使用数据的内在信息,这些特征值目前提供给服务器,并没有根据这些特征值来约束过程实际使用数据的情况,说白了就是没有使用的
包括以下四种选择
3.3.1 CONTAINS SQL表示子程序不包含读或者写数据的语句
3.3.2 NO SQL 表示子程序不包含sql
3.3.3 READS SQL DATA 表示子程序包含读数据的语句,但是不包含写数据的语句
3.3.4 MODIFIES SQL DATA 表示子程序包含写数据的语句。
3.4 SQL SECURITY DEFINER
用来指定存储过程是使用创建者的许可来执行,还是执行者的许可来执行,默认值是DEFINER
DEFINER 创建者的身份来调用,对于当前用户来说:如果执行存储过程的权限,且创建者有访问表的权限,当前用户可以成功执行过程的调用的
说白了就是当前用户调用存储过程,存储过程执行的具体操作是借助定义存储过程的user的权限执行的。
INVOKER 调用者的身份来执行,对于当前用户来说:如果执行存储过程的权限,以当前身份去访问表,如果当前身份没有访问表的权限,即便是有执行过程的权限,仍然是无法成功执行过程的调用的。
说白了就是当前用户调用存储过程,只有当前用户有执行存储过程中涉及的对象的操作的权限的时候,才能成功执行。
3.5 COMMENT
存储过程的注释性信息写在COMMENT里面,这里只能是单行文本,多行文本会被移除到回车换行等。
二.案例
上面看了存储过程的语法,下面测试几个存储过程的案例,来帮助我们熟悉MySQL的存储过程
需求:对于删除用户数据,需要保留用户数据到历史表,并保留一条删除的信息到审计表。
测试数据:
-- 用户表
create table user_info(id int not null auto_increment,name varchar(200),id_number varchar(50),primary key(id));
insert into user_info values (1,'张三','420123199001011234');
insert into user_info values (2,'李四','420123199001011235');
insert into user_info values (3,'王五','420123199001011236');
-- 用户历史数据表
create table user_info_history(id int not null auto_increment,type varchar(50),user_id int,name varchar(200),id_number varchar(50),create_time datetime,primary key(id));
-- 用户记录删除审计表
create table audit_log(id int not null auto_increment ,type varchar(50),login_user varchar(50),audit_date datetime,user_id int,primary key(id));
存储过程:
delimiter //
create procedure p_del_users(in pi_user_id int,
in pi_login_user varchar(200)
)
begin
-- 将用户数据录入用户历史表
insert into user_info_history(type,user_id,name,id_number,create_time)
select 'DELETE',id,name,id_number,now()
from user_info
where id = pi_user_id;
-- 删除用户数据
delete from user_info where id = pi_user_id;
-- 增加审计记录
insert into audit_log(type,login_user,audit_date,user_id)
select 'DELETE',pi_login_user,now(),pi_user_id;
end;
//
delimiter ;
测试记录:
mysql> delimiter //
mysql>
mysql> create procedure p_del_users(in pi_user_id int,
-> in pi_login_user varchar(200)
-> )
-> begin
->
-> -- 将用户数据录入用户历史表
-> insert into user_info_history(type,user_id,name,id_number,create_time)
-> select 'DELETE',id,name,id_number,now()
-> from user_info
-> where id = pi_user_id;
->
-> -- 删除用户数据
-> delete from user_info where id = pi_user_id;
->
-> -- 增加审计记录
-> insert into audit_log(type,login_user,audit_date,user_id)
-> select 'DELETE',pi_login_user,now(),pi_user_id;
->
-> end;
-> //
Query OK, 0 rows affected (0.01 sec)
mysql>
mysql> delimiter ;
mysql>
mysql>
mysql> select * from user_info;
+----+--------+--------------------+
| id | name | id_number |
+----+--------+--------------------+
| 1 | 张三 | 420123199001011234 |
| 2 | 李四 | 420123199001011235 |
| 3 | 王五 | 420123199001011236 |
+----+--------+--------------------+
3 rows in set (0.00 sec)
mysql>
-- 调用存储过程,删除id为1的用户表记录
mysql> call p_del_users(1,'TX0001');
Query OK, 1 row affected, 1 warning (0.01 sec)
-- 查询表 发现已被删除
mysql> select * from user_info;
+----+--------+--------------------+
| id | name | id_number |
+----+--------+--------------------+
| 2 | 李四 | 420123199001011235 |
| 3 | 王五 | 420123199001011236 |
+----+--------+--------------------+
2 rows in set (0.00 sec)
-- 用户历史记录保留表也保留了记录
mysql> select * from user_info_history;
+----+--------+---------+--------+--------------------+---------------------+
| id | type | user_id | name | id_number | create_time |
+----+--------+---------+--------+--------------------+---------------------+
| 1 | DELETE | 1 | 张三 | 420123199001011234 | 2020-05-25 17:04:22 |
+----+--------+---------+--------+--------------------+---------------------+
1 row in set (0.00 sec)
-- 审计表也有删除审计的数据
mysql> select * from audit_log;
+----+--------+------------+---------------------+---------+
| id | type | login_user | audit_date | user_id |
+----+--------+------------+---------------------+---------+
| 1 | DELETE | TX0001 | 2020-05-25 17:04:22 | 1 |
+----+--------+------------+---------------------+---------+
1 row in set (0.00 sec)
mysql>
这样看,是不是觉得好简单,存储过程也不怎么样啦,就是把3个sql拼接起来了而已,也没什么复杂的。
别慌,我下面慢慢来增加复杂程度。
MySQL默认是自动提交,这个时候,3个sql中如果有一个失败了,例如第2个sql成功的删除了用户数据,但是第3个sql增加审计记录的时候报错了,没有成功,这样就会导致审计数据的缺失,这个时候我们该如何处理呢?
我们来看下面的例子:
-- 把删除的用户从新录入用户表
insert into user_info values (1,'张三','420123199001011234');
-- 将audit_log表user_id设为唯一索引,这样第二次录入数据就会报错
create unique index i_auditlog_userid on audit_log(user_id);
-- 运行存储过程报错
mysql> call p_del_users(1,'TX0001');
ERROR 1062 (23000): Duplicate entry '1' for key 'audit_log.i_auditlog_userid'
-- 用户表数据删除
mysql> select * from user_info;
+----+--------+--------------------+
| id | name | id_number |
+----+--------+--------------------+
| 2 | 李四 | 420123199001011235 |
| 3 | 王五 | 420123199001011236 |
+----+--------+--------------------+
2 rows in set (0.00 sec)
-- 历史记录表,也保留了历史数据
mysql> select * from user_info_history;
+----+--------+---------+--------+--------------------+---------------------+
| id | type | user_id | name | id_number | create_time |
+----+--------+---------+--------+--------------------+---------------------+
| 1 | DELETE | 1 | 张三 | 420123199001011234 | 2020-05-25 17:04:22 |
| 2 | DELETE | 1 | 张三 | 420123199001011234 | 2020-05-25 17:19:30 |
+----+--------+---------+--------+--------------------+---------------------+
2 rows in set (0.00 sec)
-- 审计表报错,审计记录缺失
mysql>
mysql> select * from audit_log;
+----+--------+------------+---------------------+---------+
| id | type | login_user | audit_date | user_id |
+----+--------+------------+---------------------+---------+
| 1 | DELETE | TX0001 | 2020-05-25 17:04:22 | 1 |
+----+--------+------------+---------------------+---------+
1 row in set (0.00 sec)
此时,可以在存储过程里面,加入事务,这样3个sql语句,要么一起成功,要么一起失败
-- 把删除的用户从新录入用户表
insert into user_info values (1,'张三','420123199001011234');
delimiter //
create procedure p_del_users(in pi_user_id int,
in pi_login_user varchar(200)
)
begin
-- 开启事务
start transaction;
-- 将用户数据录入用户历史表
insert into user_info_history(type,user_id,name,id_number,create_time)
select 'DELETE',id,name,id_number,now()
from user_info
where id = pi_user_id;
-- 删除用户数据
delete from user_info where id = pi_user_id;
-- 增加审计记录
insert into audit_log(type,login_user,audit_date,user_id)
select 'DELETE',pi_login_user,now(),pi_user_id;
-- 开启事务后需要主动提交
commit;
end;
//
delimiter ;
运行结果如下:
-- 此时直接报错了,而且查询数据,3个sql均没有成功
mysql> p_del_users(1,'TX0001');
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'p_del
_users(1,'TX0001')' at line 1
mysql>
mysql> select * from user_info;
+----+--------+--------------------+
| id | name | id_number |
+----+--------+--------------------+
| 1 | 张三 | 420123199001011234 |
| 2 | 李四 | 420123199001011235 |
| 3 | 王五 | 420123199001011236 |
+----+--------+--------------------+
3 rows in set (0.00 sec)
mysql> select * from audit_log;
+----+--------+------------+---------------------+---------+
| id | type | login_user | audit_date | user_id |
+----+--------+------------+---------------------+---------+
| 1 | DELETE | TX0001 | 2020-05-25 17:04:22 | 1 |
+----+--------+------------+---------------------+---------+
1 row in set (0.00 sec)
mysql>
如果我不想开发人员调用我的存储过程的时候报错,成功就commit,失败就rollback,然后返回一个标识给开发人员
此时,程序要做如下调整:
delimiter //
create procedure p_del_users(in pi_user_id int,
in pi_login_user varchar(200),
out po_result varchar(200)
)
begin
# 定义一个变量,默认为0
DECLARE l_error INTEGER DEFAULT 0;
# 当有SQL报错的时候,设置变量值为1
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET l_error=1;
# 给输出一个默认值
SET po_result = concat('删除',pi_user_id,'成功');
-- 开启事务
start transaction;
-- 将用户数据录入用户历史表
insert into user_info_history(type,user_id,name,id_number,create_time)
select 'DELETE',id,name,id_number,now()
from user_info
where id = pi_user_id;
-- 删除用户数据
delete from user_info where id = pi_user_id;
-- 增加审计记录
insert into audit_log(type,login_user,audit_date,user_id)
select 'DELETE',pi_login_user,now(),pi_user_id;
# 成功就删除,失败就回滚
IF l_error = 1 THEN
ROLLBACK;
# 如失败,修改输出
SET po_result = concat('删除',pi_user_id,'失败');
ELSE
COMMIT;
END IF;
end;
//
delimiter ;
执行结果如下:
-- 没有返回错误,通过返回结果知道这次删除数据失败
mysql> call p_del_users(1,'TX0001',@result);
Query OK, 0 rows affected, 2 warnings (0.00 sec)
mysql> select @result;
+---------------+
| @result |
+---------------+
| 删除1失败 |
+---------------+
1 row in set (0.00 sec)
现在我们又接到审计部门的一个需求,对于修改用户表的记录,也要做历史数据保留和审计记录
-- 删除 audit_log表唯一索引
drop index i_auditlog_userid on audit_log;
存储过程代码如下:
delimiter //
create procedure p_upd_users(in pi_user_id int,
in pi_login_user varchar(200),
in pi_name varchar(50),
in pi_idnumber varchar(50),
out po_result varchar(200)
)
begin
-- 定义一个变量,初始值为0
DECLARE l_error INTEGER DEFAULT 0;
-- 如果有SQL异常,则l_error为1
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET l_error=1;
-- 默认设置为成功
SET po_result = concat('修改',pi_user_id,'成功');
-- 开启事务
start transaction;
-- 将用户数据录入用户历史表
insert into user_info_history(type,user_id,name,id_number,create_time)
select 'UPDATE',id,name,id_number,now()
from user_info
where id = pi_user_id;
-- 修改用户数据
update user_info
set name = pi_name,
id_number = pi_idnumber
where id = pi_user_id;
-- 增加审计记录
insert into audit_log(type,login_user,audit_date,user_id)
select 'UPDATE',pi_login_user,now(),pi_user_id;
-- 没有错误就提交,有错误进行回滚
IF l_error = 1 THEN
ROLLBACK;
-- 如果有错误,返回删除失败
SET po_result = concat('修改',pi_user_id,'失败');
ELSE
COMMIT;
END IF;
end;
//
delimiter ;
执行结果如下:
mysql> call p_upd_users(1,'TX0001','刘伟','420123199001011234',@result);
Query OK, 0 rows affected, 4 warnings (0.01 sec)
mysql> select @result;
+---------------+
| @result |
+---------------+
| 修改1成功 |
+---------------+
1 row in set (0.00 sec)
mysql> select * from user_info;
+----+--------+--------------------+
| id | name | id_number |
+----+--------+--------------------+
| 1 | 刘伟 | 420123199001011234 |
| 2 | 李四 | 420123199001011235 |
| 3 | 王五 | 420123199001011236 |
+----+--------+--------------------+
3 rows in set (0.00 sec)
mysql> select * from user_info_history;
+----+--------+---------+--------+--------------------+---------------------+
| id | type | user_id | name | id_number | create_time |
+----+--------+---------+--------+--------------------+---------------------+
| 1 | DELETE | 1 | 张三 | 420123199001011234 | 2020-05-25 17:04:22 |
| 2 | DELETE | 1 | 张三 | 420123199001011234 | 2020-05-25 17:19:30 |
| 6 | UPDATE | 1 | 刘伟 | 420123199001011234 | 2020-05-25 17:36:39 |
+----+--------+---------+--------+--------------------+---------------------+
3 rows in set (0.00 sec)
mysql> select * from audit_log;
+----+--------+------------+---------------------+---------+
| id | type | login_user | audit_date | user_id |
+----+--------+------------+---------------------+---------+
| 1 | DELETE | TX0001 | 2020-05-25 17:04:22 | 1 |
| 6 | UPDATE | TX0001 | 2020-05-25 17:36:39 | 1 |
+----+--------+------------+---------------------+---------+
2 rows in set (0.00 sec)
假设此时开发不想调用2个存储过程,此时可以封装起来,让开发调用一个存储过程
delimiter //
create procedure p_main_users(in pi_type varchar(50),
in pi_user_id int,
in pi_login_user varchar(200),
in pi_name varchar(50),
in pi_idnumber varchar(50),
out po_result varchar(200)
)
begin
if pi_type = 'UPDATE' then
call p_upd_users(pi_user_id,pi_login_user,pi_name,pi_idnumber,po_result);
elseif pi_type = 'DELETE' then
call p_del_users(pi_user_id,pi_login_user,po_result);
else
-- 空的块,什么都不做
begin
end;
end if;
end;
//
delimiter ;
运行结果:
mysql> delimiter //
mysql>
mysql> create procedure p_main_users(in pi_type varchar(50),
-> in pi_user_id int,
-> in pi_login_user varchar(200),
-> in pi_name varchar(50),
-> in pi_idnumber varchar(50),
-> out po_result varchar(200)
-> )
-> begin
->
-> if pi_type = 'UPDATE' then
-> call p_upd_users(pi_user_id,pi_login_user,pi_name,pi_idnumber,po_result);
-> elseif pi_type = 'DELETE' then
-> call p_del_users(pi_user_id,pi_login_user,po_result);
-> else
-> -- 空的块,什么都不做
-> begin
-> end;
-> end if;
->
-> end;
-> //
Query OK, 0 rows affected (0.01 sec)
mysql>
mysql> delimiter ;
mysql>
mysql> call p_main_users('DELETE',1,'TX0002',null,null,@result);
Query OK, 0 rows affected, 7 warnings (0.01 sec)
mysql> select @result;
+---------------+
| @result |
+---------------+
| 删除1成功 |
+---------------+
1 row in set (0.00 sec)
mysql> call p_main_users('UPDATE',1,'TX0002','张伟','420123199001011235',@result);
Query OK, 0 rows affected, 4 warnings (0.01 sec)
mysql> select @result;
+---------------+
| @result |
+---------------+
| 修改1成功 |
+---------------+
1 row in set (0.00 sec)
这里只是简单的介绍了下MySQL的存储过程,结合前面的,循环和游标配合使用,MySQL可以实现复杂的程序逻辑。