MySQL學習筆記(三):存儲過程

一、存儲過程概念

  • 什么是存儲過程

  • 一組為了完成特定功能的SQL 語句集。
    更加直白的理解:存儲過程可以說是一個記錄集,它是由一些T-SQL語句組成的代碼塊,這些T-SQL語句代碼像一個方法一樣實現一些功能(對單表或多表的增刪改查),然后再給這個代碼塊取一個名字,在用到這個功能的時候調用他就行了。

  • 優點

  • 存儲過程是一個預編譯的代碼塊,執行效率比較高

  • 一個存儲過程替代大量T_SQL語句 ,可以降低網絡通信量,提高通信速率

  • 可以一定程度上確保數據安全(可設定只有某些用戶才具有對指定存儲過程的使用權)

  • 缺點

  • 如果更改范圍大到需要對輸入存儲過程的參數進行更改,或者要更改由其返回的數據,則您仍需要更新程序集中的代碼以添加參數、更新 GetValue() 調用,等等,這時候估計比較繁瑣了。

  • 可移植性差

  • 很多存儲過程不支持面向對象的設計,無法采用面向對象的方式將業務邏輯進行封裝,從而無法形成通用的可支持復用的業務邏輯框架。

  • 代碼可讀性差,相當難維護。

  • 要不要用存儲過程
    個人覺得各組件應該各司其職。MySQL就是存儲數據的倉庫,邏輯實現還是應該放在業務層。
    建議看看別人說的。為什么要用存儲過程


二、存儲過程使用

存儲過程的創建語法:
DELIMITER //
CREATE PROCEDURE 儲存過程名([in|out|inout] 參數 datatype)
BEGIN
SQL語句代碼快
END
//
DELIMITER ;

幾點說明:

  • 存儲過程名字后面的“()”是必須的,即使沒有一個參數,也需要“()”
  • begin end 可以在只有一條sql語句的時候省略。
  • 每條語句的末尾,都要加上分號 “;”
  • 不能在存儲過程中使用 “return” 關鍵字。
  • DELIMITER 的意思是,告訴mysql,下面的語句中,語句定界符不再是分號(;),而是雙斜線(//)。這里的雙斜線可以任意指定,比如指定為|或者///,都是可以的(但不要指定為sql語句中經常出現的逗號或等號等,不然就結束了),如:
mysql> delimiter // 
mysql> create procedure simpleproc (out param1 int) 
    -> begin 
    -> select count(*) into param1 from t; 
    -> end 
    -> //
Query OK, 0 rows affected

mysql> delimiter |
mysql> create procedure simpleproc1 (out param1 int) 
    -> begin 
    -> select count(*) into param1 from t;
    -> end 
    -> |
Query OK, 0 rows affected

mysql> delimiter ///
mysql> create procedure simpleproc2 (out param1 int)
    -> begin 
    -> select count(*) into param1 from t;
    -> end
    -> ///
Query OK, 0 rows affected

還要注意,定義完存儲過程之后,要重新將分號(;)指定為語義分隔符。即調用DELIMITER ;

  • 關于參數

  • in表示向存儲過程傳遞參數,out表示從存儲過程返回參數,而inout表示傳遞參數和返回參數;如果不顯式指定in、out、inout,則默認為in。習慣上,對于是in的參數,我們都不會顯式指定;

  • 參數只能指定參數類型,不能指定長度;

  • 參數不能指定默認值。

  • 關于注釋

/*   
這是   
多行 
注釋
*/
declare a int; -- 這是單行 MySQL 注釋 (注意 -- 后至少要有一個空格)   
if a is null then set a = 0; # 這也是個單行 MySQL 注釋   
查看已經創建的存儲過程:

show procedure status where Db='數據庫名';或者:先use 數據庫名,再show procedure status

mysql> show procedure status where Db='cpgl';
+------+--------+-----------+----------------+---------------------+---------------------+---------------+---------+----------------------+----------------------+--------------------+
| Db   | Name   | Type      | Definer        | Modified            | Created             | Security_type | Comment | character_set_client | collation_connection | Database Collation |
+------+--------+-----------+----------------+---------------------+---------------------+---------------+---------+----------------------+----------------------+--------------------+
| cpgl | hi     | PROCEDURE | root@localhost | 2017-05-30 11:39:44 | 2017-05-30 11:39:44 | DEFINER       |         | utf8                 | utf8_general_ci      | utf8_general_ci    |
| cpgl | pr_add | PROCEDURE | root@localhost | 2017-05-30 11:41:42 | 2017-05-30 11:41:42 | DEFINER       |         | utf8                 | utf8_general_ci      | utf8_general_ci    |
+------+--------+-----------+----------------+---------------------+---------------------+---------------+---------+----------------------+----------------------+--------------------+
2 rows in set
刪除存儲過程

drop procedure '存儲過程名字';

mysql> drop procedure hi;
Query OK, 0 rows affected
實例

從網上找了幾個例子,真正用到的時候,可以參考:包含了事務,參數,嵌套調用,游標,循環等使用

例子1

drop procedure if exists pro_rep_shadow_rs;   
delimiter |   
----------------------------------   
-- rep_shadow_rs   
-- 用來處理信息的增加,更新和刪除   
-- 每次只更新上次以來沒有做過的數據   
-- 根據不同的標志位   
-- 需要一個輸出的參數,   
-- 如果返回為0,則調用失敗,事務回滾   
-- 如果返回為1,調用成功,事務提交   
--   
-- 測試方法   
-- call pro_rep_shadow_rs(@rtn);   
-- select @rtn;   
----------------------------------   
create procedure pro_rep_shadow_rs(out rtn int)   
begin   
    -- 聲明變量,所有的聲明必須在非聲明的語句前面   
    declare iLast_rep_sync_id int default -1;   
    declare iMax_rep_sync_id int default -1;   
    -- 如果出現異常,或自動處理并rollback,但不再通知調用方了   
    -- 如果希望應用獲得異常,需要將下面這一句,以及啟動事務和提交事務的語句全部去掉   
    declare exit handler for sqlexception rollback;   
    -- 查找上一次的   
    select eid into iLast_rep_sync_id from rep_de_proc_log where tbl='rep_shadow_rs';   
    -- 如果不存在,則增加一行   
    if iLast_rep_sync_id=-1 then   
      insert into rep_de_proc_log(rid,eid,tbl) values(0,0,'rep_shadow_rs');   
      set iLast_rep_sync_id = 0;   
    end if;   
       
    -- 下一個數字   
    set iLast_rep_sync_id=iLast_rep_sync_id+1;   
    -- 設置默認的返回值為0:失敗   
    set rtn=0;   
       
    -- 啟動事務   
    start transaction;   
    -- 查找最大編號   
    select max(rep_sync_id) into iMax_rep_sync_id from rep_shadow_rs;   
    -- 有新數據   
    if iMax_rep_sync_id>=iLast_rep_sync_id then   
        -- 調用   
        call pro_rep_shadow_rs_do(iLast_rep_sync_id,iMax_rep_sync_id);   
        -- 更新日志   
        update rep_de_proc_log set rid=iLast_rep_sync_id,eid=iMax_rep_sync_id where tbl='rep_shadow_rs';   
    end if;   
       
    -- 運行沒有異常,提交事務   
    commit;   
    -- 設置返回值為1  
    set rtn=1;   
end;   
|   
delimiter ;   

例子2

drop procedure if exists pro_rep_shadow_rs_do;   
delimiter |   
---------------------------------   
-- 處理指定編號范圍內的數據   
-- 需要輸入2個參數   
-- last_rep_sync_id 是編號的最小值   
-- max_rep_sync_id 是編號的最大值   
-- 無返回值   
---------------------------------   
create procedure pro_rep_shadow_rs_do(last_rep_sync_id int, max_rep_sync_id int)   
begin   
    declare iRep_operationtype varchar(1);   
    declare iRep_status varchar(1);   
    declare iRep_Sync_id int;   
    declare iId int;   
    -- 這個用于處理游標到達最后一行的情況   
    declare stop int default 0;   
    -- 聲明游標   
    declare cur cursor for select id,Rep_operationtype,iRep_status,rep_sync_id from rep_shadow_rs where rep_sync_id between last_rep_sync_id and max_rep_sync_id;   
    -- 聲明游標的異常處理,設置一個終止標記   
    declare CONTINUE HANDLER FOR SQLSTATE '02000' SET stop=1;   
       
    -- 打開游標   
    open cur;   
       
    -- 讀取一行數據到變量   
    fetch cur into iId,iRep_operationtype,iRep_status,iRep_Sync_id;   
    -- 這個就是判斷是否游標已經到達了最后   
    while stop <> 1 do  
        -- 各種判斷   
        if iRep_operationtype='I' then   
            insert into rs0811 (id,fnbm) select id,fnbm from rep_shadow_rs where rep_sync_id=iRep_sync_id;   
        elseif iRep_operationtype='U' then   
        begin   
            if iRep_status='A' then   
                insert into rs0811 (id,fnbm) select id,fnbm from rep_shadow_rs where rep_sync_id=iRep_sync_id;   
            elseif iRep_status='B' then   
                delete from rs0811 where id=iId;   
            end if;   
        end;   
        elseif iRep_operationtype='D' then   
            delete from rs0811 where id=iId;   
        end if;    
           
        -- 讀取下一行的數據    
        fetch cur into iId,iRep_operationtype,iRep_status,iRep_Sync_id;   
    end while;  -- 循環結束   
    close cur; -- 關閉游標   
 end;   
|  
delimiter ;

關于游標定義和使用,可以參考:MySql存儲過程—游標(Cursor)

  • 游標循環次數不對的問題
    在使用游標的時候,最容易出現的問題就是循環次數與實際記錄數不一:第一種就是循環次數比實際記錄數多一次;第二種情況就是循環次數遠小于記錄數。
    第一種情況的發生,是由于對while do結構(或者有時候使用repeat)以及cursor的性質認識不夠導致的,cursor會在找不到記錄時(一般是循環完了最后一行,也有可能是循環時的select into 賦值語句結果為空導致)發生NOT FOUND,可以根據這一點來判斷循環結束。而while do 結構類似于java中的do while 結構,無論怎樣都會先do再判斷。上面例子2中的寫法可以很好的解決這個問題:在每次循環結尾,執行賦值操作,相當于讓cursor的判斷提前一步。
    第二種情況的發生,一般是select into 賦值語句結果為空(有待研究)。

  • 存儲過程如何調試
    我使用的有兩種:
    第一種是在call調用存儲過程之前,定義全局變量,如SET @test=0;在存儲過程的函數體中的某個地方,為該變量賦值,如SET @test=1;在調用完存儲過程之后,使用select @test的方式查看之前定義的@test的值。如果存儲過程寫的沒問題,執行到了你寫SET @test=1;的地方,則值為1,否則還是初始值0。
    第二種則是,在函數體中,使用select var1,var2...這種方式:var1,var2...是你在其中定義的變量,你想在某處查看它們的值是否與你期望的一樣,就在該處寫上select var1,var2...語句。這樣,在控制臺調用存儲過程時,就會打印出這樣變量的值。
    下面的存儲過程可供參考:

CREATE PROCEDURE copy_nfi_item(nfi_id_from varchar(100), nfi_id_to varchar(100))
    COMMENT '復制nfi考核,與業務無關;如果兩個部門的nfi考核項相同,可以通過該存儲過程快捷復制。'
BEGIN
        
    DECLARE v_item_id varchar(50);
    DECLARE v_percent float(11,0);
    DECLARE v_first_level_duty varchar(255);
    DECLARE v_first_level_duty_weight float(11,0);
    DECLARE v_title varchar(255);
    DECLARE v_task text;
    DECLARE v_task_weight float(11,0);
    DECLARE v_plan_begin_date date;
    DECLARE v_plan_end_date date;
    DECLARE v_complete_date date;
    DECLARE v_results text;
    DECLARE v_scoring_standard text;
    DECLARE v_results_desc text;
    DECLARE v_provide_dept varchar(255);
    DECLARE v_check_dept varchar(255);
    DECLARE v_self_marks float(11,0);
    DECLARE v_self_score float(11,2);
    DECLARE v_dept_marks float(11,0);
    DECLARE v_dept_score float(11,2);
    DECLARE v_nonfinancial_score float(11,2);
    DECLARE v_president_score float(11,2);
    DECLARE v_review_marks float(11,2);
    DECLARE v_review_score float(11,2);
    DECLARE v_advice text;
    DECLARE v_nfi_id varchar(50);
    DECLARE v_executor_id varchar(50);
    DECLARE v_status int(11);
    DECLARE v_step int(11);
    DECLARE v_dr bit(1);
    DECLARE v_ts datetime;
    DECLARE v_improvements text;
    DECLARE v_update_ts varchar(50);

    DECLARE temp_dept_id_from VARCHAR(200);
    DECLARE temp_dept_id_to VARCHAR(200);
    DECLARE v_count int(10) DEFAULT 0;


    -- 這個用于處理游標到達最后一行的情況
    DECLARE stopFlag int default 0;
     
    -- 聲明游標:指向數據源  
    DECLARE cur CURSOR FOR SELECT item.item_id,item.percent,item.first_level_duty,item.first_level_duty_weight,item.title,item.task,item.task_weight,item.plan_begin_date,item.plan_end_date,
                item.complete_date,item.results,item.scoring_standard,item.results_desc,item.provide_dept,item.check_dept,item.self_marks,item.self_score,item.dept_marks,item.dept_score,item.review_marks,item.review_score,
                item.advice,item.nfi_id,item.status,item.step,item.dr,item.ts,item.executor_id,item.nonfinancial_score,item.president_score,item.improvements FROM nfi_item item WHERE item.nfi_id = nfi_id_from;

    -- 聲明游標的異常處理,設置一個終止標記 
    -- 另一種寫法:DECLARE CONTINUE HANDLER FOR NOT FOUND SET stop=1;   
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET stopFlag=1;  


    -- 打開游標   
    OPEN cur;


    /**

    使用游標的一個常見問題就是循環次數不對。這樣可以正確。
    mysql的while與java里的類似,會先進入其中,do之后,再判斷。
    在循環體的最后使用fetch,是為了讓游標提前更進一步,使得stopFlag=1。

    */
-- 讀取一行數據到變量   
        FETCH cur INTO v_item_id,v_percent,v_first_level_duty,v_first_level_duty_weight,v_title,v_task,v_task_weight,v_plan_begin_date,v_plan_end_date,v_complete_date,v_results,
                v_scoring_standard,v_results_desc,v_provide_dept,v_check_dept,v_self_marks,v_self_score,v_dept_marks,v_dept_score,v_review_marks,v_review_score,v_advice,v_nfi_id,v_status,v_step,
                v_dr,v_ts,v_executor_id,v_nonfinancial_score,v_president_score,v_improvements;   

    -- 判斷是否游標已經到達了最后
    WHILE stopFlag <> 1 DO
            
            SET v_count = v_count + 1;

            -- 測試用,這樣的語法會在控制臺直接輸出變量的值
            SELECT v_item_id,v_percent, temp_dept_id_from, temp_dept_id_to, v_count;

            -- 幾個特殊的變量重新賦值:使用SELECT 表中字段名 INTO 變量名的方式 
            SELECT n.dept_id INTO temp_dept_id_from FROM nfi n WHERE n.nfi_id = nfi_id_from;
            SELECT n.dept_id INTO temp_dept_id_to FROM nfi n WHERE n.nfi_id = nfi_id_to;

            IF temp_dept_id_from = v_provide_dept THEN SET v_provide_dept = temp_dept_id_to;
            END IF;

            SET v_item_id = CONCAT(SUBSTR(DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'),3),SUBSTR(UUID(),1,8));
            SET v_nfi_id = nfi_id_to;

            SELECT n.psn_code INTO v_executor_id FROM nfi n WHERE n.nfi_id = nfi_id_to;

            -- 執行插入
            INSERT INTO nfi_item
                    (item_id,percent,first_level_duty,first_level_duty_weight,title,task,task_weight,plan_begin_date,plan_end_date,complete_date,
                    results,scoring_standard,results_desc,provide_dept,check_dept,self_marks,self_score,dept_marks,dept_score,review_marks,review_score,
                    advice,nfi_id,status,step,dr,ts,executor_id,nonfinancial_score,president_score,improvements)
            VALUES(
                    v_item_id,v_percent,v_first_level_duty,v_first_level_duty_weight,v_title,v_task,v_task_weight,v_plan_begin_date,v_plan_end_date,v_complete_date,v_results,
                    v_scoring_standard,v_results_desc,v_provide_dept,v_check_dept,v_self_marks,v_self_score,v_dept_marks,v_dept_score,v_review_marks,v_review_score,v_advice,v_nfi_id,v_status,v_step,
                    v_dr,v_ts,v_executor_id,v_nonfinancial_score,v_president_score,v_improvements);

       -- 讀取一行數據到變量   
         FETCH cur INTO v_item_id,v_percent,v_first_level_duty,v_first_level_duty_weight,v_title,v_task,v_task_weight,v_plan_begin_date,v_plan_end_date,v_complete_date,v_results,
                    v_scoring_standard,v_results_desc,v_provide_dept,v_check_dept,v_self_marks,v_self_score,v_dept_marks,v_dept_score,v_review_marks,v_review_score,v_advice,v_nfi_id,v_status,v_step,
                    v_dr,v_ts,v_executor_id,v_nonfinancial_score,v_president_score,v_improvements;
    END WHILE;  -- 循環結束   
    CLOSE cur; -- 關閉游標   
END
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容