为什么 postgres set_config 的 is_local = true 不为整个事务保留变量?

why does postgres set_config's is_local = true not persist the variable for the whole transaction?

我有一个函数,它使用 set_configis_local = true 来设置变量。 现在我希望在同一事务中使用带有 current_settings 的变量的 select 语句能够访问该变量,因为文档状态:

set_config(setting_name, new_value, is_local) ... set parameter and return new value

set_config sets the parameter setting_name to new_value. If is_local is true, the new value will only apply to the current transaction.

但这对我来说不起作用,我得到

ERROR:  unrecognized configuration parameter "auth.tenant_id"
SQL state: 42704

有什么我错的地方吗?

这是函数:

CREATE OR REPLACE FUNCTION auth.authorize(IN a_user_id uuid)
    RETURNS boolean
    LANGUAGE 'plpgsql'
    SECURITY DEFINER 
AS $BODY$
declare
v_tenant_id uuid;
v_user_role auth.user_role;
begin
select tenant_id, user_role into strict v_tenant_id, v_user_role from auth.authorizations where user_id = a_user_id;
perform set_config('auth.tenant_id', v_tenant_id::text, true);
perform set_config('auth.user_role', v_user_role::text, true);
return true;
exception when no_data_found then return false;
end;
$BODY$;

这是第二个 select 语句失败的事务

begin;
select * from auth.authorize(uuid('180e1b14-21e5-4e66-a9b8-db09139d6278'));
select current_setting('auth.tenant_id') as tenant_id, current_setting('auth.user_role') as user_role;
commit;

set_config(is_local => true) 的效果似乎被限制在您的 BEGIN … EXCEPTION 区块创建的隐式子交易中。

(跳到我的 TL;DR 解决方案的结尾。)

下面是我执行类似操作时发生的情况,但在 BEGIN … EXCEPTION 子事务之外调用 set_config()

create or replace function set_config_outside_of_begin_except_block(denominator int)
    returns bool
    language 'plpgsql'
as $body$
begin
    perform set_config('my.setting', 'set before subtransaction', true);

    begin
        perform 10 / denominator;
        return true;
    exception when division_by_zero then
        return false;
    end;
end;
$body$;

begin
select current_setting('my.setting') as my_setting;
select set_config_outside_of_begin_except_block(1);
rollback;
CREATE FUNCTION
BEGIN
ERROR:  unrecognized configuration parameter "my.setting"
 set_config_outside_of_begin_except_block
------------------------------------------
 t
(1 row)

        my_setting
---------------------------
 set before subtransaction
(1 row)

ROLLBACK

set_config() 调用移至 BEGIN … EXCEPTION 块之前,my.setting 的新设置已在我们的函数调用之外保留。

而且,正如您接下来将看到的,如果在上面的 BEGIN … EXCEPTION 块中发生异常甚至无关紧要,这是有道理的,因为 set_config() 被称为 输入 block/subtransaction 之前:

begin
select current_setting('my.setting') as my_setting;
rollback;
BEGIN
 set_config_outside_of_begin_except_block
------------------------------------------
 f
(1 row)
        my_setting

---------------------------
 set before subtransaction
(1 row)

ROLLBACK

现在,我将稍微修改代码以更接近您的示例,并更接近于重现您描述的行为:

create or replace function set_config_in_begin_except_block(denominator int)
    returns bool
    language 'plpgsql'
as $body$
begin
    perform set_config('my.setting', 'set in subtransaction', true);
    perform 10 / denominator;
    return true;
exception when division_by_zero then
    return false;
end;
$body$;

begin;
select set_config_in_begin_except_block(0);
select current_setting('my.setting') as my_setting;
rollback;
CREATE FUNCTION
BEGIN
 set_config_in_begin_except_block
----------------------------------
 f
(1 row)

 my_setting
------------

(1 row)

ROLLBACK
begin
select set_config_in_begin_except_block(1);
select current_setting('my.setting') as my_setting;
rollback;
BEGIN
 set_config_in_begin_except_block
----------------------------------
 t
(1 row)

      my_setting
-----------------------
 set in subtransaction
(1 row)

ROLLBACK

请注意,现在,

  1. BEGIN … EXCEPTION 块确实导致对 my.setting 的更改丢失,
  2. 但只有当异常 (division_by_zero) 确实被引发时,
  3. 即使 division_by_zero 是由语句 after set_config().
  4. 触发的

所以,它有点接近您描述的行为,但又不完全是。您的设置不会在 BEGIN … EXCEPTION 块之外持续存在 无论 是否遇到 no_data_found 异常。

绕个弯路:我偶然发现了你的问题,因为我想知道我是否可以将设置视为与(子)事务堆栈一起展开的堆栈。事实证明我可以:

create or replace function set_config_stacked(denominator int)
    returns bool
    language 'plpgsql'
as $body$
begin
    perform set_config('my.setting', 'set before substraction', true);

    begin
        perform set_config('my.setting', 'set within subtransaction before division', true);
        perform 10 / denominator;
        return true;
    exception when division_by_zero then
        return false;
    end;
end;
$body$;

begin;
select set_config_stacked(1);
select current_setting('my.setting') as my_setting;
rollback;
CREATE FUNCTION
BEGIN
 set_config_stacked
--------------------
 t
(1 row)

                my_setting
-------------------------------------------
 set within subtransaction before division
(1 row)

ROLLBACK
begin;
select set_config_stacked(0);
select current_setting('my.setting') as my_setting;
rollback;
BEGIN
 set_config_stacked
--------------------
 f
(1 row)

       my_setting
-------------------------
 set before substraction
(1 row)

ROLLBACK

所以当 BEGIN … EXCEPTION 块的子交易关闭时 my.setting 实际上恢复到它以前的设置。

我需要最后一项更改来重现您的函数的行为:

create table tab (id uuid primary key, stuff text);
insert into tab (id, stuff) values (uuid('180e1b14-21e5-4e66-a9b8-db09139d6278'), 'some stuff');

create or replace function set_config_stacked(id$ uuid)
    returns bool
    language 'plpgsql'
as $body$
declare
    rec tab%rowtype;
begin
    perform set_config('my.setting', 'set before substraction', true);

    begin
        perform set_config('my.setting', 'set within subtransaction before SELECT', true);
        select * into strict rec from tab where id = id$;
        perform set_config('my.setting', 'set within subtransaction after SELECT', true);
        return true;
    exception when no_data_found then
        return false;
    end;
end;
$body$;

begin;
select set_config_stacked(uuid('180e1b14-21e5-4e66-a9b8-db09139d6278'));  -- exist
select current_setting('my.setting') as my_setting;
rollback;
CREATE TABLE
INSERT 1 0
CREATE FUNCTION
 set_config_stacked
--------------------
 t
(1 row)

               my_setting
----------------------------------------
 set within subtransaction after SELECT
(1 row)

ROLLBACK
begin;
select set_config_stacked(uuid('cc7ad0c3-7e3a-49a0-b7d8-7b4093ae0028'));  -- doesn't exist
select current_setting('my.setting') as my_setting;
rollback;
BEGIN
 set_config_stacked
--------------------
 f
(1 row)

       my_setting
-------------------------
 set before substraction
(1 row)

ROLLBACK

如您所见,您所描述的行为仍有一方面我无法重现。此版本的 set_config_stacked() 围绕 no_data_found 异常运行,其行为与基于 division_by_zero 异常的先前版本相同。

但是,您的示例表明当遇到 no 异常时,您的设置也不会在 auth.authorize() 之外持续存在。这让我感到困惑,而且我无法重现,至少在 Postgres 14 上是这样。

尽管如此,您的问题可以通过将调用移至 BEGIN … EXCEPTION … END 块下方的 set_config() 来解决。您仍然希望在该块 设置变量,但是执行这些调用和 return true 来自要创建的新外部 BEGIN … END 块(没有 EXCEPTION).