使用的是MySQL数据库

1. 存储过程的优点

-- 1)可以存储和管理SQL代码;
-- 2)执行快;
-- 3)可以加强数据安全性;(如:限制用户对数据的访问权限,取消对表的直接访问权限)

2. 创建存储过程 CREATE PROCEDURE

  • 存储过程可以在Stored Procedures中查看;
-- 存储过程存储SQL语句,写法:
-- CREATE PROCEDURE 存储过程名字(参数)
-- BEGIN
--     存储过程的主体(SQL语句),*MySQL要求SQL语句用分号隔开,一条SQL语句也需要加分号
-- END
 
-- 如何将一整个存储过程打包给MySQL,而不只是存储过程主体呢?(其他DBMS不需要改动默认分隔符)
-- 需要将分割SQL语句的分号改成其他的分隔符,在CREATE PROCEDURE语句前加上默认分隔符DELIMITER语句,一般开发者使用两个美元符号$$,或者其他不会在SQL语句中用到的字符序列;
-- 注意END后面也要加上自定义的分隔符号,并且最后要把默认分隔符改为分号。
-- 添加默认分隔符后的存储过程写法:
-- DELIMITER $$
-- CREATE PROCEDURE 存储过程名字(参数)
-- BEGIN
--     存储过程的主体-SQL语句,*MySQL要求SQL语句用分号隔开,一条SQL语句也需要加分号
-- END$$
-- 
-- DELIMITER ;
 
-- 调用存储过程,用CALL语句,写法:
-- CALL 存储过程名字()


-- 创建存储过程:查询clients表,并将这个查询作为存储过程
DELIMITER $$
CREATE PROCEDURE get_clients()
BEGIN
	SELECT * FROM clients;
END$$

DELIMITER ;
-- 调用存储过程get_clients
CALL get_clients();
 
 
-- 练习:创建名字为get_invoices_with_balance的存储过程,存储发票结余大于0的查询结果
DELIMITER $$
CREATE PROCEDURE get_invoices_with_balance()
BEGIN
   -- 因为在之前已经创建过带有结余的视图,这里可以直接查询视图
	SELECT *
	FROM invoice_with_balance
	WHERE balance > 0;
END$$

DELIMITER ;

3. 使用MySQL工作台创建存储过程

  • 选中“Stored Procedures”,右击“Create Stored Procedure”;
  • 右侧会出现编写存储过程的窗口,在这里不需要改默认分隔符;
  • 只需要修改存储过程名称,并编写存储过程主体即可。

4. 删除存储过程 DROP PROCEDURE

  • 同一个存储过程的删除和创建代码放在同一个SQL文件中,不同的存储过程放在不同的SQL文件中,视图也一样;
-- 删除存储过程写法;
-- DROP PROCEDURE 存储过程名称
-- 若删除一个不存在的存储过程,MySQL会报错,因此加上IF EXISTS关键字
-- DROP PROCEDURE IF EXISTS 存储过程名称


-- 保存 删除和创建存储过程的代码,先写删除再写创建,这样每次执行后都会先删除原来的过程再创建更新后的存储过程;
DROP PROCEDURE IF EXISTS get_clients;

DELIMITER $$
CREATE PROCEDURE get_clients()
BEGIN
	SELECT * FROM clients;
END$$

DELIMITER ;

5. 参数

-- 实现:创建按洲state获取客户的存储过程,获取洲名,并返回所在洲的客户信息
DROP PROCEDURE IF EXISTS get_clients_by_state;

DELIMITER $$
CREATE PROCEDURE get_clients_by_state
(
	 -- CHAR是参数类型,表示是确定字符个数的字符型,括号里的2表示2个字符;
    -- 也可以使用VARCHAR 也是字符型,但表示可变长度字符,即输入的字符个数不固定,只要不超过规定的字符个数即可,VARCHAR使用较多;
	state CHAR(2)
    -- 由于参数名与列名相同,需要给参数加上前缀或后缀区分,常用的方法有:
    -- 1、前缀 p_state  pState
    -- 2、后缀 state_param
    -- 3、也可以给列名之前加表别名用于区分
)
BEGIN
	SELECT *
    FROM clients c
    WHERE c.state = state;
END$$

DELIMITER ;
 
-- 调用存储过程,查询洲名为CA的数据
CALL get_clients_by_state('CA');
 
 
-- 练习:写一个存储过程,返回给定客户的发表,存储过程名字为get_invoices_by_client
DROP PROCEDURE IF EXISTS get_invoices_by_client;

DELIMITER $$
CREATE PROCEDURE get_invoices_by_client
(
	-- 类型为INT时,可以不用规定个数;VARCHAR需要规定字符个数
	client_name VARCHAR(20)
)
BEGIN
    -- 注意:直接用*表示结果显示invoices表和clients表中所有的列
	 SELECT *
    FROM invoices i
    JOIN clients c USING (client_id)
    WHERE c.name = client_name;
END$$

DELIMITER ;
 
-- *注意:因为存储过程添加了形参,所以调用的时候,括号中一定要填写实参,不可以空着;
-- 查询客户名为Vinte的所有发票信息;
CALL get_invoices_by_client('Vinte');

6. 带默认值的参数

-- 若存储过程调用者无法明确具体的实参,可以设置一个默认值;
-- 使用IF语句就需要用END IF结束;告诉MySQL IF语句改到哪里结束;

-- 第一种:查询值为空时,返回符合设置的默认值的数据;
-- 实现:创建按洲state获取客户的存储过程,且洲名为空,默认返回洲名为CA的客户信息;
DROP PROCEDURE IF EXISTS get_clients_by_state;

DELIMITER $$
CREATE PROCEDURE get_clients_by_state
(
	state CHAR(2)
)
BEGIN
	IF state IS NULL THEN
		SET state = 'CA';
	END IF;
    
	SELECT *
    FROM clients c
    WHERE c.state = state;
END$$

DELIMITER ;
 
-- 查询洲名为空的客户
CALL get_clients_by_state(NULL);
 
 
-- 第二种:查询值为空,返回所有数据;
-- (1) 不为参数设置默认值,而是写成分段查询;以下为存储过程主体;这种写法不专业!
	IF state IS NULL THEN
		SELECT * FROM clients;
	ELSE
		SELECT *
		FROM clients c
		WHERE c.state = state;
	END IF;
 
-- (2) 把IF语句中的两段查询合并成一段,使用IFNULL(),以下为存储过程主体;
	SELECT *
	FROM clients c
    -- 使用IFNULL() 第一个参数为空时,就使用第二个参数
    -- 第一个state是存储过程定义的参数,第二个c.state是查询语句的state;
    -- WHERE子句也等于c.state = c.state;自身等于自身,则返回的也是整个表数据
	WHERE c.state = IFNULL(state,c.state);
 
-- 练习:创建存储过程get_payments,要求查询涉及client_id 和 payment_method_id的数据,
-- 若两个参数都为NULL,则输出表的所有数据;
-- 若其中一个参数不为NULL,则根据其中一个参数输出数据;
-- 若两个参数都不为NULL,则输出的数据要同时满足这两个条件;(以下代码直接在工作台创建)
CREATE DEFINER=`root`@`localhost` PROCEDURE `get_payments`(
	client_id INT,
    payment_method_id TINYINT
)
BEGIN
	SELECT *
    FROM payments p
    WHERE
		p.client_id = IFNULL(client_id,p.client_id) AND
		p.payment_method = IFNULL(payment_method_id,p.payment_method);
END
 
-- 调用查询存储过程
-- CALL get_payments(NULL,NULL);

7. 参数验证 - 确保存储过程不会往数据库存储错误数据

-- *注意存储过程可以存储负值,但根据实际情况,有些数据不能为负值,因此还需要使用IF语句设置数据范围;
-- 过程更新数据
-- 添加三个参数 发票id INT; 付款金额 小数 DECIMAL(参数位数,小数位数); 付款日期 DATE
-- 实现:根据输入的发票id,修改这张发票的付款金额和付款日期。
DROP PROCEDURE IF EXISTS make_payment;

DELIMITER $$
CREATE PROCEDURE make_payment
(
	 invoice_id INT,
    -- 付款金额位数总共9位,其中小数位2位f
    payment_amount DECIMAL(9,2),
    payment_date DATE
)
BEGIN
    -- 以下为验证存储过程的参数的方法
    -- 因为传递的参数可以为负数,而金额<=0 是错误数据,因此需要添加IF语句来验证付款总额参数
    IF payment_amount <= 0 THEN
    -- SIGNAL SQLSTATE '错误代码的字符串常量'
    -- 22003 - A numeric value is out of range 数值超出范围,也叫超位值
	     SIGNAL SQLSTATE '22003'
           -- 设置描述信息,可以帮助调用者识别失败原因
		SET MESSAGE_TEXT = '付款金额不得小于0';
    END IF;
    
   -- 以下为更新语句
	UPDATE invoices i
   SET 
	   i.payment_total = payment_amount,
      i.payment_date = payment_date
	WHERE i.invoice_id = invoice_id;
END$$

DELIMITER ;

8. 输出参数

-- 实现:查询客户发票未付款的发票总数和发票总金额
-- 直接在sql工作台创建的存储过程
CREATE DEFINER=`root`@`localhost` PROCEDURE `get_paid_invoices_for_client`(
	client_id INT,
   -- 因为需要输出发票数量和发票总金额,所以需要在参数前面添加OUT关键字前缀,表示输出参数
   OUT invoice_count INT,
   OUT invoice_total DECIMAL(9,2)
)
BEGIN
    -- 查询不同客户,且付款金额为0的发票个数和发票总金额
	 SELECT COUNT(*),SUM(invoice_total)
    -- 以下代码表示读取数据后,复制给下面这些输出参数
    INTO invoice_count,invoice_total
    FROM invoices i
    WHERE i.client_id = client_id
		AND payment_total = 0;
END
 
-- 调用get_paid_invoices_for_client过程
-- *用@符号作前缀定义变量,这里定义了两个变量,初始值设为0;
set @invoice_count = 0;
set @invoice_total = 0;
-- 当调用过程时,需要传递这些变量,(用来存储调用过程后产生的结果集(我的理解))
call sql_invoicing.get_paid_invoices_for_client
   -- 第一个参数是invoice_id为3;
	(3, @invoice_count, @invoice_total);
-- 调用后,需要用选择语句来读取这些值并显示。
select @invoice_count, @invoice_total;

9. 变量 - 通常在调用有输出参数的存储过程时使用这些变量

  • 使用场景:通常使用这些类型变量在存储过程中执行计算任务。
-- 两种变量
-- 第一种:用户或会话变量 - 在客户端会话过程中会被保存,但从MySQL断线后变量会被清空;
-- 定义变量:用SET语句定义,并用@符号作前缀 (如:set @invoice_count = 0;)
 
-- 第二种:本地变量 - 可以在存储过程或者函数内定义
-- 本地变量不会在整个客户端会话过程中被保存,一旦存储过程完成执行任务,这些变量就会被清空
-- 用DECLARE定义
 
-- 实现:定义风险因素 = 发票总金额 / 发票份数 * 5,并显示计算结果
CREATE DEFINER=`root`@`localhost` PROCEDURE `get_risk_factor`()
BEGIN
	-- 以下为在存储过程中声明并使用本地变量
   -- *DECLARE只能在BEGIN 和 END 之间使用
   -- 定义risk_factor为精准的浮点型,位数为9,小数位有2位,默认值为0;
	DECLARE risk_factor DECIMAL(9,2) DEFAULT 0;
   -- 注意这里的发票总金额名称要与发票表中的发票金额名称区分开来,不然结果为NULL(我自己试的是这样)
	DECLARE invoices_total DECIMAL(9,2);
   DECLARE invoices_count INT;
    
	-- 把发票份数和发票总金额给到变量
    SELECT COUNT(*),SUM(invoice_total)
    INTO invoices_count,invoices_total
    FROM invoices;  
    
    -- 计算risk_factor
    SET risk_factor = invoices_total / invoices_count * 5;
    
    -- 获取并显示risk_factor
    SELECT risk_factor;
END

10. 函数 - 与存储过程很像,但只能返回单一值

  • 函数和存储过程的区别:函数只能返回单一值,存储过程可以返回多行和多列的结果集。
  • 我们自己创建了函数后,可以在选择语句中使用。
-- 实现:将风险因素创建为函数,进行调用
-- 以下是直接在工作台创建的函数
CREATE DEFINER=`root`@`localhost` FUNCTION `get_risk_factor_for_client`(
	client_id INT
) 
-- RETURNS后面接的是数据类型,这里是整型,也可以换成MySQL其他的数据类型;
RETURNS int(11)
-- RETURN函数后面紧接着就要设置函数属性,每个MySQL都至少要有一个属性
-- 这里只需要读取数据库中的数据,所以只添加了读取属性即可;
    READS SQL DATA
-- 除此之外,MySQL还有很多函数属性
-- 如 DETERMINISTIC 确定性 意思是给这个函数的是同样的一组值,那么返回值也永远是一样的值;(没看懂,我理解的是,这组值是确定的,所以给的一直都是这组值,所以是同样的一组值)
-- 使用场景:不想根据数据库中的数据返回值的时候可以使用;
-- 如 MODIFIES SQL DATA 修改SQL数据 意思是函数中有插入、更新或者删除函数;
BEGIN
	DECLARE risk_factor DECIMAL(9,2) DEFAULT 0;
   -- 注意这里的发票总金额名称要与发票表中的发票金额名称区分开来,不然结果为NULL
	DECLARE invoices_total DECIMAL(9,2);
   DECLARE invoices_count INT;
    
	-- 把发票份数和发票总金额给到变量
   SELECT COUNT(*),SUM(invoice_total)
   INTO invoices_count,invoices_total
   FROM invoices i
   -- 这里要根据客户id来计算每个客户的风险因素
   WHERE i.client_id = client_id;
    
   SET risk_factor = invoices_total / invoices_count * 5;
    
	-- 函数最后需要一个返回值,这里是返回计算的风险因素risk_factor
   -- 因为该客户无发票时,他的COUNT(*) = 0,SUM(invoice_total) = NULL
   -- NULL / 0 = NULL,所以我们希望risk_factor为NULL时,输出为0,因此可以使用IFNULL()
	RETURN IFNULL(risk_factor, 0);
END
 

-- 实现:查询在客户表中客户id、姓名对应的风险因素
SELECT
	 client_id,
    name,
    get_risk_factor_for_client(client_id) AS risk_factor
FROM clients;
 

-- 删除函数
DROP FUNCTION IF EXISTS get_risk_factor_for_client;

11. 其他约定

-- 函数、存储过程的命名有很多种,以下列举可以有哪些命名约定:
-- 1)加前缀,函数加fn,存储过程加proc
-- 2)小驼峰,如函数fnGetRiskFactor,存储过程procGetRiskFactor
-- 3)加下划线,如函数fn_get_risk_factor,存储过程proc_get_risk_factor
-- 4)或者有人不喜欢加前缀,直接写名字,遵循当前公司的约定即可。
 
-- 存储过程中需要添加默认分隔符DELIMITER 有$$ 或者 // 或者其他,也是遵循当前公司的约定即可。