一、綜述
今天在PostgreSQL遇到一個奇怪的現象,簡而言之,是想用函數(存儲過程)實現插入記錄,整個過程沒報錯但事后卻沒找到記錄!忙活半天,才發現原因是PostgreSQL函數(存儲過程)有自動COMMIT或ROLLBACK的特殊規定。
二、問題重現
以下用示例表和示例代碼來重現該問題。
create table t1 ( ID int not null primary key, name varchar(20) );
涉及的存儲過程是從oracle那邊直接拷貝過來后再修改過的,原先是動態SQL,這里簡化為靜態SQL。注意其中有個commit;根據PostgreSQL的要求,對事務增加begin...exception...end,否則會有錯誤或警告。示例腳本代碼為:
create or replace function p1(pid int, pname varchar) returns void as $$ begin begin --pg對事務的要求 insert into t1 values(pid, pname); commit; exception when others then end; --pg對事務的要求 end; $$ language plpgsql;
依次執行腳本創建存儲過程、調用存儲過程、查找示例表,結果如下:
postgres=# \i test1.sql CREATE FUNCTION postgres=# select p1(1, 'abc'); p1 ---- (1 行記錄) postgres=# select * from t1;
id | name
----+------
(0 行記錄)
要插入的記錄並不存在!驚喜不驚喜?意外不意外?
三、原因分析及解決
仔細查找有關資料,發現有這么一個解釋:
Functions and trigger procedures are always executed within a transaction established by an outer
query — they cannot start or commit that transaction, since there would be no context for them to
execute in. However, a block containing an EXCEPTION clause effectively forms a subtransaction that
can be rolled back without affecting the outer transaction.
其意義是PostgreSQL的函數總是默認為一個事務,總是自動Commit或Rollback。
其實一開始沒增加begin...exception...end時,PostgreSQL報錯“can't begin/end transaction in pl/pgsql”,已經隱含了這層信息。只是腦子里還是延續Oracle的習慣,而畫蛇添足了。
於是,修改存儲過程的腳本,按最簡單的法子來:
create or replace function p2(pid int, pname varchar) returns void as $$ begin insert into t1 values(pid, pname); end; $$ language plpgsql;
為驗證此說法是否正確,在再次創建函數、調用函數后,增加一個回滾(事先已設置AutoCommit為false)的操作,然后再查詢記錄:
postgres=# \i test1.sql CREATE FUNCTION postgres=# select p2(1, 'abc'); p2 ---- (1 行記錄) postgres=# rollback; WARNING: there is no transaction in progress ROLLBACK postgres=# select * from t1; id | name ----+------ 1 | abc (1 行記錄)
可見,這次記錄已成功插入,且外部的回滾操作對其無影響。
四、總結
Oracle是可以在存儲過程或函數里指定Commit/Rollback的,如果沒有,則外部調用者可以回滾存儲過程內部的操作。
但在PostgreSQL,函數(存儲過程)總是自動將其所有操作當作一個事務,外部無法對內部操作提交或回滾。
問題好像已經解決,但留有一個疑問沒弄明白,為什么PostgreSQL允許在函數體中加關於事務的begin...exception...end,但結果卻好像是沒提交?