Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL存储过程和触发器

MySQL中的存储过程和触发器详解

作者:BUG召唤师

本文系统介绍了MySQL存储过程的创建与使用,包括变量定义、条件判断、循环控制、游标操作等核心知识,详细讲解了存储过程的优缺点分析,以及存储函数与触发器的区别与应用场景,通过具体示例演示了如何实现参数传递、条件处理、日志记录等功能

本文系统介绍了MySQL存储过程的创建与使用,包括变量定义、条件判断、循环控制、游标操作等核心知识。

详细讲解了存储过程的优缺点分析,以及存储函数与触发器的区别与应用场景。

通过具体示例演示了如何实现参数传递、条件处理、日志记录等功能,并深入解析了行级触发器的实现机制。

文章还总结了存储过程在实际开发中的常见问题与解决方案,为数据库编程提供了全面指导。

一、存储过程

1. 存储过程介绍

MySQL 的存储过程是为了完成特定功能的一组 sql 语句集,用户通过存储过程的名字和参数进行调用,类似于 java 中的方法。

2. 优缺点分析

存储过程的优点:

存储过程的缺点:

3. 存储过程的语法

delimiter 关键字用于定义语句的结束标识符,例如 "delimiter //" 就是将结束标识符定义为 "//";

-- 修改 sql 语句结束标识符为 //
delimiter //

-- 创建存储过程
create procedure 存储过程名(参数, 参数...)
begin
    select 列名, 列名... from exam;
end//

-- 修改 sql 语句结束标识符为 ;
delimiter;

-- 查看 topoc01 中的所有存储过程
select * from information_schema.routines where routine_schema = 数据库名;

-- 查看存储过程的定义
show create procedure 存储过程名;

-- 调用存储过程
call 存储过程名(参数, 参数...);

-- 删除存储过程
drop procedure if exists 存储过程名;

注意:

二、变量

1. 系统变量

系统变量分为全局变量和会话变量;

查询系统变量:

-- 查看系统变量
show global variables;

-- 查看会话变量
show session variables;

-- 搭配模糊查询 - 全局变量
show global variables like 'xxx%';

-- 搭配模糊查询 - 会话变量
show session variables like '%xxx%';

-- 使用 select 进行精确查询 - 全局变量
select @@global.系统变量名;

-- 使用 select 进行精确查询 - 会话变量
select @@session.系统变量名;

注意:

设置系统变量:

-- 设置系统变量 - 全局变量
set global 系统变量名 = 值;

-- 设置系统变量 - 会话变量
set session 系统变量名 = 值;

-- 设置系统变量 - 全局变量
set @@global.全局变量名 = 值;

-- 设置系统变量 - 会话变量
set @@session.会话变量名 = 值;

注意:

2. 用户自定义变量

用户自定义变量是 sql 会话中定义的变量,不用提前申明,作用域为当前会话;

-- 申明 + 赋值
set @var_name := 表达式;

select @var_name :=  表达式;

select 列名 into @var_name from 表名;

-- 使用自定义变量
select @var_name;

3. 局部变量

局部变量只在存储过程,函数或者触发器的范围内有效;

需要使用关键字 declare 申明,作用域为 begin 和 end 中间的代码块内;

-- 局部变量申明的语法
declare 局部变量名 类型 default 默认值;

-- 局部变量在存储过程中的应用

delimiter//

-- 定义存储过程
create procedure p1()
begin 
    -- 定义局部变量
    declare stu_count int default 0;
    -- 给局部变量赋值
    select count(*) into stu_count from student;
    -- 使用局部变量
    select stu_count;
end//

delimiter ;

三、SQL 编程

1. 条件判断语句

-- 条件判断语句
if 条件1 then
    具体执行逻辑;
elseif 条件2 then
    具体执行逻辑;
...
else
    具体执行逻辑;
end if;
-- 练习
-- 分数>=90分等级为优秀
-- 分数>=80且分数<90分等级为良好
-- 分数>=60分且分数<80分等级为及格
-- 分数<60分等级为不及格

delimiter //

create procedure p1()
begin
    declare score int default 86;
    declare result varchar(20);

    if score >= 90 then
        set result := '优秀';
    elseif score >=80 then
        set result := '良好';
    elseif score >= 60 then 
        set result := '及格';
    else
        set result := '不及格';
    end if;

    select result;
end//

delimiter ;

call p1();

2. 参数

参数类型有三种:in(输入型),out(输出型),inout(输入输出型);

-- 练习
-- 传⼊⼀个分数的值,判定当前分数对应的等级 
-- 分数>=90分等级为优秀
-- 分数>=80且分数<90分等级为良好
-- 分数>=60分且分数<80分等级为及格
-- 分数<60分等级为不及格

delimiter //

create procedure p2(in score int, out result varchar(20))
begin
    if score >= 90 then
        set result := '优秀';
    elseif score >=80 then
        set result := '良好';
    elseif score >= 60 then 
        set result := '及格';
    else
        set result := '不及格';
    end if;
end//

delimiter ;

-- 调用
call p2(86, @result);
select @result;

-- 传⼊⼀个分数的值,在传⼊分数的基础上加10分,然后返回
delimiter //

create procedure p3(inout score int)
begin
    set score = score + 10;
end//

delimiter ;

set @score := 50;
call p3(@score);
select @score;

3. case 判断语句

-- 语法⼀
 
CASE case_value
 WHEN when_value THEN statement_list
 [WHEN when_value THEN statement_list] ...
 [ELSE statement_list]
END CASE

 
-- 语法⼆
 
statement_list 
(如果存在
CASE
 WHEN search_condition THEN statement_list
 [WHEN search_condition THEN statement_list] ...
 [ELSE statement_list]
END CASE
-- ⽰例⼀:传⼊⼀个状态码,输出该状态码表⽰的含义
-- 0:成功
-- 10001:⽤⼾名或密码错误
-- 10002:您没有对应的权限,请联系管理员
-- 20001:你传⼊的参数有误
-- 20002:没有找到相应的结果

delimiter //

create procedure p4(in code int, out result varchar(20))
begin
    case code
        when 0 then 
            set result := '成功';
        when 10001 then 
            set result := '用户名或密码错误';
        when 10002 then 
            set result := '您没有对应的权限,请联系管理员';
        when 20001 then 
            set result := '你传⼊的参数有误';
        when 20002 then 
            set result := '没有找到相应的结果';
        else 
            set result = '服务器错误,请联系管理员';
    end case;
end//

delimiter ;

call p4(10001, @result);
select @result;


-- ⽰例⼆:根据传⼊的⽉份,输出该⽉份属于哪个季度
-- 1~3⽉为第⼀季度
-- 4~6⽉为第二季度
-- 7~9⽉为第三季度
-- 1~12⽉为第四季度

delimiter //

create procedure p5(in month int, out result varchar(20))
begin 
    case
        when month >= 1 and month <= 3 then 
            set result := '第⼀季度';
        when month >= 4 and month <= 6 then
            set result := '第二季度';
        when month >= 7 and month <= 9 then
            set result := '第三季度';
        when month >= 10 and month <= 12 then 
            set result := '第四季度';
        else 
            set result := '非法参数';
    end case;
end//

delimiter ;

call p5(6, @result);
select @result;

4. 循环

while 循环:

WHILE search_condition Do
    statement_list
END WHILE;
-- 传⼊⼀个数n,计算从1累加到n的值
delimiter //

create procedure p6(in n int, out result int)
begin 
  declare num int default 1;
  set result := 0;
  while num <= n do 
   set result := result + num;
   set num := num + 1;
  end while;
end//

delimiter ;

call p6(3, @result);
select @result;

repeat 循环:

REPEAT
 statement_list
UNTIL search_condition END REPEAT;
delimiter //

create procedure p7(in n int, out sum int)
begin
  set sum := 0;
  repeat 
    set sum := sum + n;
    set n := n - 1;
  until n = 0 end repeat;
end//

delimiter ;

call p7(5, @sum);
select @sum;

loop 循环:

[begin_label:] LOOP
    statement_list
END LOOP [end_label]
-- 传⼊⼀个数n,累加从1累加到n之间偶数的值
delimiter //

create procedure p8(in n int)
begin
  declare sum int default 0; 
  sum_label: LOOP
    IF n <= 0 THEN
      LEAVE sum_label; 
    END IF; 
    set sum := sum + n;
    set n := n - 1;
  END LOOP sum_label;
  
  select sum;

end//

delimiter ;

call p8(100);

-- 传⼊⼀个数n,累加从1累加到n之间偶数的值
delimiter //

create procedure p9(in n int)
BEGIN
  declare sum int default 0;
  sum_label: LOOP
    IF n <= 0 THEN
      LEAVE sum_label; 
    END IF; 
    IF n % 2 != 0 then 
      set n := n - 1;
      ITERATE sum_label;
    END IF;
    set sum := sum + n;
    set n := n - 1;
  END LOOP sum_label;
  
  select sum;

END//

delimiter ;

call p9(10);

5. 游标和条件处理程序

游标是一种数据库对象,允许在存储过程和函数中对查询到的结果进行逐行检索。

-- 声明游标
declare 游标名 cursor for 查询语句;

-- 打开游标
open 游标名;

-- 获取游标记录
fetch 游标名 into 变量名, 变量名...;

-- 关闭游标
close 游标名;
-- 声明条件处理程序
declare handler_action handler for condition_value, condition_value...
    statement;

handler_action:{
    continue, - 继续执行当前程序
    exit - 退出当前程序
}

condition_value:{
     mysql_error_code  -- MYSQL错误码
        
    | SQLSTATE [VALUE] sqlstate_value  -- 状态码
 
    | SQLWARNING  -- 所有以 01开头的 SQLSTATE 代码
 
    | NOT FOUND   -- 所有以 02开头的 SQLSTATE 代码
 
    | SQLEXCEPTION -- 所有没有被 SQLWARNING 或 NOT FOUND 捕获的 SQLSTATE 代码
}
-- 示例:传入班级编号,查询学生表中属于该班级的学生信息
-- 并将符合条件的学生信息写入到⼀张新表中

-- 新表及字段 t_student_class(id,student_name,class_name)

-- 实现逻辑
-- 定义变量⽤于接收查询结果集中每⼀⾏中列的值
-- 声明游标,⽤于接收查询结果集
-- 创建新表
-- 开启游标
-- 从游标中获取结果集中的记录
-- 向新表中写⼊数据
-- 关闭游标

delimiter //

create procedure p10(in class_id int)
begin
    -- 定义变量⽤于接收查询结果集中每⼀⾏中列的值
    declare student_name varchar(20);
    declare class_name varchar(20);
    -- 声明游标,⽤于接收查询结果集
    declare s_cursor cursor for 
        select student.name, class.name from student, class 
        where student.class_id = class.id and class.id = class_id;
    -- 创建新表
    create table if not exists t_student_class (
        id int primary key auto_increment,
        student_name varchar(20) not null, 
        class_name varchar(20) not null
    );
    -- 开启游标
    open s_cursor;
    -- 从游标中获取结果集中的记录
    while true do
        fetch s_cursor into student_name, class_name;
         -- 向新表中写⼊数据
        insert into t_student_class values (student_name, class_name);
    -- 关闭游标
    close s_cursor;
end//

delimiter ;

注意:

delimiter //

create procedure p10(in class_id int)
begin
    -- 定义变量⽤于接收查询结果集中每⼀⾏中列的值
    declare student_name varchar(20);
    declare class_name varchar(20);
    -- 定义循环退出条件
    declare is_done bool default false;
    -- 声明游标,⽤于接收查询结果集
    declare s_cursor cursor for 
        select student.name, class.name from student, class 
        where student.class_id = class.id and class.id = class_id;
    -- 声明条件处理程序
    declare continue handler for not found
        set is_done := true;
    -- 创建新表
    create table if not exists t_student_class (
        id int primary key auto_increment,
        student_name varchar(20) not null, 
        class_name varchar(20) not null
    );
    -- 开启游标
    open s_cursor;
    -- 从游标中获取结果集中的记录
    while not is_done do 
        fetch s_cursor into student_name, class_name;
         -- 向新表中写⼊数据
        insert into t_student_class values (null, student_name, class_name);
    end while;
    -- 关闭游标
    close s_cursor;
end//

delimiter ;

注意,上述程序的条件处理程序的问题是:

while 循环没有退出方式,因此 while 循环并不适合这里的需求,而 loop 循环是能够满足需求的方式,因此使用 loop 循环代替 while 循环,将代码更改为:

delimiter //

create procedure p10(in class_id int)
begin
    -- 定义变量⽤于接收查询结果集中每⼀⾏中列的值
    declare student_name varchar(20);
    declare class_name varchar(20);
    -- 定义循环退出条件
    declare is_done bool default false;
    -- 声明游标,⽤于接收查询结果集
    declare s_cursor cursor for 
        select student.name, class.name from student, class 
        where student.class_id = class.id and class.id = class_id;
    -- 声明条件处理程序
    declare continue handler for not found
        set is_done := true;
    -- 创建新表
    create table t_student_class(id,student_name,class_name);
    -- 开启游标
    open s_cursor;
    -- 从游标中获取结果集中的记录
    read_loop: loop
        fetch s_cursor into student_name, class_name;
        if is_done then
            leave read_loop;
        end if;
        insert into t_student_class values (null, student_name, class_name);
    end loop read_loop;
    -- 关闭游标
    close s_cursor;
end//

delimiter ;

6. 存储函数

MySQL 的存储函数是返回值的存储过程,参数只能是 in 类型,因此 in 也可以省略;

存储函数和存储过程的主要区别是:存储函数必须有返回值,存储过程没有返回值,只能用输出型或者输入输出型参数作为输出的结果。

-- 定义存储函数
delimiter //

create function 存储函数名(参数列表) returns 类型 characterristic
begin
    SQL 语句...
    return 返回值;
end//

delimiter ;

characteristic:
    deterministic | not deterministic | no sql | reads sql data | modifies sql data 

-- 使用存储函数
select 存储函数名(参数列表);

示例:传入一个数 n,计算从 1 累加到 n 的值;

delimiter //

create function func1(n int) returns int deterministic
begin

    declare total int default 0;
    while n >= 1 do
        set total := total + n;
        set n := n - 1;
    end while;

    return total;

end//

delimiter ;


select func1(100);

四、触发器

1. 触发器的定义和类型

触发器是一个与表相关联的数据库对象;

在对表进行 insert,update,delete 操作时,触发并执行触发器中定义的 sql 语句;

MySQL 支持 3 中类型的触发器:insert 触发器, update 触发器,delete 触发器;

可以使用 OLD 和 NEW 关键字引用触发器中发生变化的数据被容:

触发器也可以分为行级触发器和语句级触发器:

2. 触发器的语法及实现

-- 定义触发器
delimiter //

create trigger if not exists 触发器名 
    触发时间 触发类型 on 表名 for each row
begin
    触发器逻辑;
end

delimiter ;


trigger time: before | after

trigger event: insert / update / delete

-- 查看触发器
show triggers;

-- 删除触发器
drop trigger if exists 触发器名;

示例:通过触发器记录学生表的变更日志,将变更日志写入到表 student_log 中,包含增加,修改和删除操作。

1. 创建 student_log 表

-- 创建 student_log 表
create table student_log (
    id bigint primary key auto_increment,
    operation_type varchar(10) not null comment '操作类型:insert, update, delete',
    operation_time datetime not null comment '操作时间',
    operation_id bigint not null comment '操作记录 id',
    operation_data varchar(500) not null comment '操作数据'
);

2. 创建 isnert 触发器

-- 创建 insert 触发器
delimiter //

create trigger tgr_student_insert
    after insert on student for each row
begin
    insert into student_log (
        operation_type,
        operation_time,
        operation_id,
        operation_data
    ) values (
        'insert',
        now(),
        new.id,
        concat(new.id, ',' new.name, ',' new.sno, ',' new.age, ',' new.gender, ',' new.enroll_date, ',' new.class_id )
    );
end//

delimiter ;

-- 插入数据记录
insert into student values (null, '曹操', '300001', 28, 1, '2024-9-1', 3);

3. 创建 update 触发器

-- 创建 update 触发器
delimiter //

create trigger tgr_student_update
    after update on student for each row
begin
    insert into student_log (
        operation_type,
        operation_date,
        operation_id,
        operation_data
    ) values (
        'update',
        now(),
        new.id,
        concat(old.id, ', ', old.name, ', ', old.sno, ', ', old.age, ', ', old.gender, ', ', old.enroll_date, ', ', old.class_id, ' | ', 
               new.id, ', ', new.name, ', ', new.sno, ', ', new.age, ', ', new.gender, ', ', new.enroll_date, ', ', new.class_id)
    );
end//

delimiter ;

-- 更新学生表
update student set age = 20 where name = '曹操'; 
   
update student set class_id = 3 where id >= 7;

4. 创建 delete 触发器

-- 创建 delete 触发器
delimiter //

create trigger tgr_student_delete
    after delete on student for each row
begin
    insert into student_log (
        operation_type,
        operation_time,
        operation_id,
        operation_data
    ) values (
        'delete',
        now(),
        old.id,
        concat(old.id, ', ', old.name, ', ',old.sno, ', ',old.age, ', ',old.gender, ', ',old.enroll_date, ', ',old.class_id)
    );
end//

delimiter ;

-- 删除数据记录
delete from student where name = '曹操';

五、总结

1. 存储过程的作用是什么?

存储过程的作用是通过一个 sql 语句集,在数据库层面实现特定的业务逻辑;类似于 java 中的方法。

优点是:

缺点是:

2. 如何创建一个存储过程?

使用 create 关键字:create procedure 存储过程名字(参数列表);

begin 后面编写 sql 语句集,end 结束语句集的编写;

需要注意的点是:要使用 delimiter 来修改 sql 语句终止符,默认终止符是分号,避免在命令行中执行的时候,服务器认为语句中的分号为终止符,导致创建失败;

3. MySQL 中的变量有哪几种?

有三种,分别是:系统变量,用户自定义变量,局部变量;

系统变量又分为全局变量和会话变量,全局变量修改成功后对所有会话都生效,但是如果服务器重启,修改的效果就会失效;会话变量修改后只在当前会话生效,会话关闭后,就会失效;

用户自定义变量:不需要提前声明,用的时候直接写 @变量名 即可;

局部变量用于存储过程,存储函数,触发器中,需要提前使用关键字 declare 声明;

4. 如何定义一个变量?

系统变量是不可以自定义的;

用户自定义变量可以通过 set @变量名定义;

局部变量通过 declare 变量名 变量类型 默认值的方式定义;

5. MySQL中使用变量是否需要提前声明?

如果是用户自定义变量,不需要提前声明;

如果在存储过程,存储函数,触发器中使用局部变量,需要使用 declare 关键字提前申明;

6. MySQL中的参数分为哪几种?

MuSQL 中参数分为 3 种:in - 输入型参数, out - 输出型参数,inout - 输入输出型参数;

存储过程,存储函数都可以使用输入型参数,但是存储函数只能使用输入型参数,因此 in 可以省略;存储过程中可以使用输出型参数或者输入输出型参数代替返回值;

7. 用过游标吗?游标的作用是什么?

用过游标,游标的作用是遍历使用查询语句获得的结果集;游标默认指向的是查询结果集的前一行,使用 fetch 游标名 into 变量名的方式将结果集中的列保存到变量中,fetch 操作后,游标会向结果集的下一行移动;

因此游标通常会和循环搭配使用,由于游标走到结果集到最后一行会发生越界,sql 会报错,通常回合条件处理程序配合使用;

8. 了解条件处理程序吗?介绍⼀下如何使用?

条件处理程序需要注意两点:

第一点是条件,条件可以是 MySQL 的错误码,或者 SQL 语言的状态码;

第二点是处理程序:满足条件后才会执行处理程序,处理程序的逻辑有两种,continue 和 exit,满足条件时,continue 会继续让程序执行,增强程序处理问题的能力;exit 则会退出程序;

局部变量和游标必须要在条件处理程序之前先定义; 

定义条件处理程序的方式是 declare handler_action hander condition_value,...;

9. 存储函数与存储过程的区别是什么?

存储函数必须要求有返回值,存储过程不需要有返回值;

存储函数的参数列表只能有 in 类型的参数,因此 in 也可以省略;

存储函数声明的时候必须要写 returns,后面跟上返回值类型,在 MySQL 8 中,开启 binlog 的前提条件下,返回值类型后面必须写 deterministic,或者 not deterministic,不写会报错;

10. 如何查看数据库中创建的存储过程?

select * from information_schema.routines where routine_schema = '数据库名';

也可以:

show create procedure 存储过程名;

11. 什么是触发器?

触发器是一个与表相关联的数据库对象;

在进行 insert, update, delete 操作时,触发执行触发器定义的 sql 语句;

12. MySQL中触发器分为几种类型?

MySQL 中触发器分为三种类型:insert 触发器,update 触发器,delete 触发器;

触发器的触发时间分为 before 和 after;

13. 行级触发器与语句级触器的区别是什么?

14. 说⼀下了解的触发器使用场景都有哪些?

根据触发时间分为 before 和 after;

15. 如果对⼀表中的数据进行更新,要在日志表中记录该条记录更新前与更新后的值,如何实现?

首先要创建日志表;

第二要定义一个 update 触发器,触发器时间设置为 after;

触发器的语句集中写 insert 语句,记录发生变化时的操作类型,操作时间,操作数据记录的 id,还有针对列的修改;、

16. 如何查看数据库中创建的触发器?

show triggers; 

可以查看数据库中定义的所有触发器;

也可以

show create trigger 触发器名;

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文