有朋友问怎么实现存储过程中的SAVEPOINT,直接调用是不可以的,因为PG不允许事务控制语句出现在存储过程中,但我们还是有办法做到一定程度的模拟。
1、用BEGIN块模拟
随便建个表:
CREATE TABLE a(col1 int);
存储过程如下:
CREATE OR REPLACE FUNCTION ins_t() RETURNS void
AS $$
BEGIN
INSERT INTO a VALUES(100);
BEGIN
INSERT INTO a VALUES(200);
RAISE 'any error';
EXCEPTION
WHEN others THEN
null;
END;
END;
$$ LANGUAGE plpgsql;
测试:
flying=# SELECT ins_t(); ins_t
-------
(1 row)
flying=# SELECT * FROM a;
col1
------
100
(1 row)
可以看到,内嵌BEGIN块中的INSERT操作并没有生效。
2、PL/pgSQL如何实现
块定义在pl_gram.y中,有兴趣可以自己去看,块的执行代码位于 src/pl/plpgsql/src/pl_exec.c。
static int
exec_stmt_block(PLpgSQL_execstate *estate, PLpgSQL_stmt_block *block)
{
...
if (block->exceptions)
{
...
BeginInternalSubTransaction(NULL);
...
PG_TRY();
...
PG_CATCH();
...
RollbackAndReleaseCurrentSubTransaction();
...
只有当块定义有EXCEPTION部分,才会出现创建子事务的动作,否则(它对应的else分支内)只会当做普通语句处理。抛出例外时,就把这个子事务会滚掉。
而如果不抛出例外,而是WARNING或者NOTICE之类,也不会引起它的回滚,尝试一下就知道。PG怎么知道要不要回滚,关键在PG_TRY,这个说起来话长,暂时了解到这里足够。
3、没有EXCEPTION处理会怎样
去掉例外处理部分
CREATE OR REPLACE FUNCTION ins_t() RETURNS void
AS $$
BEGIN
INSERT INTO a VALUES(100);
BEGIN
INSERT INTO a VALUES(200);
RAISE 'any error';
END;
END;
$$ LANGUAGE plpgsql;
结果什么都没有,因为它没有启用子事务,所以会全部回滚。
flying=# select ins_t();
ERROR: any error
CONTEXT: PL/pgSQL function ins_t() line 6 at RAISE
flying=# select * from a;
col1
------
(0 rows)
flying=#
4、神奇的PG_TRY和PG_CATCH
它们用到的是sigsetjmp,PG中很多地方用到它,了解这个可能需要一定的汇编语言知识,也需要了解错误抛出机制,有机会再详细讲。
本篇主要是讲在不能使用事务控制语句的情况下,模拟实现ROLLBACK TO SAVEPOINT xxx,以及内部怎样实现,到这里也就结束了,更多细节以后有机会再讲。
从这里也可以看出,PG里边有很多很多值得挖掘的点,所以,没事儿就读代码,一定有惊喜。
上边的过程也很清楚看出,如果需要修改代码实现自己的某个功能,那么:(1)、找到某个现有的相似功能;(2)、阅读关联代码并理解;(3)、能满足自己的需要,调查结束动手实现;(4)、不符合要求返回(1)。为什么要这么做,因为我们并没有PG的开发文档,很难了解里边的API(就像上边的子事务控制)定义,以我的经验这是很好的途径。但这也是有前提的,要么本身很熟悉PG的操作,要么身边有这么一个人,不然也无从下手。如果各位有好的经验,欢迎分享给我。
欢迎关注公众号,文章同步