MySQL存储过程

1、定义

1.1、什么是储存过程

存储过程是在大型数据库系统中,一组为了完成特定功能的SQL语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字和参数(如果该存储过程有参数)来执行它。存储过程是数据库中的一个重要对象,任何一个设计良好的数据库应用程序都应该用到存储过程。

优点:

  • 重复使用:存储过程可以重复使用,可以减少开发人员的工作量。

  • 提高效率:存储过程在第一次使用的时候会编译,一次编译后以后不用再次编译,提高了效率。

  • 减少网络流量:存储过程位于服务器上,调用的时候只需要传递存储过程的名称以及参数就可以了,因此降低了网络传输的数据量。

  • 安全性:参数化的存储过程可以防止SQL注入,而且可以将Grant、Deny以及Revoke权限应用于存储过程

  • 在生产环境下,可以通过直接修改存储过程的方式修改业务逻辑(或bug),而不用重启服务器。

缺点:

  • 调试麻烦

  • 移植性差

  • 维护性差

CREATE DEFINER=`root`@`%` PROCEDURE `getName`(IN `uid` int,OUT my_uname VARCHAR(255))
BEGIN
    #Routine body goes here...
  select admin_name into my_uname from admin_user where id=uid;
    select my_uname;
END

1.2、什么是存储函数

存储函数为一组存储程序,带函数名、参数,且返回一个结果集。存储函数和存储过程的结构类似,但必须有一个return 子句来返回结果。

-- 需要声明返回值类型,默认int
CREATE DEFINER=`root`@`%` FUNCTION `queryChildrenAreaInfo`(areaId INT) RETURNS varchar(4000) CHARSET utf8
BEGIN
    DECLARE sTemp VARCHAR(4000);
    DECLARE sTempChd VARCHAR(4000);
​
    SET sTemp='$';
    SET sTempChd = CAST(areaId AS CHAR);
​
    WHILE sTempChd IS NOT NULL DO
    SET sTemp= CONCAT(sTemp,',',sTempChd);
    SELECT GROUP_CONCAT(id) INTO sTempChd FROM personal_dept WHERE FIND_IN_SET(dept_parentId,sTempChd)>0;
    END WHILE;
    
    RETURN sTemp;
END

MySQL创建存储函数时报错:> 1418 - This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you might want to use the less safe log_bin_trust_function_creators variable)

原因分析:

因为CREATE PROCEDURE, CREATE FUNCTION, ALTER PROCEDURE,ALTER FUNCTION,CALL, DROP PROCEDURE, DROP FUNCTION等语句都会被写进二进制日志,然后在从服务器上执行。但是,一个执行更新的不确定子程序(存储过程、函数、触发器)是不可重复的,在从服务器上执行(相对与主服务器是重复执行)可能会造成恢复的数据与原始数据不同,从服务器不同于主服务器的情况。

为了解决这个问题,MySQL强制要求:

在主服务器上,除非子程序被声明为确定性的或者不更改数据,否则创建或者替换子程序将被拒绝。这意味着当创建一个子程序的时候,必须要么声明它是确定性的,要么它不改变数据。

解决办法也有两种:

第一种是在创建子程序(存储过程、函数、触发器)时,声明为DETERMINISTIC或NO SQL与READS SQL DATA中的一个,

create function plusefunction(num1 int,num2 int)
returns int
DETERMINISTIC
begin
    return num1 + num2;
END

第二种是信任子程序的创建者,禁止创建、修改子程序时对SUPER权限的要求,设置log_bin_trust_routine_creators全局系统变量为1。设置方法有三种:

  1. 在客户端上执行SET GLOBAL log_bin_trust_function_creators = 1;

  2. MySQL启动时,加上–log-bin-trust-function-creators选贤,参数设置为1

  3. 在MySQL配置文件my.ini或my.cnf中的[mysqld]段上加log-bin-trust-function-creators=1

1.3、两者区别

3.1.1 返回值上的不同

存储函数将向调用者返回一个且仅有一个结果值,使用return关键字。

存储过程将返回一个或多个结果集(函数做不到这一点),或者只是来实现某种效果或动作而无需返回结果。

3.1.2 调用方式的不同

存储函数嵌入在sql中使用的,可以在select中调用,就像内置函数一样,比如cos()、sin()。

存储过程通过 CALL 语句调用。

3.1.3 参数的不同

存储函数的参数类型类似于IN参数

存储过程的参数类型有三种:IN参数、OUT参数、INOUT参数

in:数据只是从外部传入内部使用(值传递),可以是数值也可以是变量

out:只允许过程内部使用(不用外部数据),给外部使用的(引用传递:外部的数据会被先清空才会进入到内部),只能是变量

inout:外部可以在内部使用,内部修改的也可以给外部使用,典型的引用 传递,只能传递变量。

3.1.4 内容

存储过程是用户定义的一系列sql语句的集合,设计特定表或其它对象的任务,用户可以调用存储过程。

而函数通常是数据库已定义的方法,它接收参数并返回某种类型的值并且不涉及特定用户表。

存储过程的功能更加强大,包括能够执行对表的操作(比如创建表,删除表等)和事务 操作,这些功能是存储函数不具备的。

过程和函数语法基本一致

2、未定义

3、语法

-- 官方参考网址
https://dev.mysql.com/doc/refman/5.6/en/sql-statements.html
https://dev.mysql.com/doc/refman/5.6/en/sql-compound-statements.html

3.0 语法结构

-- [DEFINER = user]:DEFINER用于指明存储过程是由哪个用户定义的,默认存储过程的定义者是存储过程,跟存储过程的使用权限无关
-- PROCEDURE:/prəˈsiːdʒə(r)/
-- proc_parameter参数部分:type类型可以是MySQL支持的所有类型,可以如下书写:[ IN | OUT | INOUT ] param_name type
CREATE [DEFINER = user] PROCEDURE sp_name ([proc_parameter[,...]])
    [characteristic ...] routine_body  -- routine_body(程序体)部分,可以书写合法的SQL语句 BEGIN ... END

简单演示:

-- 声明结束符。因为MySQL默认使用‘;’作为结束符,而在存储过程中,会使用‘;’作为一段语句的结束,导致‘;’使用冲突
CREATE PROCEDURE hello_procedure(in age int)
BEGIN
  SELECT 'hello procedure';
  SELECT age;
END
​
call hello_procedure(); -- 调用存储过程

3.1 变量及赋值

类比一下java中的局部变量和成员变量的声明和使用
局部变量:

用户自定义,在begin/end块中有效。DECLARE必须紧跟在BEGIN之后声明

语法:
声明变量:declare var_name type [default var_value];
举例:(1)declare nickname varchar(32);     (2)declare emp_no int default 0;  -- /dɪˈkleə(r)/
-- set赋值
create procedure sp_var01()
begin
  declare nickname varchar(32) default 'unkown';  -- default赋默认值
  set nickname = 'ZS';
  -- set nickname := 'SF';
  select nickname;
end
-- into赋值
create procedure sp_var_into()
begin
  declare emp_name varchar(32) default 'unkown' ;
  declare emp_no int default 0;
  select e.empno,e.ename into emp_no,emp_name from emp e where e.empno = 7839;
  select emp_no,emp_name;
end
用户变量:

用户自定义,当前会话(连接)有效。类比java的成员变量

语法: set @var_name = 'ZS' (不需要提前声明,使用即声明)
-- 赋值
create procedure sp_var02()
begin
  set @nickname = 'ZS';
  -- set nickname := 'SF';
end
​
call sp_var02()
select @nickname  --可以看到结果
会话变量:

由系统提供,当前会话(连接)有效

语法:
@@session.var_name
show session variables; -- 查看会话变量
select @@session.unique_checks; -- 查看某会话变量
set @@session.unique_checks = 0; --修改会话变量
全局变量:

由系统提供,整个mysql服务器有效

语法:
@@global.var_name

举例

-- 查看全局变量中变量名有char的记录
show global variables like '%char%'; 
​
-- 查看全局变量character_set_client的值
select @@global.character_set_client; 

3.2 入参出参

-- 语法
in | out | inout param_name type

举例

-- IN类型,传入
create procedure sp_param01(in age int)
begin
  set @user_age = age;
end
​
-- 测试
call sp_param01(10) $$
select @user_age$$
-- OUT类型,输出
-- 需求:输出传入的地址字符串对应的部门编号。
-- OUT类型,输出
-- 需求:输出传入的地址字符串对应的部门编号。
create procedure sp_param02(in loc varchar(64),out dept_no int(11))
begin
  select d.deptno into dept_no from dept d where d.loc = loc;
end
​
-- 测试
call sp_param01('DALLAS');
-- INOUT类型,传入并输出
create procedure sp_param03(inout name varchar(64))
begin
 set name = concat('hello' ,name);
 select name;
end
​
-- 测试
call sp_param03('小明');

3.3 流程控制-判断

官网说明
https://dev.mysql.com/doc/refman/5.6/en/flow-control-statements.html
if
-- 语法
IF search_condition THEN statement_list
    [ELSEIF search_condition THEN statement_list] ...
    [ELSE statement_list]
END IF

举例:

-- 前置知识点:timestampdiff(unit,exp1,exp2) 取差值exp2-exp1差值,单位是unit
select timestampdiff(year,e.hiredate,now()) from emp e where e.empno = '7499';
-- 需求:入职年限<=38是新手 >38并且<=40老员工 >40元老
delimiter $$
create procedure sp_hire_if()
begin
  declare result varchar(32);
  if timestampdiff(year,'2001-01-01',now()) > 40 
    then set result = '元老';
  elseif timestampdiff(year,'2001-01-01',now()) > 38
    then set result = '老员工';
  else 
    set result = '新手';
  end if;
  select result;
end$$
delimiter ;
case

此语法是不仅可以用在存储过程,查询语句也可以用!

-- 语法一(类比java的switch):
CASE case_value
    WHEN when_value THEN statement_list
    [WHEN when_value THEN statement_list] ...
    [ELSE statement_list]
END CASE
-- 语法二:
CASE
    WHEN search_condition THEN statement_list
    [WHEN search_condition THEN statement_list] ...
    [ELSE statement_list]
END CASE

举例:

-- 需求:入职年限年龄<=38是新手 >38并 <=40老员工 >40元老
delimiter $$
create procedure sp_hire_case()
begin
  declare result varchar(32);
  declare message varchar(64);
  case
      when timestampdiff(year,'2001-01-01',now()) > 40 
    then 
      set result = '元老';
      set message = '老爷爷';
    when timestampdiff(year,'2001-01-01',now()) > 38
    then 
      set result = '老员工';
      set message = '油腻中年人';
    else 
      set result = '新手';
      set message = '萌新';
  end case;
  select result, message;
end$$
delimiter ;

3.4 流程控制-循环

loop(死循环)

loop是死循环,需要手动退出循环,使用leave(退出循环)。可以把leave看成我们java中的break

-- 语法 luːp
[begin_label:] LOOP
    ...SQL等逻辑
END LOOP [end_label]
--需求:循环打印1到10
-- leave控制循环的退出
delimiter $$
create procedure sp_flow_loop()
begin
  declare c_index int default 1;
  declare result_str  varchar(256) default '1';
  
  cnt:loop
    if c_index >= 10 then leave cnt;
    end if;
​
    set c_index = c_index + 1;
    set result_str = concat(result_str,',',c_index);
  end loop cnt;
  
  select result_str;
end$$
​
-- iterate + leave控制循环
delimiter $$
create procedure sp_flow_loop02()
begin
  declare c_index int default 1;
  declare result_str  varchar(256) default '1';
  cnt:loop
​
    set c_index = c_index + 1;
    set result_str = concat(result_str,',',c_index);
    if c_index < 10 then 
      iterate cnt; 
    end if;
    -- 下面这句话能否执行到?什么时候执行到? 当c_index < 10为false时执行
    leave cnt;
    
  end loop cnt;
  select result_str;
  
end$$
repeat(do...while)

/rɪˈpiːt/

[begin_label:] REPEAT
    ...SQL等逻辑
UNTIL search_condition  -- 直到满足条件,退出循环
END REPEAT [end_label]
-- 需求:循环打印1到10
delimiter $$
create procedure sp_flow_repeat()
begin
  declare c_index int default 1;
  -- 收集结果字符串
  declare result_str varchar(256) default '1';
  count_lab:repeat
    set c_index = c_index + 1;
    set result_str = concat(result_str,',',c_index);
    until c_index >= 10
  end repeat count_lab;
  select result_str;
end$$
while
[begin_label:] WHILE search_condition DO
    ...SQL等逻辑
END WHILE [end_label]
-- 需求:循环打印1到10
delimiter $$
create procedure sp_flow_while()
begin
  declare c_index int default 1;
  -- 收集结果字符串
  declare result_str varchar(256) default '1';
  while c_index < 10 do
    set c_index = c_index + 1;
    set result_str = concat(result_str,',',c_index);
  end while;
  select result_str;
end$$

3.5 流程控制-退出、继续循环

leave
类比java的breake
-- 退出 LEAVE can be used within BEGIN ... END or loop constructs (LOOP, REPEAT, WHILE).
LEAVE label
iterate
类比java的continue
-- 开启下一次循环 ITERATE can appear only within LOOP, REPEAT, and WHILE statements
ITERATE label

3.6 游标

用游标得到某一个结果集,逐行处理数据。

语法

在语法中,变量声明、游标声明、handler声明是必须按照先后顺序书写的,否则创建存储过程出错。

-- 声明语法  /ˈkɜːsə(r)/
DECLARE cursor_name CURSOR FOR select_statement
-- 打开语法
OPEN cursor_name
-- 取值语法
FETCH cursor_name INTO var_name [, var_name] ...
-- 关闭语法
CLOSE cursor_name

游标示例

CREATE PROCEDURE cursor_example_multi()
BEGIN
-- 定义游标
DECLARE cur1 CURSOR FOR SELECT id, name FROM users WHERE age > 30;
-- 定义变量
DECLARE done INT DEFAULT FALSE;
DECLARE id_value INT;
DECLARE name_value VARCHAR(50);
​
-- 打开游标
OPEN cur1;
​
-- 处理游标结果集  /fetʃ/
read_loop_1: LOOP
FETCH cur1 INTO id_value, name_value;
IF done THEN
LEAVE read_loop_1;
END IF;
   -- 处理当前行数据
   SELECT CONCAT('cur1:', id_value, ':', name_value);
END LOOP;
​
-- 关闭游标1
CLOSE cur1;
END

游标 + 句柄 示例

游标的句柄(handler)不是必须的,可以根据需要选择是否使用它。如果在游标声明时没有定义句柄,则MySQL会自动为该游标生成一个隐含的句柄,可以使用FETCH语句来遍历结果集。

-- 需求:按照部门名称查询员工,通过select查看员工的编号、姓名、薪资。(注意,此处仅仅演示游标用法)
create procedure sp_create_table02(in dept_name varchar(32))
begin
  -- 定义局部变量
  declare e_no int;
  declare e_name varchar(32);
  declare e_sal decimal(7,2);
  -- 句柄值
  declare lp_flag boolean default true;
  
  -- 声明游标
  declare emp_cursor cursor for 
    select e.empno,e.ename,e.sal
    from emp e,dept d
    where e.deptno = d.deptno and d.dname = dept_name;
    
  -- handler,定义句柄,当数据未发现时将标记位设置为false
  declare continue handler for NOT FOUND set lp_flag = false;
    
  -- 打开游标
  open emp_cursor;
  
  -- 循环获取游标值
  emp_loop:loop
    -- e_no,e_name,e_sal 分别对应上 empno,ename,sal
    fetch emp_cursor into e_no,e_name,e_sal;
    -- 判断标志位,true表示有数据,false表示没有数据,结束循环。
    if lp_flag then
      select e_no,e_name,e_sal;
    else
      leave emp_loop;
    end if;
  end loop emp_loop;
  
  -- 关闭游标
  close emp_cursor;
end
​
call sp_create_table02('RESEARCH');

3.7 存储过程中的handler

DECLARE handler_action HANDLER
    FOR condition_value [, condition_value] ...
    statement
​
handler_action: {
    CONTINUE
  | EXIT
  | UNDO
}
​
condition_value: {
    mysql_error_code
  | SQLSTATE [VALUE] sqlstate_value
  | condition_name
  | SQLWARNING
  | NOT FOUND
  | SQLEXCEPTION
}
​
​
CONTINUE: Execution of the current program continues.
EXIT: Execution terminates for the BEGIN ... END compound statement in which the handler is declared. This is true even if the condition occurs in an inner block.
​
​
SQLWARNING: Shorthand for the class of SQLSTATE values that begin with '01'.
NOT FOUND: Shorthand for the class of SQLSTATE values that begin with '02'.
SQLEXCEPTION: Shorthand for the class of SQLSTATE values that do not begin with '00', '01', or '02'.
-- 各种写法:
DECLARE exit HANDLER FOR SQLSTATE '42S01' set @res_table = 'EXISTS';
DECLARE continue HANDLER FOR 1050 set @res_table = 'EXISTS';
DECLARE continue HANDLER FOR not found set @res_table = 'EXISTS';

4、练习

——大家注意,存储过程的业务过程在java代码中一般也可以实现,我们下面的需求是为了练习存储过程

4.1 利用存储过程更新数据

为某部门(需指定)的人员涨薪100;如果是公司总裁,则不涨薪。
delimiter //
create procedure high_sal(in dept_name varchar(32))
begin
  declare e_no int;
  declare e_name varchar(32);
  declare e_sal decimal(7,2);
  
  declare lp_flag boolean default true;
  
  declare emp_cursor cursor for 
    select e.empno,e.ename,e.sal
    from emp e,dept d
    where e.deptno = d.deptno and d.dname = dept_name;
    
  -- handler 句柄
  declare continue handler for NOT FOUND set lp_flag = false;
    
  open emp_cursor;
  
  emp_loop:loop
    fetch emp_cursor into e_no,e_name,e_sal;
    
    if lp_flag then
      if e_name = 'king' then 
        iterate emp_loop;
      else 
        update emp e set e.sal = e.sal + 100 where e.empno = e_no;
      end if;
    else
      leave emp_loop;
    end if;
    
  end loop emp_loop;
  set @end_falg = 'exit_flag';
  close emp_cursor;
end 
​
call high_sal('ACCOUNTING');
​

4.2 循环创建表

创建下个月的每天对应的表comp_2020_04_01、comp_2020_04_02、...
(模拟)需求描述:
我们需要用某个表记录很多数据,比如记录某某用户的搜索、购买行为(注意,此处是假设用数据库保存),当每天记录较多时,如果把所有数据都记录到一张表中太庞大,需要分表,我们的要求是,每天一张表,存当天的统计数据,就要求提前生产这些表——每月月底创建下一个月每天的表!
-- 知识点 预处理 prepare语句from后使用局部变量会报错 
-- https://dev.mysql.com/doc/refman/5.6/en/sql-prepared-statements.html
PREPARE stmt_name FROM preparable_stmt
EXECUTE stmt_name [USING @var_name [, @var_name] ...]
{DEALLOCATE | DROP} PREPARE stmt_name
-- 知识点 时间的处理
-- EXTRACT(unit FROM date)截取时间的指定位置值
-- DATE_ADD(date,INTERVAL expr unit)  日期运算
-- LAST_DAY(date)  获取日期的最后一天
-- YEAR(date) 返回日期中的年
-- MONTH(date) 返回日期的月
-- DAYOFMONTH(date) 返回日
-- 思路:循环构建表名 comp_2020_05_01 到 comp_2020_05_31;并执行create语句。
​
create procedure sp_create_table()
begin
  declare next_year int;
  declare next_month int;
  declare next_month_day int;
    
  declare next_month_str char(2);
  declare next_month_day_str char(2);
  
  -- 处理每天的表名
  declare table_name_str char(10);
  
  declare t_index int default 1;
  -- declare create_table_sql varchar(200);
  
  -- 获取下个月的年份
  set next_year = year(date_add(now(),INTERVAL 1 month));
  -- 获取下个月是几月 
  set next_month = month(date_add(now(),INTERVAL 1 month));
  -- 下个月最后一天是几号
  set next_month_day = dayofmonth(LAST_DAY(date_add(now(),INTERVAL 1 month)));
  
  if next_month < 10
    then set next_month_str = concat('0',next_month);
  else
    set next_month_str = concat('',next_month);
  end if;
  
  
  while t_index <= next_month_day do
    
    if (t_index < 10)
      then set next_month_day_str = concat('0',t_index);
    else
      set next_month_day_str = concat('',t_index);
    end if;
    
    -- 2020_05_01
    set table_name_str = concat(next_year,'_',next_month_str,'_',next_month_day_str);
    -- 拼接create sql语句
    set @create_table_sql = concat(
          'create table comp_',
          table_name_str,
          '(`grade` INT(11) NULL,`losal` INT(11) NULL,`hisal` INT(11) NULL) COLLATE=\'utf8_general_ci\' ENGINE=InnoDB');
    -- FROM后面不能使用局部变量!
    prepare create_table_stmt FROM @create_table_sql;
    execute create_table_stmt;
    DEALLOCATE prepare create_table_stmt;
    
    set t_index = t_index + 1;
    
  end while;  
end
​
call sp_create_table()

4.3 其他场景:

“为用户添加购物积分,并更新到用户的总积分表中”等需要对多张表进行CRUD操作的业务。
而且内部可以使用事务命令。

5、其他知识点

5.1 characteristic

characteristic:
    COMMENT 'string'
  | LANGUAGE SQL
  | [NOT] DETERMINISTIC
  | { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
  | SQL SECURITY { DEFINER | INVOKER }

其中,SQL SECURITY的含义如下:

  MySQL存储过程是通过指定SQL SECURITY子句指定执行存储过程的实际用户;
  如果SQL SECURITY子句指定为DEFINER(定义者),存储过程将使用存储过程的DEFINER执行存储过程,验证调用存储
  过程的用户是否具有存储过程的execute权限和DEFINER用户是否具有存储过程引用的相关对象(存储过程中的表等对象)的权限;
  如果SQL SECURITY子句指定为INVOKER(调用者),那么MySQL将使用当前调用存储过程的用户执行此过程,并验证用户是否具有存储过程的execute权限和存储过程引用的相关对象的权限;
  如果不显示的指定SQL SECURITY子句,MySQL默认将以DEFINER执行存储过程。

5.2 死循环处理

-- 如有死循环处理,可以通过下面的命令查看并结束
show processlist;
kill id;

5.3 可以在select语句中写case

https://dev.mysql.com/doc/refman/5.6/en/control-flow-functions.html
select 
  case
    when timestampdiff(year,e.hiredate,now()) <= 38 then '新手'
    when timestampdiff(year,e.hiredate,now()) <= 40 then '老员工'
    else '元老'
  end hir_loc,
  e.*
from emp e;

5.4 临时表

create temporary table 表名(
  字段名 类型 [约束],
  name varchar(20) 
)Engine=InnoDB default charset utf8;
​
-- 需求:按照部门名称查询员工,通过select查看员工的编号、姓名、薪资。(注意,此处仅仅演示游标用法)
delimiter $$
create procedure sp_create_table02(in dept_name varchar(32))
begin
  declare emp_no int;
  declare emp_name varchar(32);
  declare emp_sal decimal(7,2);
  declare exit_flag int default 0;
  
  declare emp_cursor cursor for
    select e.empno,e.ename,e.sal
    from emp e inner join dept d on e.deptno = d.deptno where d.dname = dept_name;
  
  declare continue handler for not found set exit_flag = 1;
  
  -- 创建临时表收集数据
  CREATE temporary TABLE `temp_table_emp` (
    `empno` INT(11) NOT NULL COMMENT '员工编号',
    `ename` VARCHAR(32) NULL COMMENT '员工姓名' COLLATE 'utf8_general_ci',
    `sal` DECIMAL(7,2) NOT NULL DEFAULT '0.00' COMMENT '薪资',
    PRIMARY KEY (`empno`) USING BTREE
  )
  COLLATE='utf8_general_ci'
  ENGINE=InnoDB;  
  
  open emp_cursor;
  
  c_loop:loop
    fetch emp_cursor into emp_no,emp_name,emp_sal;
    
    
    if exit_flag != 1 then
      insert into temp_table_emp values(emp_no,emp_name,emp_sal); 
    else
      leave c_loop;
    end if;
    
  end loop c_loop;
  
  select * from temp_table_emp;
  
  select @sex_res; -- 仅仅是看一下会不会执行到
  close emp_cursor;
  
end$$
​
call sp_create_table02('RESEARCH');

你可能感兴趣的:(数据库,mysql,数据库)