MySQL 存储过程

需求

有时候我们需要写一些逻辑代码来处理MySQL中的数据,尤其是在版本升级过程中,历史数据需要处理。这时候我们希望通过sql脚本就能完成数据的处理。

本例将介绍一个简单的业务逻辑

准备工作

创建3张表,一张学生表,一张班级表,一张学生与班级绑定关系表。 Version-1: 班级表中没有学生数量,导致每次查询班级学生数量都需要通过关联表统计。 Version-2: 新版本决定将学生数量统计出来,并存放到班级表中,方便查询。

这只是一个假想的需求,不要较真,只是演示如何通过sql实现。

Version-1 版本数据库

CREATE TABLE flyway.student (
    id INT UNSIGNED auto_increment NOT NULL,
    name varchar(16) NULL,
    age TINYINT NULL,
    CONSTRAINT student_PK PRIMARY KEY (id)
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8
COLLATE=utf8_general_ci;

CREATE TABLE `class` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `class_name` varchar(16) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

CREATE TABLE flyway.student_with_class (
    id INT UNSIGNED auto_increment NOT NULL,
    student_id INT UNSIGNED NOT NULL,
    class_id INT UNSIGNED NOT NULL,
    CONSTRAINT student_with_class_PK PRIMARY KEY (id)
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8
COLLATE=utf8_general_ci;

INSERT INTO flyway.student
(name, age)
VALUES('小明', 7),('小王', 7),('小红', 7),('小花', 7);

INSERT INTO flyway.class
(class_name)
VALUES('一年级1班'),('一年级2班'),('一年级3班');

INSERT INTO flyway.student_with_class
(student_id, class_id)
VALUES(1, 1),(2, 2),(3, 3),(4, 1);

Version-2 升级脚本

-- 班级表新增学生数量字段
ALTER TABLE flyway.class ADD student_count INT UNSIGNED DEFAULT 0 NULL;

编写sql脚本处理数据

思路:查询出所有班级,循环遍历每个班级,并对每个班级进行数量统计,然后更新这个班级的学生数量。

-- 删除存储过程(如果存在)
drop procedure if exists countStudent;

-- 定义语句结束符号,默认是逗号,每写一个语句以分号结尾,MySQL会立即执行,在这里不能这样做。
DELIMITER $$

create procedure countStudent()
begin
    --  这里声明一个 int 类型的本地变量,用于后面存放值
    declare studentCount int DEFAULT 0;
    declare classId int;
    declare className varchar(16);
    --  这里定义一个结束标识变量,当所有班级都更新后,就可以结束任务了。
    declare done TINYINT DEFAULT 0;

    --  将查询出来的班级列表定义,这里用 cursor 来表明
    declare classes cursor for select id, class_name from class;
    declare CONTINUE HANDLER FOR NOT FOUND SET done = 1;

    --  开启 cursor
    open classes;
        --  这里开始循环处理每个班级
        class: LOOP
            --  这里是获取 classes 中的一行数据,并将其复制给定义好的变量上。注意保持和查询顺序一致
            fetch from classes into classId, className;
            --  如果处理完了,就停止循环
            IF done THEN LEAVE class; END IF;
            -- 这里开启事务,视情况而定
            start transaction;
                select count(*) into studentCount from student_with_class swc where swc.class_id = classId;
                update class set student_count = studentCount where id = classId and class_name = className;
            commit;
        end loop;
    close classes;

end $$

-- 恢复结束符号为分号
delimiter ;

-- 调用存储过程,也即执行我们的业务逻辑
call countStudent();

-- 如果是一次性的就删除。
drop procedure countStudent;

注意事项:

  1. 一定要使用 DELIMITER $$ 重新声明语句结束符号

  2. declare 变量一定要在 cursor 以及 HANDLER 之前定义。

难点(以前困扰我自己的):

  1. 查询出来的数据用什么东西接收: 通过 declare 声明变量

  2. 如何接收: 通过 into 关键字,将 select 中得字段存储在声明的变量中

  3. 列表数据如何处理: 循环 加上 获取每一行数据 (fetch … into)

其他:

  1. 业务逻辑根据实际情况在循环体中处理,需结合MySQL特有的语法

总之,对我来说难点就是查询出来的数据如何处理,其他业务逻辑都有对应的语法。