1、存储过程简介
(1)存储过程:是为以后使用而保存的一条或多条SQL语句或函数。可以将它视为批文件,不过它的作用不仅仅限于批处理;通常被称为函数或子程序。
(2)支持存储过程的DBMS:Oracle、SQL Server、MySQL 5及更高版本;而Microsoft Access和SQLite不支持存储过程。
(3)存储过程的优点:简单、安全、高性能
- 把处理封装在一个易用的单元中,简化了复杂的操作,实现过程化编程
- 不用反复建立一系列处理步骤,保证数据的一致性,防止错误
- 简化对变动的管理,以达到安全性;通过存储过程限制对基础数据的访问,减少了数据讹误的机会
- 存储过程通常以编译过的形式存储,所以DBMS处理命令所需的工作量少,提高了性能;也即是说存储过程的语句已经保存在数据库里了,语句已经被解析过了,以可执行格式存在。
- 存储过程可以利用SQL元素和特性来编写功能更强大更灵活的代码
(4)存储过程的缺陷:
- 不同DBMS中的存储过程语法有所不同,编写真正的可移植存储过程几乎是不可能的,不过存储过程的自我调用可以相对保持可移植
- 编写存储过程比编写基本SQL语句复杂,需要更高的技能,更丰富的经验
(5)存储过程的创建
create procedure procedure_name(in arg1 type1, out argN typeN) begin ... end
存储过程可以定义输入参数,用in关键字来表示;也可以定义输出参数,用out关键字来表示。存储过程的业务代码都放置在begin和end语句中,并且每天语句的结束法默认是分号;通过declare来声明变量,并且所有的变量声明都要放在代码块中的开头;通过set或者select...into来给变量赋值;此外,也可以通过select语句显示返回的值。整体来说,重要掌握了存储过程的相关语法(可以参考该链接),撰写业务逻辑代码还是不难的。
(6)存储过程的执行
-- MySQL 执行存储过程 CALL procedure_name([param1 [, ...]]) -- SQL Server 执行存储过程 EXECUTE [ @RETURN STATUS =] procedure_name [[[@param1_name = ] VALUE | [@param2_name = ] @VARIABLE [ OUTPUT ]] [WITH RECOMPILE] -- Oracle 执行存储过程 EXECUTE [ @RETURN STATUS =] procedure_name [[[@param1_name = ] VALUE | [@param2_name = ] @VARIABLE [ OUTPUT ]] [WITH RECOMPILE]
在MySQL中执行存储过程的方式是:call database_name.procedure_name(arg1,...,argN),其实也可以不用数据库名,直接用存储过程名,即call procedure_name(arg1,...,argN)
(7)代码注释
在写存储过程中应该添加适当的注释,这样更容易地理解和更安全地修改代码;增加注释不影响性能,也不存在缺陷。对代码行进行注释的标准方式是在之前放置两个连字符(--),注意了连字符和注释内容之间至少要隔一个空格;所有的DBMS都支持--连字符进行注释。MySQL还支持井号(#)进行注释。
(8)触发器
触发器是特殊的存储过程,它在特定的数据库活动发生时自动执行。触发器可以与特定表上的insert、update和delete操作(或组合)相关联。在大多数情况下,触发器是很不错的函数,但是它会导致更多的I/O开销。
- 存储过程只是简单的存储SQL语句,触发器与单个的表相关联。
- 触发器的内容不能修改,只能替换或者重新创建它。
- 不同的DBMS,它们的触发器操作时机可能不同,有的是在特定操作执行之前执行,有的是在特定操作执行之后执行。
- 触发器内的代码数据访问权:
- insert操作中的所有新数据
- update操作中的所有新数据和旧数据
- delete操作中删除的数据
- 触发器的用途
- 保证数据一致。
- 基于某个表的变动在其他表上执行活动。
- 进行额外的验证并根据需要回退数据。
- 计算列的值或更新时间戳。
- 一般来说,约束的处理比触发器快,应尽量使用约束。
2、游标简介
(1)游标:也称为光标,是一个存储在DBMS服务器上的数据库查询,它不是一条select语句,而是被该语句检索出来的结果集。
(2)用途:对检索出来的数据进行行前进或者后退操作,主要用于交互式应用,如用户滚动屏幕上的数据
(3)特性:
- 能够标记游标为只读,使数据能读取,但不能更新和删除
- 能控制可以执行的定向操作(向前、向后、第一、最后、绝对位置、相对位置等)
- 能标记某些列为可编辑的,某些列为不可编辑的
- 规定范围,使游标对创建它的特定请求(如存储过程)或对所有请求可访问
- 只是DBMS对检索出的数据(而不是指出表中活动数据)进行复制,使数据在游标打开和访问期间不变化
(4)支持游标的DBMS:DB2、MariaDB、MySQL 5、SQL Server、SQLite、Oracle和PostgreSQL,而Microsoft Access不支持
(5)游标对基于Web的应用用处不大(ASP、ASP.NET、ColdFusion、PHP、Python、Ruby、JSP等),大多数Web应用开发人员不使用游标
(6)使用:
- 声明游标: DECLARE cursor_name CURSOR FOR SELECT * FROM table_name; // 还没有检索数据
-- MySQL游标的声明 DECLARE cursor_name CURSOR FOR select_statement -- SQL Server游标的声明 DECLARE cursor_name CURSOR FOR select_statement [FOR [READ ONLY | UPDATE {[co lumn_list]}]] -- Oracle游标的声明 DECLARE CORSOR cursor_name IS {select_statement}
- 打开游标:OPEN cursor_name; // 开始检索数据,即指定游标的SELECT语句被执行,并且查询的结果集被保存在内存里的特定区域。
-- MySQL打开游标 OPEN cursor_name -- SQL Server打开游标 OPEN cursor_name -- Oracle打开游标 OPEN cursor_name [param1 [, param2]]
- 获取数据:FETCH cursor_name into var1,var2,...,varn; // 当游标cursor_name检索完数据后,只有等到下一次fetch时才会触发结束的标志
-- MySQL游标获取数据 FETCH cursor_name INTO var1_name [, var2_name] ... -- SQL Server游标获取数据 FETCH NEXT FROM cursor_name [INTO fetch_list] -- Oracle游标获取数据 FETCH cursor_name {INTO : host_var1 [[INDICATOR] : indicator_var1] [, : host_var2 [[INDICATOR] : indicator_var2]] | USING DESCRIPTOR DESCRIPTOR}
- 关闭游标:CLOSE cursor_name;
-- MySQL关闭游标,会主动释放资源,所以不需要DEALLOCATE语句 CLOSE cursor_name -- SQL Server关闭游标和释放资源 CLOSE cursor_name DEALLOCATE cursor_name -- Oracle关闭游标,会主动释放资源,所以不需要DEALLOCATE语句 CLOSE cursor_name
3、存储过程代码示例
(1)主存储过程:该存储过程的名称为main_procedure_name,并且在该存储过程中调用另外一个存储过程,其名称为child_procedure_name
CREATE PROCEDURE `database_name`.`main_procedure_name`() begin declare tmp_id varchar(15); declare done int default false; declare tmp_cursor cursor for select distinct pk_id from database_name.table_name where `some_field` = 'some_value'; // 根据实际情况添加限定条件 declare continue handler for not found set done = true; open tmp_cursor; pk_id_loop:loop fetch tmp_cursor into tmp_id; if done then leave pk_id_loop; end if; call database_name.child_procedure_name(tmp_id); end loop pk_id_loop; close account_cursor; end
(2)子存储过程:该存储过程的名称为child_procedure_name,其伪代码流程如下所示
CREATE PROCEDURE `database_name`.`child_procedure_name`(in input_param varchar(15)) begin declare counts int default 0; declare cmp_result int default 0; declare cur_id varchar(20); declare cur_value varchar(30); declare pre_id varchar(20); declare pre_value varchar(30); declare next_id varchar(20); declare next_value varchar(30); declare done int default false; declare cursor_name cursor for select id, value from database_name.table_name where some_field = input_param order by another_field desc; declare continue handler for not found set done = true; select count(*) into counts from database_name.table_name where some_field = input_param; if counts = 1 then open cursor_name; fetch cursor_name into cur_id, cur_value; ... close cursor_name; elseif credit_count = 2 then open cursor_name; fetch cursor_name into cur_id, cur_value; fetch cursor_name into pr_id, pre_value; ... close cursor_name; elseif credit_count > 2 then open cursor_name; info_loop:loop fetch cursor_name into cur_id, cur_value; fetch cursor_name into pre_id, pre_value; ... set next_id = pre_id; set next_value = pre_value; leave info_loop; end loop info_loop; close cursor_name;; end if; end
子存储过程的改造,不用区分总记录数为1、2和更多条的情况,以下是改造后的伪代码流程:
CREATE PROCEDURE `database_name`.`child_procedure_name`(in input_param varchar(15)) begin declare counts int default 0; declare cmp_result int default 0; declare cur_id varchar(20); declare cur_value varchar(30); declare pre_id varchar(20); declare pre_value varchar(30); declare next_id varchar(20); declare next_value varchar(30); declare flag int default 0; declare done int default false; declare cursor_name cursor for select id, value from database_name.table_name where some_field = input_param order by another_field desc; declare continue handler for not found set done = true; select count(*) into counts from database_name.table_name where some_field = input_param; set flag = counts % 2; open cursor_name; info_loop:loop fetch cursor_name into cur_id, cur_value; fetch cursor_name into pre_id, pre_value; if done then # 只有一条记录数据 # 记录数据为偶数条 # 记录数据为奇数条 leave info_loop; end if; ... set next_id = pre_id; set next_value = pre_value; leave info_loop; end loop info_loop; close cursor_name;; end
-- 注意了,在跳出循环的地方需要进行commit提交操作
子存储过程再次改造,每次循环只获取一条数据,跟上一次保存的数据进行比较,伪代码如下所示:
CREATE PROCEDURE `database_name`.`child_procedure_name`(in input_param varchar(15)) begin declare cmp_result int default 0; declare cur_id varchar(20); declare cur_value varchar(30); declare pre_id varchar(20); declare pre_value varchar(30); declare next_id varchar(20); declare next_value varchar(30); declare done int default false; declare cursor_name cursor for select id, value from database_name.table_name where some_field = input_param order by another_field desc; declare continue handler for not found set done = true; open cursor_name; info_loop:loop fetch cursor_name into cur_id, cur_value; if done then ... leave info_loop; end if; ... set next_id = pre_id; set next_value = pre_value; leave info_loop; end loop info_loop; close cursor_name;; end
上述的存储过程代码只是一个业务逻辑思路过程,可供参考,比如可以用来循环处理或者比较相邻两条记录的数据。在业务处理模块中,每次循环取一条数据的逻辑代码要简单些,但是循环的次数较多,具体的循环次数为总记录数+1;而每次循环取两条数据的逻辑代码要复杂些,不过循环的次数是单条数据循环次数的一般,具体的循环次数为总记录数 / 2 + 1,时间效率更高些。