|
|
|
@ -777,6 +777,473 @@ mysql> SELECT barcode,goodsname,specification
|
|
|
|
|
- 视图不是越多越好,特别是嵌套的视图(就是在视图的基础上创建视图),我不建议你使用,因为逻辑复杂,可读性不好,容易变成系统的潜在隐患。
|
|
|
|
|
|
|
|
|
|
### 2.2 存储过程:如何提高程序的性能和安全性?
|
|
|
|
|
- 在我们的超市项目中,每天营业结束后,超市经营者都要计算当日的销量,核算成本和毛利等营业数据,这也就意味着每天都要做重复的数据统计工作。其实,这种数据量大,而
|
|
|
|
|
且计算过程复杂的场景,就非常适合使用存储过程。
|
|
|
|
|
- 简单来说呢,存储过程就是把一系列 SQL 语句预先存储在 MySQL 服务器上,需要执行的时候,客户端只需要向服务器端发出调用存储过程的命令,服务器端就可以把预先存储好
|
|
|
|
|
的这一系列 SQL 语句全部执行。
|
|
|
|
|
- 这样一来,不仅执行效率非常高,而且客户端不需要把所有的 SQL 语句通过网络发给服务器,减少了 SQL 语句暴露在网上的风险,也提高了数据查询的安全性。
|
|
|
|
|
|
|
|
|
|
- 如何创建存储过程?
|
|
|
|
|
- 在创建存储过程的时候,我们需要用到关键字 CREATE PROCEDURE
|
|
|
|
|
```sql
|
|
|
|
|
CREATE PROCEDURE 存储过程名 ([ IN | OUT | INOUT] 参数名称 类型)程序体
|
|
|
|
|
```
|
|
|
|
|
- 假设在日结计算中,我们需要统计每天的单品销售,包括销售数量、销售金额、成本、毛利、毛利率等。同时,我们还要把计算出来的结果存入单品统计表中。
|
|
|
|
|
- 销售单明细表(demo.transactiondetails)中包括了每笔销售中的商品编号、销售数量、销售价格和销售金额。
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT *
|
|
|
|
|
-> FROM demo.transactiondetails;
|
|
|
|
|
+---------------+------------+----------+------------+------------+
|
|
|
|
|
| transactionid | itemnumber | quantity | salesprice | salesvalue |
|
|
|
|
|
+---------------+------------+----------+------------+------------+
|
|
|
|
|
| 1 | 1 | 1.000 | 89.00 | 89.00 |
|
|
|
|
|
| 1 | 2 | 2.000 | 5.00 | 10.00 |
|
|
|
|
|
| 2 | 1 | 2.000 | 89.00 | 178.00 |
|
|
|
|
|
| 3 | 2 | 10.000 | 5.00 | 50.00 |
|
|
|
|
|
| 3 | 3 | 3.000 | 15.00 | 45.00 |
|
|
|
|
|
+---------------+------------+----------+------------+------------+
|
|
|
|
|
5 rows in set (0.00 sec)
|
|
|
|
|
```
|
|
|
|
|
- 销售单头表(demo.transactionhead)中包括流水单号、收款机编号、会员编号、操作员编号、交易时间。
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT *
|
|
|
|
|
-> FROM demo.transactionhead;
|
|
|
|
|
+---------------+------------------+-----------+----------+------------+------
|
|
|
|
|
| transactionid | transactionno | cashierid | memberid | operatorid | transdat
|
|
|
|
|
+---------------+------------------+-----------+----------+------------+------
|
|
|
|
|
| 1 | 0120201201000001 | 1 | 1 | 1 | 2020-12-01 00:00:00 |
|
|
|
|
|
| 2 | 0120201201000002 | 1 | NULL | 1 | 2020-12-01 00:00:00 |
|
|
|
|
|
| 3 | 0120201202000001 | 1 | NULL | 1 | 2020-12-02 00:00:00 |
|
|
|
|
|
+---------------+------------------+-----------+----------+------------+------
|
|
|
|
|
3 rows in set (0.00 sec)
|
|
|
|
|
```
|
|
|
|
|
- 商品信息表(demo.goodsmaster)中包括商品编号、商品条码、商品名称、规格、单位、售价和平均进价。
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT *
|
|
|
|
|
-> FROM demo.goodsmaster;
|
|
|
|
|
+------------+---------+-----------+---------------+------+------------+------
|
|
|
|
|
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgim
|
|
|
|
|
+------------+---------+-----------+---------------+------+------------+------
|
|
|
|
|
| 1 | 0001 | 书 | NULL | 本 | 89.00 | 33.50 |
|
|
|
|
|
| 2 | 0002 | 笔 | NULL | 支 | 5.00 | 3.50 |
|
|
|
|
|
| 3 | 0003 | 胶水 | NULL | 瓶 | 15.00 | 11.00 |
|
|
|
|
|
+------------+---------+-----------+---------------+------+------------+------
|
|
|
|
|
3 rows in set (0.00 sec)
|
|
|
|
|
```
|
|
|
|
|
- 存储过程会用刚刚的三个表中的数据进行计算,并且把计算的结果存储到下面的这个单品统计表中。
|
|
|
|
|
```sql
|
|
|
|
|
mysql> DESCRIBE demo.dailystatistics;
|
|
|
|
|
+-------------+---------------+------+-----+---------+----------------+
|
|
|
|
|
| Field | Type | Null | Key | Default | Extra |
|
|
|
|
|
+-------------+---------------+------+-----+---------+----------------+
|
|
|
|
|
| id | int | NO | PRI | NULL | auto_increment |
|
|
|
|
|
| itemnumber | int | YES | MUL | NULL | |
|
|
|
|
|
| quantity | decimal(10,3) | YES | | NULL | |
|
|
|
|
|
| actualvalue | decimal(10,2) | YES | | NULL | |
|
|
|
|
|
| cost | decimal(10,2) | YES | | NULL | |
|
|
|
|
|
| profit | decimal(10,2) | YES | | NULL | |
|
|
|
|
|
| profitratio | decimal(10,4) | YES | | NULL | |
|
|
|
|
|
| salesdate | datetime | YES | MUL | NULL | |
|
|
|
|
|
+-------------+---------------+------+-----+---------+----------------+
|
|
|
|
|
8 rows in set (0.01 sec)
|
|
|
|
|
```
|
|
|
|
|
- 我们现在就来创建一个存储过程,完成单品销售统计的计算。
|
|
|
|
|
- 第一步,我们**把 SQL 语句的分隔符改为“//”**。因为存储过程中包含很多 SQL 语句,如果不修改分隔符的话,MySQL 会在读到第一个 SQL 语句的分隔符“;”的时候,认为语句
|
|
|
|
|
结束并且执行,这样就会导致错误。
|
|
|
|
|
- 第二步,我们来创建存储过程,把要处理的**日期作为一个参数传入**(关于参数,下面我会具体讲述)。同时,**用 BEGIN 和 END 关键字把存储过程中的 SQL 语句包裹起来,形成
|
|
|
|
|
存储过程的程序体**。
|
|
|
|
|
- 第三步,在程序体中,**先定义 2 个数据类型为 DATETIME 的变量**,用来记录要计算数据的起始时间和截止时间。
|
|
|
|
|
- 第四步,**删除保存结果数据的单品统计表中相同时间段的数据**,目的是防止数据重复。
|
|
|
|
|
- 第五步,**计算起始时间和截止时间内单品的销售数量合计、销售金额合计、成本合计、毛利和毛利率,并且把结果存储到单品统计表中**。
|
|
|
|
|
- 这五个步骤,我们就可以用下面的代码来实现。
|
|
|
|
|
```sql
|
|
|
|
|
mysql> DELIMITER // -- 设置分割符为//
|
|
|
|
|
-> CREATE PROCEDURE demo.dailyoperation(transdate TEXT)
|
|
|
|
|
-> BEGIN -- 开始程序体
|
|
|
|
|
-> DECLARE startdate,enddate DATETIME; -- 定义变量
|
|
|
|
|
-> SET startdate = date_format(transdate,'%Y-%m-%d'); -- 给起始时间赋值
|
|
|
|
|
-> SET enddate = date_add(startdate,INTERVAL 1 DAY); -- 截止时间赋值为1天以后
|
|
|
|
|
-> -- 删除原有数据
|
|
|
|
|
-> DELETE FROM demo.dailystatistics
|
|
|
|
|
-> WHERE
|
|
|
|
|
-> salesdate = startdate;
|
|
|
|
|
-> -- 插入新计算的数据
|
|
|
|
|
-> INSERT into dailystatistics
|
|
|
|
|
-> (
|
|
|
|
|
-> salesdate,
|
|
|
|
|
-> itemnumber,
|
|
|
|
|
-> quantity,
|
|
|
|
|
-> actualvalue,
|
|
|
|
|
-> cost,
|
|
|
|
|
-> profit,
|
|
|
|
|
-> profitratio
|
|
|
|
|
-> )
|
|
|
|
|
-> SELECT
|
|
|
|
|
-> LEFT(b.transdate,10),
|
|
|
|
|
-> a.itemnumber,
|
|
|
|
|
-> SUM(a.quantity), -- 数量总计
|
|
|
|
|
-> SUM(a.salesvalue), -- 金额总计
|
|
|
|
|
-> SUM(a.quantity*c.avgimportprice), -- 计算成本
|
|
|
|
|
-> SUM(a.salesvalue-a.quantity*c.avgimportprice), -- 计算毛利
|
|
|
|
|
-> CASE sum(a.salesvalue) WHEN 0 THEN 0
|
|
|
|
|
-> ELSE round(sum(a.salesvalue-a.quantity*c.avgimportprice)/sum(a.salesvalue),
|
|
|
|
|
-> FROM
|
|
|
|
|
-> demo.transactiondetails AS a
|
|
|
|
|
-> JOIN
|
|
|
|
|
-> demo.transactionhead AS b
|
|
|
|
|
-> ON (a.transactionid = b.transactionid)
|
|
|
|
|
-> JOIN
|
|
|
|
|
-> demo.goodsmaster c
|
|
|
|
|
-> ON (a.itemnumber=c.itemnumber)
|
|
|
|
|
-> WHERE
|
|
|
|
|
-> b.transdate>startdate AND b.transdate<enddate
|
|
|
|
|
-> GROUP BY
|
|
|
|
|
-> LEFT(b.transdate,10),a.itemnumber
|
|
|
|
|
-> ORDER BY
|
|
|
|
|
-> LEFT(b.transdate,10),a.itemnumber;
|
|
|
|
|
-> END
|
|
|
|
|
-> // -- 语句结束,执行语句
|
|
|
|
|
Query OK, 0 rows affected (0.01 sec)
|
|
|
|
|
-> DELIMITER ; -- 恢复分隔符为;
|
|
|
|
|
```
|
|
|
|
|
- 在这个存储过程中,我们用到了存储过程的参数定义和程序体,这些具体是什么意思呢?
|
|
|
|
|
|
|
|
|
|
- 存储过程的参数定义
|
|
|
|
|
- 存储过程可以有参数,也可以没有参数。一般来说,当我们通过客户端或者应用程序调用存储过程的时候,如果需要与存储过程进行数据交互,比如,存储过程需要根据输入的数
|
|
|
|
|
值为基础进行某种数据处理和计算,或者需要把某个计算结果返回给调用它的客户端或者用程序,就需要设置参数。否则,就不用设置参数。
|
|
|
|
|
- 参数有 3 种,分别是 IN、OUT 和 INOUT。
|
|
|
|
|
- IN 表示输入的参数,**存储过程只是读取这个参数的值。如果没有定义参数种类,默认就是 IN,表示输入参数**。
|
|
|
|
|
- OUT 表示输出的参数,**存储过程在执行的过程中,把某个计算结果值赋给这个参数**,执行完成之后,调用这个存储过程的客户端或者应用程序就可以**读取这个参数返回的值了**。
|
|
|
|
|
- INOUT **表示这个参数既可以作为输入参数,又可以作为输出参数使用**。
|
|
|
|
|
- **除了定义参数种类,还要对参数的数据类型进行定义**。在这个存储过程中,我定义了一个参数 transdate 的数据类型是 TEXT。这个参数的用处是告诉存储过程,我要处理的是哪一
|
|
|
|
|
天的数据。我没有指定参数种类是 IN、OUT 或者 INOUT,这是因为在 MySQL 中,**如果不指定参数的种类,默认就是 IN,表示输入参数**。
|
|
|
|
|
- 知道了参数,下面我具体讲解一下这个存储过程的程序体。存储过程的具体操作步骤都包含在程序体里面,我们来分析一下程序体中 SQL 操作的内容,就可以知道存储过程到底在
|
|
|
|
|
做什么。
|
|
|
|
|
|
|
|
|
|
- 存储过程的程序体
|
|
|
|
|
- 程序体中包含的是存储过程需要执行的 SQL 语句,一般通过关键字 **BEGIN 表示 SQL 语句的开始,通过 END 表示 SQL 语句的结束**
|
|
|
|
|
- 在程序体的开始部分,我定义了 2 个变量,分别是 startdate 和 enddate。它们都是 DATETIME 类型,作用是根据输入参数 transdate,计算出需要筛选的数据的时间区间。
|
|
|
|
|
- 后面的代码分 3 步完成起始时间和截止时间的计算,并且分别赋值给变量 startdate 和 enddate。
|
|
|
|
|
- 第一步,使用 DATE_FROMAT()函数,把输入的参数,按照 YYYY 年 MM 月 DD 日的格式转换成了日期时间类型数据,比如输入参数是“2020-12-01”,那么,转换成的日期、
|
|
|
|
|
时间值是“2020-12-01 00:00:00”,表示 2020 年 12 月 01 日 00 点 00 分 00 秒。
|
|
|
|
|
- 第二步,把第一步中计算出的值,作为起始时间赋值给变量 startdate。
|
|
|
|
|
- 第三步,把第一步中计算出的值,通过 DATE_ADD()函数,计算出 1 天以后的时间赋值给变量 enddate。
|
|
|
|
|
- 这样,我就获得了需要计算的销售时段。计算出了起始时间和截止时间之后,我们先删除需要计算日期的单品统计数据,以防止数据重复。接着,我们重新计算单品的销售统计,
|
|
|
|
|
并且把计算的结果插入到单品统计表。
|
|
|
|
|
- 在计算单品销售统计的时候,也分为 3 步:
|
|
|
|
|
- 按照“成本 = 销售数量×平均价格”的方式计算成本;
|
|
|
|
|
- 按照“毛利 = 销售金额 - 成本”的方式计算毛利;
|
|
|
|
|
- 按照“毛利率 = 毛利 ÷销售金额”的方式计算毛利率。
|
|
|
|
|
- 需要注意的是,这里我**使用 CASE 函数来解决销售金额为 0 时计算毛利的问题**。这是为了防止计算出现被 0 除而报错的情况。不要以为销售金额就一定大于 0,在实际项目运行的
|
|
|
|
|
过程中,会出现因为优惠而导致实际销售金额为 0 的情况。我建议你在实际工作中,把这些极端情况都考虑在内,提前进行防范,这样你的代码才能稳定可靠。
|
|
|
|
|
- 存储过程通过开始时定义的分隔符“//”结束,MySQL 执行这段 SQL 语句,就创建出了一个存储过程:demo.dailyoperation. **最后,你不要忘了把分隔符改回到“;”**。
|
|
|
|
|
- 怎么知道我们创建的存储过程是否成功了呢?下面我介绍一下查看存储过程的方法。
|
|
|
|
|
|
|
|
|
|
- 如何查看存储过程?
|
|
|
|
|
- 我们可以通过 SHOW CREATE PROCEDURE 存储过程名称,来查看刚刚创建的存储过程:
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SHOW CREATE PROCEDURE demo.dailyoperation \G
|
|
|
|
|
*************************** 1. row ***************************
|
|
|
|
|
Procedure: dailyoperation -- 存储过程名
|
|
|
|
|
sql_mode: STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
|
|
|
|
|
Create Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `dailyoperation`
|
|
|
|
|
BEGIN -- 开始程序体
|
|
|
|
|
DECLARE startdate,enddate DATETIME;
|
|
|
|
|
SET startdate = date_format(transdate,'%Y-%m-%d');
|
|
|
|
|
SET enddate = date_add(startdate,INTERVAL 1 DAY);
|
|
|
|
|
DELETE FROM demo.dailystatistics -- 删除重复
|
|
|
|
|
WHERE
|
|
|
|
|
salesdate = startdate;
|
|
|
|
|
INSERT into dailystatistics -- 将计算结果插入每日统计表
|
|
|
|
|
(
|
|
|
|
|
salesdate,
|
|
|
|
|
itemnumber,
|
|
|
|
|
quantity,
|
|
|
|
|
actualvalue,
|
|
|
|
|
cost,
|
|
|
|
|
profit,
|
|
|
|
|
profitratio
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
LEFT(b.transdate,10),
|
|
|
|
|
a.itemnumber,
|
|
|
|
|
SUM(a.quantity),
|
|
|
|
|
SUM(a.salesvalue),
|
|
|
|
|
SUM(a.quantity*c.avgimportprice),
|
|
|
|
|
SUM(a.salesvalue-a.quantity*c.avgimportprice),
|
|
|
|
|
CASE sum(a.salesvalue) WHEN 0 THEN 0
|
|
|
|
|
ELSE round(sum(a.salesvalue-a.quantity*c.avgimportprice)/sum(a.salesvalue),4)
|
|
|
|
|
FROM
|
|
|
|
|
demo.transactiondetails AS a
|
|
|
|
|
JOIN
|
|
|
|
|
demo.transactionhead AS b
|
|
|
|
|
ON (a.transactionid = b.transactionid)
|
|
|
|
|
JOIN
|
|
|
|
|
demo.goodsmaster c
|
|
|
|
|
ON (a.itemnumber=c.itemnumber)
|
|
|
|
|
WHERE
|
|
|
|
|
b.transdate>startdate AND b.transdate<enddate
|
|
|
|
|
GROUP BY
|
|
|
|
|
LEFT(b.transdate,10),a.itemnumber
|
|
|
|
|
ORDER BY
|
|
|
|
|
LEFT(b.transdate,10),a.itemnumber;
|
|
|
|
|
END -- 结束程序体
|
|
|
|
|
character_set_client: gbk -- 采用的字符集gbk
|
|
|
|
|
collation_connection: gbk_chinese_ci -- 连接校对采用的字符集
|
|
|
|
|
Database Collation: utf8mb4_0900_ai_ci -- 数据校对字符集
|
|
|
|
|
1 row in set (0 00 sec)
|
|
|
|
|
```
|
|
|
|
|
- 如何调用存储过程?
|
|
|
|
|
- 下面我们来尝试调用一下这个存储过程,并且给它传递一个参数“2020-12-01”,也就是计算 2020 年 12 月 01 日的单品统计数据:
|
|
|
|
|
```sql
|
|
|
|
|
mysql> CALL demo.dailyoperation('2020-12-01');
|
|
|
|
|
Query OK, 2 rows affected (0.03 sec)
|
|
|
|
|
```
|
|
|
|
|
- 存储过程执行结果提示“Query OK”,表示执行成功了。“2 rows affected”表示执行的结果影响了 2 条数据记录。
|
|
|
|
|
- 我们用 SELECT 语句来查看一下单品统计表,看看有没有把单品统计的结果存入单品统计表中。
|
|
|
|
|
```sql
|
|
|
|
|
mysql> SELECT * -- 查询单品统计表中的数据
|
|
|
|
|
-> FROM demo.dailystatistics;
|
|
|
|
|
+----+------------+----------+-------------+--------+--------+-------------+--
|
|
|
|
|
| id | itemnumber | quantity | actualvalue | cost | profit | profitratio | sal
|
|
|
|
|
+----+------------+----------+-------------+--------+--------+-------------+--
|
|
|
|
|
| 13 | 1 | 3.000 | 267.00 | 100.50 | 166.50 | 0.6236 | 2020-12-01 00:00:00 |
|
|
|
|
|
| 14 | 2 | 2.000 | 10.00 | 7.00 | 3.00 | 0.3000 | 2020-12-01 00:00:00 |
|
|
|
|
|
+----+------------+----------+-------------+--------+--------+-------------+--
|
|
|
|
|
2 rows in set (0.00 sec)
|
|
|
|
|
```
|
|
|
|
|
- 看到了吗?我们已经能够在单品统计表中,查询到 2020 年 12 月 01 日的单品统计结果了。这也就意味着我们的存储过程被执行了,它计算出了我们需要的单品统计结果,并且
|
|
|
|
|
把统计结果存入了单品统计表中。
|
|
|
|
|
- 如何修改和删除存储过程?
|
|
|
|
|
- 如果你需要修改存储过程的内容,我建议你在 IDE 中操作。这是因为你可以在里面直接修改存储过程,而如果用 SQL 命令来修改存储过程,就必须删除存储过程再重新创
|
|
|
|
|
建,相比之下,在 IDE 中修改比较简单。
|
|
|
|
|
- 在 MySQL 中,存储过程不像普通的编程语言(比如 VC++、Java 等)那样有专门的集成 开发环境。因此,你可以通过 SELECT 语句,把程序执行的中间结果查询出来,来调试一
|
|
|
|
|
个 SQL 语句的正确性。调试成功之后,把 SELECT 语句后移到下一个 SQL 语句之后,再调试下一个 SQL 语句。这样逐步推进,就可以完成对存储过程中所有操作的调试了。当
|
|
|
|
|
然,你也可以把存储过程中的 SQL 语句复制出来,逐段单独调试。
|
|
|
|
|
- 删除存储过程很简单,你知道具体的语法就行了:
|
|
|
|
|
```sql
|
|
|
|
|
DROP PROCEDURE 存储过程名称;
|
|
|
|
|
```
|
|
|
|
|
### 2.3 游标:对于数据集中的记录,该怎么逐条处理?
|
|
|
|
|
- 所谓的游标,也就是能够对结果集中的每一条记录进行定位,并对指向的记录中的数据进行操作的数据结构。
|
|
|
|
|
|
|
|
|
|
- 游标的使用步骤
|
|
|
|
|
- 游标只能在存储程序内使用,存储程序包括存储过程和存储函数。关于存储过程,我们上节课刚刚学过,这里我简单介绍一下存储函数。创建存储函数的语法是:
|
|
|
|
|
```sql
|
|
|
|
|
CREATE FUNCTION 函数名称 (参数)RETURNS 数据类型 程序体
|
|
|
|
|
```
|
|
|
|
|
- 存储函数与存储过程很像,但有几个不同点:
|
|
|
|
|
- 存储函数必须返回一个值或者数据表,存储过程可以不返回。
|
|
|
|
|
- 存储过程可以通过 CALL 语句调用,存储函数不可以。
|
|
|
|
|
- 存储函数可以放在查询语句中使用,存储过程不行。
|
|
|
|
|
- 存储过程的功能更加强大,包括能够执行对表的操作(比如创建表,删除表等)和事务操作,这些功能是存储函数不具备的。
|
|
|
|
|
- 游标在存储函数中的使用方法和在存储过程中的使用方法是一样的。
|
|
|
|
|
|
|
|
|
|
- 在使用游标的时候,主要有 4 个步骤。
|
|
|
|
|
- 第一步,定义游标。语法结构如下:
|
|
|
|
|
```sql
|
|
|
|
|
DECLARE 游标名CURSOR FOR 查询语句
|
|
|
|
|
```
|
|
|
|
|
- 这里就是声明一个游标,它可以操作的数据集是“查询语句”返回的结果集。
|
|
|
|
|
- 第二步,打开游标。语法结构如下:
|
|
|
|
|
```sql
|
|
|
|
|
OPEN 游标名称;
|
|
|
|
|
```
|
|
|
|
|
- 打开游标之后,系统会为游标准备好查询的结果集,为后面游标的逐条读取结果集中的记录做准备。
|
|
|
|
|
- 第三步,从游标的数据结果集中读取数据。语法结构是这样的:
|
|
|
|
|
```sql
|
|
|
|
|
FETCH 游标名 INTO 变量列表;
|
|
|
|
|
```
|
|
|
|
|
- 这里的意思是通过游标,把当前游标指向的结果集中那一条记录的数据,赋值给列表中的变量。
|
|
|
|
|
- 需要注意的是,**游标的查询结果集中的字段数,必须跟 INTO 后面的变量数一致**,否则,在存储过程执行的时候,MySQL 会提示错误。
|
|
|
|
|
- 第四步,关闭游标。语法结构如下:
|
|
|
|
|
```sql
|
|
|
|
|
CLOSE 游标名;
|
|
|
|
|
```
|
|
|
|
|
- **用完游标之后,你一定要记住及时关闭游标。因为游标会占用系统资源**,如果不及时关闭,游标会一直保持到存储过程结束,影响系统运行的效率。而关闭游标的操作,会释放
|
|
|
|
|
游标占用的系统资源。
|
|
|
|
|
|
|
|
|
|
- 知道了基本步骤,下面我就结合超市项目的实际案例,带你实战一下。
|
|
|
|
|
|
|
|
|
|
- 案例串讲
|
|
|
|
|
- 在超市项目的进货模块中,有一项功能是对进货单数据进行验收。其实就是在对进货单的数据确认无误后,对进货单的数据进行处理,包括增加进货商品的库存,并修改商品的平
|
|
|
|
|
均进价。下面我用实际数据来演示一下这个操作流程。
|
|
|
|
|
- 这里我们要用到进货单头表(demo.importheadl)、进货单明细表(demo.importdetails)、库存表(demo.inventory)和商品信息表
|
|
|
|
|
(demo.goodsmaster)。
|
|
|
|
|
- 进货单头表:
|
|
|
|
|
- ![游标进货单头表](pic/游标进货单头表.png)
|
|
|
|
|
- 进货单明细表:
|
|
|
|
|
- ![游标进货单明细表](pic/游标进货单明细表.png)
|
|
|
|
|
- 库存表:
|
|
|
|
|
- ![游标库存表](pic/游标库存表.png)
|
|
|
|
|
- 商品信息表:
|
|
|
|
|
- ![游标商品信息表](pic/游标商品信息表.png)
|
|
|
|
|
- 要验收进货单,我们就需要对每一个进货商品进行两个操作:
|
|
|
|
|
- 在现有库存数量的基础上,加上本次进货的数量;
|
|
|
|
|
- 根据本次进货的价格、数量,现有商品的平均进价和库存,计算新的平均进价:(本次进货价格 * 本次进货数量 + 现有商品平均进价 * 现有商品库存)/(本次进货数量 + 现
|
|
|
|
|
有库存数量)
|
|
|
|
|
- 因为我们需要通过应用程序来控制操作流程,做成一个循环操作,每次只查询一种商品的数据记录并进行处理,一直到把进货单中的数据全部处理完。这样一来,应用必须发送很
|
|
|
|
|
多的 SQL 指令到服务器,跟服务器的交互多,不仅代码复杂,而且也不够安全。
|
|
|
|
|
- 这个时候,如果使用游标,就很容易了。因为所有的操作都可以在服务器端完成,应用程序只需要发送一个命令调用存储过程就可以了。现在,我们就来看看如何用游标来解决这
|
|
|
|
|
个问题。
|
|
|
|
|
- 我用代码创建了一个存储过程 demo.mytest()。创建存储过程的代码如下:
|
|
|
|
|
```sql
|
|
|
|
|
mysql> DELIMITER //
|
|
|
|
|
mysql> CREATE PROCEDURE demo.mytest(mylistnumber INT)
|
|
|
|
|
-> BEGIN
|
|
|
|
|
-> DECLARE mystockid INT;
|
|
|
|
|
-> DECLARE myitemnumber INT;
|
|
|
|
|
-> DECLARE myquantity DECIMAL(10,3);
|
|
|
|
|
-> DECLARE myprice DECIMAL(10,2);
|
|
|
|
|
-> DECLARE done INT DEFAULT FALSE; -- 用来控制循环结束
|
|
|
|
|
-> DECLARE cursor_importdata CURSOR FOR -- 定义游标
|
|
|
|
|
-> SELECT b.stockid,a.itemnumber,a.quantity,a.importprice
|
|
|
|
|
-> FROM demo.importdetails AS a
|
|
|
|
|
-> JOIN demo.importhead AS b
|
|
|
|
|
-> ON (a.listnumber=b.listnumber)
|
|
|
|
|
-> WHERE a.listnumber = mylistnumber;
|
|
|
|
|
-> DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; -- 条件处理语句
|
|
|
|
|
->
|
|
|
|
|
-> OPEN cursor_importdata; -- 打开游标
|
|
|
|
|
-> FETCH cursor_importdata INTO mystockid,myitemnumber,myquantity,myprice; --
|
|
|
|
|
-> REPEAT
|
|
|
|
|
-> -- 更新进价
|
|
|
|
|
-> UPDATE demo.goodsmaster AS a,demo.inventory AS b
|
|
|
|
|
-> SET a.avgimportprice = (a.avgimportprice*b.invquantity+myprice*myquantity)/
|
|
|
|
|
-> WHERE a.itemnumber=b.itemnumber AND b.stockid=mystockid AND a.itemnumber=my
|
|
|
|
|
-> -- 更新库存
|
|
|
|
|
-> UPDATE demo.inventory
|
|
|
|
|
-> SET invquantity = invquantity + myquantity
|
|
|
|
|
-> WHERE stockid = mystockid AND itemnumber=myitemnumber;
|
|
|
|
|
-> -- 获取下一条记录
|
|
|
|
|
-> FETCH cursor_importdata INTO mystockid,myitemnumber,myquantity,myprice;
|
|
|
|
|
-> UNTIL done END REPEAT;
|
|
|
|
|
-> CLOSE cursor_importdata;
|
|
|
|
|
-> END
|
|
|
|
|
-> //
|
|
|
|
|
Query OK, 0 rows affected (0.02 sec)
|
|
|
|
|
-> DELIMITER ;
|
|
|
|
|
```
|
|
|
|
|
- 这段代码比较长,核心操作有 6 步
|
|
|
|
|
- 把 MySQL 的分隔符改成“//”
|
|
|
|
|
- 开始程序体之后,我定义了 4 个变量,分别是 mystockid、myitemnumber、myquantity 和 myprice,这几个变量的作用是,存储游标中读取的仓库编号、商品编
|
|
|
|
|
号、进货数量和进货价格数据。
|
|
|
|
|
- 定义游标。这里我指定了游标的名称,以及游标可以处理的数据集(mylistnumber 指定的进货单的全部进货商品明细数据)。
|
|
|
|
|
- 定义条件处理语句“DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;”。
|
|
|
|
|
- 打开游标,读入第一条记录,然后开始执行数据操作。
|
|
|
|
|
- 关闭游标,结束程序。
|
|
|
|
|
- 可以看到,在这个存储过程中,我用到了条件处理语句,它的作用是告诉系统,在存储程序遇到问题的时候,应该如何处理。
|
|
|
|
|
|
|
|
|
|
- 条件处理语句
|
|
|
|
|
- 条件处理语句的语法结构:
|
|
|
|
|
```sql
|
|
|
|
|
DECLARE 处理方式 HANDLER FOR 问题 操作;
|
|
|
|
|
```
|
|
|
|
|
- 下面我结合刚刚的“DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;”,来解释一下条件处理语句是如何工作的。
|
|
|
|
|
- **语法结构中的“问题”是指 SQL 操作中遇到了什么问题**。比如这里的问题是“NOT FOUND”,意思就是游标走到结果集的最后,没有记录了。也就是说,数据集中的所
|
|
|
|
|
有记录都已经处理完了。
|
|
|
|
|
- 执行的操作是“SET done=TRUE”,done 是我定义的用来标识数据集中的数据是否已经处理完成的一个标记。done=TRUE,意思是数据处理完成了。
|
|
|
|
|
- **处理方式有 2 种选择,分别是“CONTINUE”和“EXIT”**,表示遇到问题,执行了语法结构中的“操作”之后,是选择继续运行程序,还是选择退出,结束程序。
|
|
|
|
|
- 所以,这个条件处理语句的意思就是:当游标读到结果集的最后,没有记录了,设置操作完成标识为真,然后继续运行程序。
|
|
|
|
|
- 在存储过程的第 5 步,为了逐一处理每一条记录,我还使用了流程控制语句。
|
|
|
|
|
- 解决复杂问题不可能通过一个 SQL 语句完成,我们需要执行多个 SQL 操作。流程控制语句的作用就是控制存储过程中 SQL 语句的执行顺序,是我们完成复杂操作必不可少的一部
|
|
|
|
|
分。下面我就给你具体讲解一下。
|
|
|
|
|
|
|
|
|
|
- 流程控制语句
|
|
|
|
|
- MySQL 的流程控制语句也只能用于存储程序。主要有 3 类。
|
|
|
|
|
- 跳转语句:ITERATE 和 LEAVE 语句。
|
|
|
|
|
- 循环语句:LOOP、WHILE 和 REPEAT 语句。
|
|
|
|
|
- 条件判断语句:IF 语句和 CASE 语句。
|
|
|
|
|
|
|
|
|
|
- 跳转语句
|
|
|
|
|
- ITERATE 语句:只能用在循环语句内,表示重新开始循环。
|
|
|
|
|
- LEAVE 语句:可以用在循环语句内,或者以 BEGIN 和 END 包裹起来的程序体内,跳出循环或者跳出程序体的操作。
|
|
|
|
|
|
|
|
|
|
- 循环语句
|
|
|
|
|
- LOOP 语句的语法结构是:
|
|
|
|
|
```sql
|
|
|
|
|
标签:LOOP
|
|
|
|
|
操作
|
|
|
|
|
END LOOP 标签;
|
|
|
|
|
```
|
|
|
|
|
- 关于这个语句,需要注意的是,LOOP 循环不能自己结束,需要用跳转语句 ITERATE 或者LEAVE 来进行控制。
|
|
|
|
|
|
|
|
|
|
- WHILE 语句的语法结构:
|
|
|
|
|
```sql
|
|
|
|
|
WHILE 条件 DO
|
|
|
|
|
操作
|
|
|
|
|
END WHILE;
|
|
|
|
|
```
|
|
|
|
|
- WHILE 循环通过判断条件是否为真来决定是否继续执行循环中的操作,你要注意一点,**WHILE 循环是先判断条件,再执行循环体中的操作**。
|
|
|
|
|
- REPEAT 语句的语法结构:
|
|
|
|
|
```sql
|
|
|
|
|
REPEAT
|
|
|
|
|
操作
|
|
|
|
|
UNTIL 条件 END REPEAT;
|
|
|
|
|
```
|
|
|
|
|
- REPEAT 循环也是通过判断条件是否为真来决定是否继续执行循环内的操作的,与 WHILE 不同的是,**REPEAT 循环是先执行操作,后判断条件**。
|
|
|
|
|
- 最后我来讲讲条件判断语句:IF 语句和 CASE 语句。
|
|
|
|
|
|
|
|
|
|
- 条件判断语句
|
|
|
|
|
- IF 语句的语法结构是:
|
|
|
|
|
```sql
|
|
|
|
|
IF 表达式1 THEN 操作1
|
|
|
|
|
[ELSEIF 表达式2 THEN 操作2]……
|
|
|
|
|
[ELSE 操作N]
|
|
|
|
|
END IF
|
|
|
|
|
```
|
|
|
|
|
- 这里“[]”中的内容是可选的。IF 语句的特点是,不同的表达式对应不同的操作。
|
|
|
|
|
|
|
|
|
|
- CASE 语句的语法结构是:
|
|
|
|
|
```sql
|
|
|
|
|
CASE 表达式
|
|
|
|
|
WHEN 值1 THEN 操作1
|
|
|
|
|
[WHEN 值2 THEN 操作2]……
|
|
|
|
|
[ELSE 操作N]
|
|
|
|
|
END CASE;
|
|
|
|
|
```
|
|
|
|
|
- 这里“[]”中的内容是可选的。CASE 语句的特点是,表达式不同的值对应不同的操作。
|
|
|
|
|
- 到这里,我们处理进货单验收的存储过程就创建好了。现在,让我们来运行一下这个存储过程,看看能不能得到我们想要的结果:
|
|
|
|
|
```sql
|
|
|
|
|
mysql> CALL demo.mytest(1234); -- 调用存储过程,验收单号是1234的进货单
|
|
|
|
|
Query OK, 0 rows affected (11.68 sec) -- 执行成功了
|
|
|
|
|
mysql> select * from demo.inventory; -- 查看库存,已经改过来了
|
|
|
|
|
+---------+------------+-------------+
|
|
|
|
|
| stockid | itemnumber | invquantity |
|
|
|
|
|
+---------+------------+-------------+
|
|
|
|
|
| 1 | 1 | 15.000 |
|
|
|
|
|
| 1 | 2 | 23.000 |
|
|
|
|
|
+---------+------------+-------------+
|
|
|
|
|
2 rows in set (0.00 sec)
|
|
|
|
|
mysql> select * from demo.goodsmaster; -- 查看商品信息表,平均进价也改过来了
|
|
|
|
|
+------------+---------+-----------+---------------+------+------------+------
|
|
|
|
|
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgim
|
|
|
|
|
+------------+---------+-----------+---------------+------+------------+------
|
|
|
|
|
| 1 | 0001 | 书 | 16开 | 本 | 89.00 | 31.00 |
|
|
|
|
|
| 2 | 0002 | 笔 | NULL | 包 | 5.00 | 2.87 |
|
|
|
|
|
+------------+---------+-----------+---------------+------+------------+------
|
|
|
|
|
2 rows in set (0.00 sec)
|
|
|
|
|
```
|
|
|
|
|
- 很显然,库存和平均价格都被正确地计算出来了。
|
|
|
|
|
- 有个小问题要提醒你注意:**如果一个操作要用到另外一个操作的结果,那我们一定不能搞错操作的顺序**。
|
|
|
|
|
|
|
|
|
|
### 2.4 触发器:如何让数据修改自动触发关联操作,确保数据一致性?
|
|
|
|
|
|
|
|
|
|
### 2.5 权限管理:如何控制数据库访问,消除安全隐患?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|