存储过程和函数
- 1. 简介
- 2. 创建存储过程
- 使用MySQL工作台创建存储过程
- 3. 删除存储过程
- 4. 参数
- 带默认值的参数
- 参数验证
- 输出参数
- 5. 变量
- 6. 函数
- 7. 其他约定
1. 简介
存储过程三大作用:
- 储存和管理SQL代码(置于数据库中,与应用层分离,同视图和函数一样,都是增加抽象层,作为模块化抽象工具)
- 数据安全
- 性能优化
假设你要开发一个使用数据库的应用程序,你应该将SQL语句写在哪里呢?
如果将SQL语句内嵌在应用程序的代码里,将使其混乱且难以维护,所以应该将SQL代码和应用程序代码分开,将SQL代码储存在所属的数据库中,具体来说,是放在储存过程(stored procedure)和函数中。
使用储存程序还有另外两个好处。首先,大部分DBMS会对储存过程中的代码进行一些优化,因此有时储存的SQL代码执行起来会更快。此外,就像视图一样,储存过程能加强数据安全。比如,我们可以移除对所有原始表的访问权限,让各种增删改的操作都通过储存过程来完成,然后就可以决定谁可以执行何种储存过程,用以限制用户对我们数据的操作范围,例如,防止特定的用户删除数据。
所以,储存过程很有用。
2. 创建存储过程
语法规则:
[DELIMITER $$]
-- delimiter 定界符;分隔符
CREATE PROCEDURE 储存过程名()
BEGIN
……;
……;
……;
END[$$]
[DELIMITER ;]
NOTE:
- MySQL中需要将储存过程主体内部的语句分隔符与SQL本身执行层面的语句分隔符 ; 区别开。要先用 DELIMITER(分隔符)关键字暂时将 SQL 语句的默认分隔符改为双美元符号 $ $ 或双斜杠 // 等,创建储存过程结束后再改回来。注意创建储存过程本身也是一个完整 SQL 语句,所以别忘了在 END 后要加一个暂时语句分隔符 ‘$$’。
- BEGIN 和 END 之间包裹的是此储存过程(PROCEDURE)的主体(body),主体里可以有多个语句,但每个语句都要以 ; 结束,包括最后一个。
- 调用此程序:点击闪电按钮或者用CALL关键字。大多数时候是在应用程序中调用。
DELIMITER $$
CREATE PROCEDURE get_invoices_with_balance()
BEGIN
SELECT *
FROM clinet_balance
WHERE balance > 0;
END$$
DELIMITER ;
使用MySQL工作台创建存储过程
也可以用点击的方式创造储存过程,右键选择 Create Stored Procedure,填空,Apply。这种方式Workbench会帮你处理暂时修改分隔符的问题。
3. 删除存储过程
标准模板:
DROP PROCEDURE IF EXISTS get_clients;
-- 注意加上IF EXISTS,以免因为此储存过程不存在而报错
DELIMITER $$
CREATE PROCEDURE get_clients()
BEGIN
SELECT * FROM clients;
END$$
DELIMITER ;
CALL get_clients()
Note:最好把删除和创建每一个储存过程的代码也储存在不同的SQL文件中,并把这样的文件放在Git这样的源码控制下,这样就能与其它团队成员共享Git储存库。他们就能在自己的机器上重建(复现)数据库以及该数据库下的所有的视图和储存过程参数
4. 参数
模板:
CREATE PROCEDURE 储存过程名(
参数1 数据类型,
参数2 数据类型,
……)
BEGIN
……;
END
例子:
USE sql_invoicing;
DROP PROCEDURE IF EXISTS get_clients_by_state;
DELIMITER $$
CREATE PROCEDURE get_clients_by_state(state CHAR(2)) -- 一般用VARCHAR,除非可以确定字符的个数
BEGINSELECT * FROM clients c -- 采用别名的形式来引用参数,也可用前缀和后缀WHERE c.state = state; -- 不能采用相同的别名来命名,如state = state
END $$
DELIMITER ;
带默认值的参数
给参数设置默认值,主要是运用条件语句块和替换空值函数。
SQL中的条件类语句:
- 替换空值 IFNULL(值1,值2);
- IF函数 IF(条件表达式, 返回值1, 返回值2)
- IF语句
- CASE语句
CREATE PROCEDURE get_clients_by_state( -- 尽量不用黄色箭头调用,传参数会出错。用CALL调用state CHAR(2))
BEGINIF state IS NULL THENSET state = 'CA';END IF;SELECT * FROM clients cWHERE c.state = state;
ENDCREATE PROCEDURE get_clients_by_state(state CHAR(2))
BEGINSELECT * FROM clients cWHERE c.state = IFNULL(state, c.state);
END
注意一个区别:
- Parameter 形参(形式参数):创建储存过程中用的占位符,如 client_id、payment_method_id
- Argument 实参(实际参数):调用时实际传入的值,如 1、3、5、NULL
参数验证
使用方法:储存过程除了可以查,也可以增删改,但修改数据前最好先进行参数验证以防止不合理的修改。主要利用 IF THEN条件语句和 SIGNAL SQLSTATE/MESSAGE_TEXT 关键字。
具体的错误类型可通过谷歌 “sqlstate error” 查阅(推荐使用IBM的那个表),这里是 ‘22 Data Exception’ 大类中的 ‘22003 A numeric value is out of range.’ 类型。
CREATE DEFINER=`root`@`localhost` PROCEDURE `make_payment`(invoice_id INT,payment_amount DECIMAL(9,2),/*9是精度(存储的有效位数), 2是小数位数。见:https://dev.mysql.com/doc/refman/8.0/en/fixed-point-types.html*/payment_date DATE)
BEGINIF payment_amount <= 0 THENSIGNAL SQLSTATE '22003'SET MESSAGE_TEXT = 'Invalid payment amount';UPDATE invoices iSETi.payment_total = payment_amount,i.payment_date = payment_dateWHERE i.invoice_id = invoice_id;
END
Note:过犹不及,加入过多的参数验证会让代码过于复杂难以维护,像payment_amount 非空这样的验证就不需要添加因为 payment_amount 字段本身就不允许空值因此MySQL 会自动报错。参数验证工作更多的应该在应用程序端接受用户输入数据时就检测和报告,那样更快也更有效。储存过程里的参数验证只是在有人越过应用程序直接访问储存过程时作为最后的防线。这里只应该写那些最关键和必要的参数验证。
输出参数
输入参数是用来给储存过程传入值的,我们也可以用输出参数(变量)来获取储存程序的值.
- 使用方法:具体是在参数的前面加上 OUT 关键字,然后再 SELECT 后加上 INTO……
调用麻烦,如无需要,不要多此一举
CREATE DEFINER=`root`@`localhost` PROCEDURE `get_unpaid_invoices_for_client`(client_id INT,OUT invoice_count INT,OUT invoice_total DECIMAL(9,2))-- 默认是输入参数,输出参数要加OUT前缀
BEGINSELECT COUNT(*),SUM(invoice_total)INTO invoice_count, invoice_total-- SELECT后跟上INTO语句将SELECT选出的值传入输出参数(输出变量)中FROM invoices iWHEREi.client_id = client_id ANDpayment_total = 0;
END
通过输出参数获取并读取数据有些麻烦,若无充足的原因,不要多此一举。
5. 变量
两种变量:
- 用户或会话变量 SET @变量名 = ……
用 SET 语句并在变量名前加 @ 前缀来定义,将在整个用户会话期间存续,在会话结束断开MySQL连接时才被清空,这种变量主要在调用带输出变量的储存过程时使用,用来传入储存过程作为输出参数来获取结果值。 - 本地变量 DECLARE 变量名 数据类型 [DEFAULT 默认值]
在储存过程或函数中通过 DECLARE 声明并使用,在函数或储存过程执行结束时就被清空,常用来执行储存过程(或函数)中的计算。
CREATE PROCEDURE get_risk_factor ()
BEGIN-- 声明三个本地变量,可设默认值DECLARE risk_factor DECIMAL(9,2) DEFAULT 0;DECLARE invoices_total DECIMAL(9,2);DECLARE invoices_count INT;-- 用SELECT得到需要的值并用INTO传入变量SELECT SUM(invoice_total), COUNT(*)FROM invoices;-- 用SET语句给risk_factor计算赋值SET risk_factor = invoices_total/invoices_count * 5;-- 用SELECT语句将结果展示出来SELECT risk_factor;
END
6. 函数
函数和储存过程的作用非常相似,唯一区别是函数只能返回单一值而不能返回多行多列的结果集,当你只需要返回一个值时就可以创建函数。区别:
- 参数设置和 body 主体之间,有一段确定返回值类型以及函数属性的语句段。
- 最后是返回(RETURN)值而不是查询(SELECT)值。
- 删除语法:DROP FUNCTION IF EXISTS。
关于函数属性的说明:
- DETERMINISTIC 决定性的,唯一输入决定唯一输出,和数据的改动更新无关,比如税收是订单总额的10%,则以订单总额为输入、税收为输出的函数就是决定性的,但这里每个顾客的 risk_factor 会随着其发票记录的增加、更新而改变,所以不是 DETERMINISTIC 的
- READS SQL DATA 需要用到 SELECT 语句进行数据读取的函数,几乎所有函数都满足
- MODIFIES SQL DATA 函数中有 增删改 或者说有 INSERT DELETE UPDATE 语句,这个例子不需要。
CREATE DEFINER=`root`@`localhost` FUNCTION `get_risk_factor_for_clients`(client_id INT) RETURNS int
READS SQL DATA
BEGINDECLARE risk_factor DECIMAL(9,2) DEFAULT 0;DECLARE invoices_total DECIMAL(9,2);DECLARE invoices_count INT;SELECT SUM(invoice_total), COUNT(*)INTO invoices_total, invoices_countFROM invoices iWHERE i.client_id = client_id;SET risk_factor = invoices_total/invoices_count * 5;RETURN IFNULL(risk_factor, 0);
END
7. 其他约定
一些命名规则:
- 函数前加上fn前缀;
- 存储过程加上proc前缀
- 命名采用驼峰式:如procGetRiskFactor
- 不采用驼峰式,则采用下划线如:get_risk_factor
- 分隔符,有人用$$,有人用//
- 入乡随俗,遵守已经有的约定