MySQL筆記(五)——存儲過程和函數、觸發器

存儲過程和函數

概覽

存儲過程和函數是事先編譯並存儲在數據庫中的一段SQL語句的集合。5.0版本開始支持。

存儲過程:無返回值,參數可使用IN、OUT和INOUT類型;

函數:必須有返回值,參數只能是IN類型。

優點

1、存儲過程和函數可重複使用,調用存儲過程和函數可簡化開發人員的工作量;

2、調用存儲過程和函數只需要傳遞存儲過程/函數名稱和參數即可,能夠減少數據在數據庫和應用服務器之間的傳輸;

3、安全性高,可設定用戶的使用權限。

主要操作

本筆記記錄的例子都是使用官方提供的demo庫sakila來進行練習的。

創建

創建存儲過程語法如下:

create

[definer = {user | current_user}]

procedure `sp_name` ([proc_parameter[, ...]])

[characteristic ...] `routine_body`;

創建函數語法與存儲過程類似,只是多了返回值類型的定義:

create

[definer = {user | current_user}]

function `func_name` ([func_parameter[, ...]])

returns type

[characteristic ...] `routine_body`;

definer:可選,指明存儲過程/函數創建用戶。

characteristic:特徵值,可選取值如下

language sql 系統默認,說明過程/函數body是以MySQL編寫。

deterministic/not deterministic 系統默認爲not deterministic。deterministic意爲確定的,即每次輸出一樣、輸出也一樣的程序。目前該特徵值還未被優化程序使用。

{ contains sql | no sql | reads sql data | modifies sql data } 程序使用數據的內在信息,目前這些特徵值只是提供給服務器,並沒有被用於約束實際使用數據的情況:

    contains sql:系統默認,表示程序不含讀/寫數據的語句;

    no sql:表示程序不包含SQL語句;

    reads sql data:表示程序只含有讀數據的語句,而沒有寫數據的語句;

    modifies sql data:表示程序含有寫數據的語句。

sql security { definer | invoker } 系統默認爲definer。用來約束程序調用時使用的是程序創建者的權限還是調用者的權限。如調用者對程序中涉及的表沒有相關權限,而該值爲invoker,則會調用失敗;若該值爲definer,則可正常調用。舉個例子:

先創建一個簡單的存儲過程,功能是向actor表中信息數據:

delimiter $$
create procedure actor_insert(p_first_name varchar(45), p_last_name varchar(45))
modifies sql data
sql security definer
begin
insert into actor(first_name, last_name)
values (p_first_name, p_last_name);
end $$
Query OK, 0 rows affected (0.00 sec)
delimiter ;

使用當前用戶測試一下存儲過程actor_insert:

call actor_insert('JIM', 'Carey');
Query OK, 1 row affected (0.01 sec)

插入成功。現在我們再創建一個對actor表沒有insert權限的用戶test,來調用這個存儲過程,因爲當前設置了sql security definer,因此調用能夠成功:

call sakila.actor_insert('xx', 'xx');
Query OK, 1 row affected (0.00 sec)

接下來修改存儲過程(alter procedure/function用於修改過程/函數的一些特徵值,如果要修改routine_body則需要刪除重新創建):

alter procedure actor_insert
sql security invoker;
Query OK, 0 rows affected (0.00 sec)

此時再調用一下:

call sakila.actor_insert('xx', 'xx');
ERROR 1142 (42000): INSERT command denied to user 'test'@'localhost' for table 'actor'

由於調用者test沒有actor表的insert權限,因此調用過程失敗。

查看

1、查看存儲過程/函數的狀態:show { procedure | function } status [like 'pattern'];

例子:查看actor_insert表狀態

show procedure status like 'actor_insert'\G;
*************************** 1. row ***************************
                  Db: sakila
                Name: actor_insert
                Type: PROCEDURE
             Definer: root@localhost
            Modified: 2020-02-09 22:46:32
             Created: 2020-02-09 17:53:05
       Security_type: INVOKER
             Comment: 
character_set_client: utf8
collation_connection: utf8_general_ci
  Database Collation: latin1_swedish_ci
1 row in set (0.00 sec)

2、查看存儲過程/函數的定義:show create { procedure | function } `sp_name`;

例子:查看actor_list表定義語句

show create procedure sakila.actor_insert\G;
*************************** 1. row ***************************
           Procedure: actor_insert
            sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
    Create Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `actor_insert`(p_first_name varchar(45), p_last_name varchar(45))
    MODIFIES SQL DATA
    SQL SECURITY INVOKER
begin
insert into actor(first_name, last_name)
values (p_first_name, p_last_name);
end
character_set_client: utf8
collation_connection: utf8_general_ci
  Database Collation: latin1_swedish_ci
1 row in set (0.00 sec)

3、所有的存儲過程/函數,都存放在information_schema庫的routines表中,可以通過routines表查詢存儲過程/函數信息:

select * from information_schema.routines where routine_name='actor_insert'\G;
*************************** 1. row ***************************
           SPECIFIC_NAME: actor_insert
         ROUTINE_CATALOG: def
          ROUTINE_SCHEMA: sakila
            ROUTINE_NAME: actor_insert
            ROUTINE_TYPE: PROCEDURE
               DATA_TYPE: 
CHARACTER_MAXIMUM_LENGTH: NULL
  CHARACTER_OCTET_LENGTH: NULL
       NUMERIC_PRECISION: NULL
           NUMERIC_SCALE: NULL
      DATETIME_PRECISION: NULL
      CHARACTER_SET_NAME: NULL
          COLLATION_NAME: NULL
          DTD_IDENTIFIER: NULL
            ROUTINE_BODY: SQL
      ROUTINE_DEFINITION: begin
insert into actor(first_name, last_name)
values (p_first_name, p_last_name);
end
           EXTERNAL_NAME: NULL
       EXTERNAL_LANGUAGE: NULL
         PARAMETER_STYLE: SQL
        IS_DETERMINISTIC: NO
         SQL_DATA_ACCESS: MODIFIES SQL DATA
                SQL_PATH: NULL
           SECURITY_TYPE: INVOKER
                 CREATED: 2020-02-09 17:53:05
            LAST_ALTERED: 2020-02-09 22:46:32
                SQL_MODE: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
         ROUTINE_COMMENT: 
                 DEFINER: root@localhost
    CHARACTER_SET_CLIENT: utf8
    COLLATION_CONNECTION: utf8_general_ci
      DATABASE_COLLATION: latin1_swedish_ci
1 row in set (0.01 sec)

刪除

刪除存儲過程/函數語法如下:

drop { procedure | function } [if exists] `sp_name`;

需要注意,刪除存儲過程/函數的用戶需要擁有該存儲過程/函數alter routine權限,且一次只能刪除一個存儲過程/函數

變量

變量定義

定義變量的語句如下:

declare var_name[, ...] var_type [default_value]

一次可定義多個相同類型的變量。變量的作用範圍限於begin...end塊中,且要位於塊中所有語句之前。

變量賦值

1、直接賦值,使用set,可以賦常量或表達式:

set `var_name`=expr [, `var_name` expr] ...;

2、通過查詢賦值(查詢的返回結果必須只有一行):

select `col_name`[, ...] into var_name[, ...] table_expr;

變量及其賦值可以參考sakila庫的inventory_in_stock函數:

BEGIN
    DECLARE v_rentals INT;
    DECLARE v_out     INT;


    SELECT COUNT(*) INTO v_rentals
    FROM rental
    WHERE inventory_id = p_inventory_id;
    ...

    SELECT COUNT(rental_id) INTO v_out
    FROM inventory LEFT JOIN rental USING(inventory_id)
    WHERE inventory.inventory_id = p_inventory_id
    AND rental.return_date IS NULL;
    ...
END

可以看到這個函數中,首先定義了變量v_rentals和v_out,因爲它們數據類型相同,這一句也可以改寫爲:

declare v_rentals, v_out int;

後面的語句都是通過查詢結果對這兩個變量賦值。從出租表rental中統計出庫存id與參數p_inventory_id相等的記錄數量即指定貨物出租的次數,並將其賦值給v_rentals;從庫存表inventory以及出租表rental中統計指定貨物出租且未歸還的次數,並賦值給v_out。

條件

條件的定義和處理,用於定義在語句執行過程中遇到問題時的處理方式,相當於Java中使用try...catch捕獲、處理異常。條件的定義及處理語法如下:

-- 相當於爲condition_value起別名爲condition_name
declare condition_name condition for condition_value;
-- handler_type即對於條件發生後的處理方式,取值有以下兩種:
--  1、continue:跳過條件發生的語句,繼續執行下一條
--  2、exit:整個程序終止執行
declare handler_type handler
    for condition_value[, ...]
    statement;

condition_value的取值有以下幾種:

1、MySQL錯誤碼

2、SQLState值

3、通過declare定義的condition_name

4、SQLWARNING,即01開頭的SQLState值,表示警告

5、NOT FOUND,即02開頭的SQLState值,表示無數據

6、SQLEXCEPTION,表示所有沒有被SQLWARNING和NOT FOUND捕獲的SQLState值

流程控制

if語句

還是以上文中出現過的sakila中的函數inventory_in_stock爲例:

BEGIN
    DECLARE v_rentals INT;
    DECLARE v_out     INT;
    ...
    IF v_rentals = 0 THEN
      RETURN TRUE;
    END IF;
    ...
    IF v_out > 0 THEN
      RETURN FALSE;
    ELSE
      RETURN TRUE;
    END IF;
END

可以看到,if語句實現條件判斷,與一般的編程語言類似,以if...end if包裹內容塊,語法可以概括爲:

if search_condition then statement_list
[elseif search_condition then statement_list] ...
[else statement_list]
end if

case語句

case語句也用於實現條件判斷,語法如下:

case case_value
when when_value then statement_list
[when when_value then statement_list] ...
end case

嘗試用case語句來改寫上文if語句中的示例代碼:

case v_rentals
when 0 then return true;
end case;

case v_out
when 0 the return true;
else return false;
end case;

在實際使用中,case更多被用於較爲複雜的條件判斷中,在有很多條件取值的情況下,case語句比if語句更爲清晰。

loop & leave語句

loop語句可以實現簡單的循環,退出循環則可以使用leave語句配合,語法如下:

[loop_label]:loop
statement_list
leave [loop_label]
end loop [loop_label]

做個簡單的練習,還是使用sakila庫,構造一個存儲過程,連續向actor表中插入10條數據:

USE `sakila`;
DROP procedure IF EXISTS `actor_insert`;

DELIMITER $$
USE `sakila`$$
CREATE PROCEDURE `actor_insert` ()
modifies sql data
BEGIN
set @i = 0;
ins: loop
if @i = 10 then
leave ins;
end if;
insert into actor(`first_name`, `last_name`) values ('test_loop', @i);
set @i = @i + 1;
end loop ins;
END$$

DELIMITER ;

最後可以看看過程中定義的@i的值是否爲10,即可判斷是否循環了10次:

 

select @i;
+------+
| @i   |
+------+
|   10 |
+------+
1 row in set (0.01 sec)

 

iterate語句

iterate用於循環中跳過當前循環剩下的語句、直接進入下一輪循環。重新定義一下上文中的過程actor_insert,使@i爲偶數時插入數據,否則不插入,@i最小爲0、最大爲10:

USE `sakila`;
DROP procedure IF EXISTS `actor_insert`;

DELIMITER $$
USE `sakila`$$
CREATE DEFINER=`root`@`%` PROCEDURE `actor_insert`()
    MODIFIES SQL DATA
BEGIN
set @i = 0;
ins: loop
set @i = @i + 1;
if @i > 10 then
leave ins;
end if;
if mod(@i, 2) = 0 then
iterate ins;
else
insert into actor(`first_name`, `last_name`) values ('test_iterate', @i);
end if;
end loop ins;
END$$

DELIMITER ;

repeat語句

repeat語句用於實現有條件控制的循環,當滿足條件時即可退出循環。語法如下:

[begin_label:] repeat
statement_list
util search_condition
end repeat [end_label];

我們還是以上文的過程actor_insert爲例,將其改爲用repeat語句控制循環:

USE `sakila`;
DROP procedure IF EXISTS `actor_insert`;

DELIMITER $$
USE `sakila`$$
CREATE DEFINER=`root`@`%` PROCEDURE `actor_insert`()
    MODIFIES SQL DATA
BEGIN
set @i = 0;
ins: repeat
insert into actor(`first_name`, `last_name`) values ('test_repeat', @i);
set @i = @i + 1;
until @i = 10
end repeat ins;
END$$

DELIMITER ;

while語句

while語句也是實現有條件控制的循環。與repeat語句相比,while語句相當於編程語言中的while...do循環,而repeat相當於while...do循環。語法如下:

[begin_label:] while search_condition do
statement_list
end while [end_label];

我們還是以上文的過程actor_insert爲例,將其改爲用while語句控制循環:

USE `sakila`;
DROP procedure IF EXISTS `actor_insert`;

DELIMITER $$
USE `sakila`$$
CREATE DEFINER=`root`@`%` PROCEDURE `actor_insert`()
    MODIFIES SQL DATA
BEGIN
set @i = 0;
ins: while @i < 10 do
insert into actor(`first_name`, `last_name`) values ('test_while', @i);
set @i = @i + 1;
end while ins;
END$$

DELIMITER ;

光標

光標,用於存儲過程/函數中,對結果集進行循環處理。

通過一個例子理解光標的使用。寫一個存儲過程,查詢payment表中的記錄,根據不同的staff_id來對各個員工的銷售額amount進行累加:

USE `sakila`;
DROP procedure IF EXISTS `payment_amount_count`;

DELIMITER $$
USE `sakila`$$
CREATE PROCEDURE `payment_amount_count` ()
reads sql data
BEGIN
declare i_staff_id int;
declare d_staff_amount decimal(5, 2);
declare cur_payment cursor for 
    select staff_id, amount from payment;
declare exit handler for not found
    close cur_payment;

set @staff1 = 1;
set @staff2 = 2;

open cur_payment;
repeat
fetch cur_payment into i_staff_id, d_staff_amount;
case i_staff_id
when 1 then set @staff1 = @staff1 + d_staff_amount;
when 2 then set @staff2 = @staff2 + d_staff_amount;
end case;
until 0
end repeat;
close cur_payment;
END$$

DELIMITER ;

select @staff1, @staff2;
+----------+----------+
| @staff1  | @staff2  |
+----------+----------+
| 33490.47 | 33924.05 |
+----------+----------+
1 row in set (0.00 sec)

上述存儲過程中,運用了很多上文記錄的內容,如變量的定義與賦值、條件處理、if語句、case語句和repeat語句。另一方面,可以通過該存儲過程,總結出光標的使用過程:

1、declare cur_name cursor for select_statement:聲明光標;

2、open cur_name:打開光標;

3、fetch cur_name into var_name[, ...]:移動光標;

4、close cur_name:關閉光標。

事件調度器

使用事件調度器,可以使數據庫安自定義的時間週期觸發某種操作,可以理解爲時間觸發器

事件調度器適用於定期收集統計信息、定期清理歷史數據、定期數據庫檢查,但是在繁忙且要求性能的數據庫服務器上要慎重部署和啓用,同時,開啓和關閉事件調度器需要超級用戶權限。

創建事件調度器的語法如下:

create event `event_name`
on schedule <schedule>
do
<event_body>;

舉個例子,創建一個test表,每隔兩秒向表中插入一條數據,一分鐘後停止插入:

首先創建test表:

create table test(
id int primary key auto_increment comment '主鍵',
last_update datetime not null comment '最後更新時間'
) charset utf8mb4;

接下來創建一個事件調度器,定時執行指定任務:

create event test_insert_event
on schedule
every 2 second ends current_timestamp + interval 1 minute
do
insert into test.test(last_update) value(now());

爲了任務調度器能夠執行,我們還需要開啓調度器:

set global event_scheduler=1;

此時就可以看看test表中是否有數據增長了。

另外,我們可以通過show events;語句來查看當前事件調度器信息:

show events\G;
*************************** 1. row ***************************
                  Db: test
                Name: test_insert_event
             Definer: root@localhost
           Time zone: SYSTEM
                Type: RECURRING
          Execute at: NULL
      Interval value: 2
      Interval field: SECOND
              Starts: 2020-02-12 00:42:46
                Ends: 2020-02-12 00:44:46
              Status: ENABLED
          Originator: 6
character_set_client: utf8
collation_connection: utf8_general_ci
  Database Collation: utf8mb4_general_ci
1 row in set (0.00 sec)

當任務調度器執行結束,該event會自動銷燬。如果想要提前結束事件,則可以通過以下語句:

-- 刪除
drop event event_name;
-- 或者,禁用
alter event event_name disable;

觸發器

觸發器是與表有關的數據庫對象,我們可以爲某張表定義觸發器,當滿足觸發器定義的條件時執行觸發器中定義的語句集合。相當於編程語言中的事件監聽與回調,而這裏的監聽對象是一張表。

查看觸發器

官方示例庫sakila中已經定義了數個觸發器,我們可以先查看一下這些觸發器。

查看數據庫中所有觸發器

show triggers\G;

這條語句可以用於查看數據庫中所有的觸發器信息,但不能查詢指定的觸發器。

查看指定觸發器

類似於視圖、存儲過程/函數,information_schema庫中也存放了所有的觸發器信息,具體是在triggers表中,可以通過指定觸發器名稱查詢該表,來查詢指定的觸發器。如查詢觸發器ins_film:

select * from information_schema.triggers where trigger_name='ins_film'\G;

查詢結果如下:

*************************** 1. row ***************************
           TRIGGER_CATALOG: def
            TRIGGER_SCHEMA: sakila
              TRIGGER_NAME: ins_film
        EVENT_MANIPULATION: INSERT
      EVENT_OBJECT_CATALOG: def
       EVENT_OBJECT_SCHEMA: sakila
        EVENT_OBJECT_TABLE: film
              ACTION_ORDER: 1
          ACTION_CONDITION: NULL
          ACTION_STATEMENT: BEGIN
    INSERT INTO film_text (film_id, title, description)
        VALUES (new.film_id, new.title, new.description);
  END
        ACTION_ORIENTATION: ROW
             ACTION_TIMING: AFTER
ACTION_REFERENCE_OLD_TABLE: NULL
ACTION_REFERENCE_NEW_TABLE: NULL
  ACTION_REFERENCE_OLD_ROW: OLD
  ACTION_REFERENCE_NEW_ROW: NEW
                   CREATED: 2020-02-03 23:36:41.96
                  SQL_MODE: STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,TRADITIONAL,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
                   DEFINER: root@localhost
      CHARACTER_SET_CLIENT: utf8mb4
      COLLATION_CONNECTION: utf8mb4_general_ci
        DATABASE_COLLATION: latin1_swedish_ci
1 row in set (0.01 sec)

創建觸發器

我們可以根據上文的ins_film這個觸發器,來總結創建觸發器的語法。

首先,觸發器需要一個作用對象,這個對象必須是一張表,即ins_film中的event_object_table: film;然後,還需要一個觸發的條件,即ins_film中的event_manipulation: insert;接着還要有觸發的時機,是在觸發條件之前呢還是之後,即ins_film中的action_timing: after;最後就是觸發器的SQL語句集了,可以看到ins_film的行爲是在有新的數據插入film表之後,將相同的數據也插入到film_text表。在SQL語句中,目標行爲新產生的記錄對象使用new表示,原先的記錄對象用old表示。還需要注意,MySQL觸發器僅支持行觸發

可以總結出創建觸發器的語法如下:

create trigger trigger_name
trigger_timing
trigger_event
on table_name
for each row
trigger_body;

trigger_timing 即觸發的時機,取值有before和after,表示在目標行爲之前執行SQL語句集還是之後;

trigger_event 即觸發行爲,取值有insert、update和delete。

作爲練習,創建觸發器test_insert_customer,當有新的記錄插入customer表時,將新紀錄的first_name、last_name和create_date列值寫入customer_added表中。

先創建customer_added表:

create table customer_added(
id int primary key auto_increment comment '主鍵',
first_name varchar(45) not null comment '名',
last_name varchar(45) not null comment '姓',
create_time datetime
) charset utf8mb4;

再創建觸發器test_insert_customer:

delimiter $$
create trigger test_insert_customer
after insert
on customer
for each rows
begin
insert into customer_added(first_name, last_name, create_time)
values(new.first_name, new.last_name, new.create_date);
end; $$
delimiter ;

最後驗證一下,向customer表中插入一條記錄:

insert into customer(`store_id`, `first_name`, `last_name`, `address_id`, `active`) values (1, 'Tom', 'Smith', 602, 1);
select * from customer where first_name='Tom' and last_name='Smith';
+-------------+----------+------------+-----------+-------+------------+--------+---------------------+---------------------+
| customer_id | store_id | first_name | last_name | email | address_id | active | create_date         | last_update         |
+-------------+----------+------------+-----------+-------+------------+--------+---------------------+---------------------+
|         602 |        1 | Tom        | Smith     | NULL  |        602 |      1 | 2020-02-12 15:47:58 | 2020-02-12 15:47:58 |
+-------------+----------+------------+-----------+-------+------------+--------+---------------------+---------------------+
1 row in set (0.00 sec)

檢查一下customer_added表:

select * from customer_added;
+----+------------+-----------+---------------------+
| id | first_name | last_name | create_time         |
+----+------------+-----------+---------------------+
|  1 | Tom        | Smith     | 2020-02-12 15:47:58 |
+----+------------+-----------+---------------------+
1 row in set (0.00 sec)

數據正確地插入了customer_added表,因此我們創建的觸發器無誤。

刪除觸發器

使用drop trigger一次可以刪除一個觸發器,未指定庫名的情況下默認是當前庫:

drop trigger [schema_name.]trigger_name;

例如刪除上文創建的觸發器test_insert_customer:

drop trigger sakila.test_insert_customer;
Query OK, 0 rows affected (0.00 sec)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章