子事务处于活动状态时无法回滚 - 错误 2D000
Cannot rollback while a subtransaction is active - Error 2D000
我编写了一个存储过程,它基本上循环遍历 fields
的数组,并在每次迭代时在数据库中执行一些操作。我想要实现的是,要么循环的所有迭代都应该发生,要么它们都不应该发生。
假设字段数组中有 5 个元素,循环迭代到第 3 个元素,然后发现某些条件为真并引发错误,我想回滚前 2 个期间发生的所有更改迭代。我已经使用 ROLLBACK
语句来实现相同的目的,但是每次它到达 ROLLBACK
语句时都会抛出以下错误:
Cannot rollback while a subtransaction is active : 2D000
令人惊讶的是,如果我在 EXCEPTION WHEN OTHERS THEN
块中注释掉 outobj := json_build_object('code',0);
语句或者完全删除整个块,它会正常工作。
我检查了 PostgreSQL documentation for error codes,但它并没有真正帮助。我的存储过程如下:
CREATE OR REPLACE PROCEDURE public.usp_add_fields(
field_data json,
INOUT outobj json DEFAULT NULL::json)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
v_user_id bigint;
farm_and_bussiness json;
_field_obj json;
_are_wells_inserted boolean;
BEGIN
-- get user id
v_user_id = ___uf_get_user_id(json_extract_path_text(field_data,'user_email'));
IF(v_user_id IS NULL) THEN
outobj := json_build_object('code',17);
RETURN;
END IF;
-- Loop over entities to create farms & businesses
FOR _field_obj IN SELECT * FROM json_array_elements(json_extract_path(field_data,'fields'))
LOOP
-- check if irrigation unit id is already linked to some other field
IF(SELECT EXISTS(
SELECT field_id FROM user_fields WHERE irrig_unit_id LIKE json_extract_path_text(_field_obj,'irrig_unit_id') AND deleted=FALSE
)) THEN
outobj := json_build_object('code',26);
-- Rollback any changes made by previous iterations of loop
ROLLBACK;
RETURN;
END IF;
-- check if this field name already exists
IF( SELECT EXISTS(
SELECT uf.field_id FROM user_fields uf
INNER JOIN user_farms ufa ON (ufa.farm_id=uf.user_farm_id AND ufa.deleted=FALSE)
INNER JOIN user_businesses ub ON (ub.business_id=ufa.user_business_id AND ub.deleted=FALSE)
INNER JOIN users u ON (ub.user_id = u.user_id AND u.deleted=FALSE)
WHERE u.user_id = v_user_id
AND uf.field_name LIKE json_extract_path_text(_field_obj,'field_name')
AND uf.deleted=FALSE
)) THEN
outobj := json_build_object('code', 22);
-- Rollback any changes made by previous iterations of loop
ROLLBACK;
RETURN;
END IF;
--create/update user business and farm and return farm_id
CALL usp_add_user_bussiness_and_farm(
json_build_object('user_email', json_extract_path_text(field_data,'user_email'),
'business_name', json_extract_path_text(_field_obj,'business_name'),
'farm_name', json_extract_path_text(_field_obj,'farm_name')
), farm_and_bussiness);
IF(json_extract_path_text(farm_and_bussiness, 'code')::int != 1) THEN
outobj := farm_and_bussiness;
-- Rollback any changes made by previous iterations of loop
ROLLBACK;
RETURN;
END IF;
-- insert into users fields
INSERT INTO user_fields (user_farm_id, irrig_unit_id, field_name, ground_water_percent, surface_water_percent)
SELECT json_extract_path_text(farm_and_bussiness,'farm_id')::bigint,
json_extract_path_text(_field_obj,'irrig_unit_id'),
json_extract_path_text(_field_obj,'field_name'),
json_extract_path_text(_field_obj,'groundWaterPercentage'):: int,
json_extract_path_text(_field_obj,'surfaceWaterPercentage'):: int;
-- add to user wells
CALL usp_insert_user_wells(json_extract_path(_field_obj,'well_data'), v_user_id, _are_wells_inserted);
END LOOP;
outobj := json_build_object('code',1);
RETURN;
EXCEPTION WHEN OTHERS THEN
raise notice '% : %', SQLERRM, SQLSTATE;
outobj := json_build_object('code',0);
RETURN;
END;
$BODY$;
如果您在 PL/pgSQL 块中有一个 EXCEPTION
子句,则整个块将在一个子事务中执行,该子事务在发生异常时回滚。所以你不能在这样的块中使用 COMMIT
或 ROLLBACK
。
如果您确实需要 ROLLBACK
,请像这样重写您的代码:
DECLARE
should_rollback boolean := FALSE;
BEGIN
FOR ... LOOP
BEGIN -- inner block for exception handling
/* do stuff */
IF (/* condition that should cause a rollback */) THEN
should_rollback := TRUE;
EXIT; -- from LOOP
END IF;
EXCEPTION
WHEN OTHERS THEN
/* handle the error */
END;
END LOOP;
IF should_rollback THEN
ROLLBACK;
/* do whatever else is needed */
END IF;
END;
现在回滚不会发生在带有异常处理程序的块中,它应该按照您想要的方式工作。
解释:
根据@Laurez Albe提供的线索,我想出了一个更简洁的方法来解决上述问题。
基本上,我所做的是,只要条件为 true
,我就会提出 custom exception
。所以当抛出异常时,block X
所做的所有更改都会优雅地回滚。我什至可以在异常条件块中执行最后一分钟的清理。
实施:
CREATE OR REPLACE procedure mProcedure(INOUT resp json DEFAULT NULL::JSON)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
field_data json := '{ "fields": [1,2,3,4,5] }';
_field_id int;
BEGIN
-- Start of block X
FOR _field_id IN SELECT * FROM json_array_elements(json_extract_path(field_data,'fields'))
LOOP
INSERT INTO demo VALUES(_field_id);
IF(_field_id = 3) THEN
RAISE EXCEPTION USING ERRCODE='22013';
END IF;
IF(_field_id = 5) THEN
RAISE EXCEPTION USING ERRCODE='22014';
END IF;
END LOOP;
SELECT json_agg(row_to_json(d)) INTO resp FROM demo d;
RETURN;
-- end of block X
-- if an exception occurs in block X, then all the changes made within the block are rollback
-- and the control is passed on to the EXCEPTION WHEN OTHERS block.
EXCEPTION
WHEN sqlstate '22013' THEN
resp := json_build_object('code',26);
WHEN sqlstate '22014' THEN
resp := json_build_object('code',22);
END;
$BODY$;
演示:
我编写了一个存储过程,它基本上循环遍历 fields
的数组,并在每次迭代时在数据库中执行一些操作。我想要实现的是,要么循环的所有迭代都应该发生,要么它们都不应该发生。
假设字段数组中有 5 个元素,循环迭代到第 3 个元素,然后发现某些条件为真并引发错误,我想回滚前 2 个期间发生的所有更改迭代。我已经使用 ROLLBACK
语句来实现相同的目的,但是每次它到达 ROLLBACK
语句时都会抛出以下错误:
Cannot rollback while a subtransaction is active : 2D000
令人惊讶的是,如果我在 EXCEPTION WHEN OTHERS THEN
块中注释掉 outobj := json_build_object('code',0);
语句或者完全删除整个块,它会正常工作。
我检查了 PostgreSQL documentation for error codes,但它并没有真正帮助。我的存储过程如下:
CREATE OR REPLACE PROCEDURE public.usp_add_fields(
field_data json,
INOUT outobj json DEFAULT NULL::json)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
v_user_id bigint;
farm_and_bussiness json;
_field_obj json;
_are_wells_inserted boolean;
BEGIN
-- get user id
v_user_id = ___uf_get_user_id(json_extract_path_text(field_data,'user_email'));
IF(v_user_id IS NULL) THEN
outobj := json_build_object('code',17);
RETURN;
END IF;
-- Loop over entities to create farms & businesses
FOR _field_obj IN SELECT * FROM json_array_elements(json_extract_path(field_data,'fields'))
LOOP
-- check if irrigation unit id is already linked to some other field
IF(SELECT EXISTS(
SELECT field_id FROM user_fields WHERE irrig_unit_id LIKE json_extract_path_text(_field_obj,'irrig_unit_id') AND deleted=FALSE
)) THEN
outobj := json_build_object('code',26);
-- Rollback any changes made by previous iterations of loop
ROLLBACK;
RETURN;
END IF;
-- check if this field name already exists
IF( SELECT EXISTS(
SELECT uf.field_id FROM user_fields uf
INNER JOIN user_farms ufa ON (ufa.farm_id=uf.user_farm_id AND ufa.deleted=FALSE)
INNER JOIN user_businesses ub ON (ub.business_id=ufa.user_business_id AND ub.deleted=FALSE)
INNER JOIN users u ON (ub.user_id = u.user_id AND u.deleted=FALSE)
WHERE u.user_id = v_user_id
AND uf.field_name LIKE json_extract_path_text(_field_obj,'field_name')
AND uf.deleted=FALSE
)) THEN
outobj := json_build_object('code', 22);
-- Rollback any changes made by previous iterations of loop
ROLLBACK;
RETURN;
END IF;
--create/update user business and farm and return farm_id
CALL usp_add_user_bussiness_and_farm(
json_build_object('user_email', json_extract_path_text(field_data,'user_email'),
'business_name', json_extract_path_text(_field_obj,'business_name'),
'farm_name', json_extract_path_text(_field_obj,'farm_name')
), farm_and_bussiness);
IF(json_extract_path_text(farm_and_bussiness, 'code')::int != 1) THEN
outobj := farm_and_bussiness;
-- Rollback any changes made by previous iterations of loop
ROLLBACK;
RETURN;
END IF;
-- insert into users fields
INSERT INTO user_fields (user_farm_id, irrig_unit_id, field_name, ground_water_percent, surface_water_percent)
SELECT json_extract_path_text(farm_and_bussiness,'farm_id')::bigint,
json_extract_path_text(_field_obj,'irrig_unit_id'),
json_extract_path_text(_field_obj,'field_name'),
json_extract_path_text(_field_obj,'groundWaterPercentage'):: int,
json_extract_path_text(_field_obj,'surfaceWaterPercentage'):: int;
-- add to user wells
CALL usp_insert_user_wells(json_extract_path(_field_obj,'well_data'), v_user_id, _are_wells_inserted);
END LOOP;
outobj := json_build_object('code',1);
RETURN;
EXCEPTION WHEN OTHERS THEN
raise notice '% : %', SQLERRM, SQLSTATE;
outobj := json_build_object('code',0);
RETURN;
END;
$BODY$;
如果您在 PL/pgSQL 块中有一个 EXCEPTION
子句,则整个块将在一个子事务中执行,该子事务在发生异常时回滚。所以你不能在这样的块中使用 COMMIT
或 ROLLBACK
。
如果您确实需要 ROLLBACK
,请像这样重写您的代码:
DECLARE
should_rollback boolean := FALSE;
BEGIN
FOR ... LOOP
BEGIN -- inner block for exception handling
/* do stuff */
IF (/* condition that should cause a rollback */) THEN
should_rollback := TRUE;
EXIT; -- from LOOP
END IF;
EXCEPTION
WHEN OTHERS THEN
/* handle the error */
END;
END LOOP;
IF should_rollback THEN
ROLLBACK;
/* do whatever else is needed */
END IF;
END;
现在回滚不会发生在带有异常处理程序的块中,它应该按照您想要的方式工作。
解释:
根据@Laurez Albe提供的线索,我想出了一个更简洁的方法来解决上述问题。
基本上,我所做的是,只要条件为 true
,我就会提出 custom exception
。所以当抛出异常时,block X
所做的所有更改都会优雅地回滚。我什至可以在异常条件块中执行最后一分钟的清理。
实施:
CREATE OR REPLACE procedure mProcedure(INOUT resp json DEFAULT NULL::JSON)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
field_data json := '{ "fields": [1,2,3,4,5] }';
_field_id int;
BEGIN
-- Start of block X
FOR _field_id IN SELECT * FROM json_array_elements(json_extract_path(field_data,'fields'))
LOOP
INSERT INTO demo VALUES(_field_id);
IF(_field_id = 3) THEN
RAISE EXCEPTION USING ERRCODE='22013';
END IF;
IF(_field_id = 5) THEN
RAISE EXCEPTION USING ERRCODE='22014';
END IF;
END LOOP;
SELECT json_agg(row_to_json(d)) INTO resp FROM demo d;
RETURN;
-- end of block X
-- if an exception occurs in block X, then all the changes made within the block are rollback
-- and the control is passed on to the EXCEPTION WHEN OTHERS block.
EXCEPTION
WHEN sqlstate '22013' THEN
resp := json_build_object('code',26);
WHEN sqlstate '22014' THEN
resp := json_build_object('code',22);
END;
$BODY$;