Postgres函数带有引用的列名

时间:2014-08-12 11:39:27

标签: postgresql plpgsql

TL; DR:

我试图将一个或多个列的名称和值传递给Postgres中的函数,该函数用于检查约束。这似乎工作正常,直到列名称需要引用(即包含大写字母),当我得到'列“x”不存在'消息。如果我引用标识符,则函数的行为会发生变化。

如果列标识符需要引用,我似乎无法找到一种方法来引用函数中的列名及其值(从检查约束调用)。


完整故事:

在Postgres中,我试图使用用户定义的函数和Check约束来模拟Unique约束。

我想这样做,因为我需要一个“条件”唯一约束,如果不满足Check中的其他条件,则可能不会强制执行唯一性。

(我很欣赏一个明显的答案可能是“你不想这样做”,或“这是一个坏主意”,但我会很感激的答案,而不是解决问题,我有更多直接。)

当前尝试:

由于作为唯一的一部分可能包含多个列,我创建了一个接受表,列数组和值数组的函数。

CREATE OR REPLACE FUNCTION is_unique(_table text, _columns text[], _values text[]) RETURNS boolean AS
$$
DECLARE 
    result integer;
    statement text = 'SELECT (EXISTS (SELECT 1 FROM ' || quote_ident(_table) || ' WHERE ';
    first boolean = true;
BEGIN
    FOR i IN array_lower(_columns,1)..array_upper(_columns,1) LOOP
        IF first THEN
            statement = statement || quote_ident(_columns[i]) || '=' || _values[i];
            first = false;
        ELSE
            statement = statement || ' AND '|| quote_ident(_columns[i]) || '=' || _values[i];
        END IF;
    END LOOP;
    statement = statement || '))::int';
    EXECUTE statement INTO result;
    RETURN NOT result::boolean;
END
$$
LANGUAGE 'plpgsql';

我在这个函数中尝试做的是形式声明:

SELECT 1 FROM _table WHERE _column[i]=_value[i] AND ...

然后可以将其用作Check约束的一部分,例如:

CHECK (is_unique('sometable'::text,'{somecolumn}'::text[],ARRAY[somecolumn]::text[]))

发生了什么:

当与不需要引用的列一起使用时,此结构似乎有效,但否则行为似乎会中断。一旦我插入一行,即使值是唯一的,我也无法插入另一行。我认为问题是列的值可能与自身进行比较,或者正在比较标识符。

有没有人对如何更改我的代码以解决此问题有任何建议?不幸的是,在这种情况下,应对引用的标识符很重要。

1 个答案:

答案 0 :(得分:2)

我认为您可能真的在寻找partial unique indexesexclusion constraints。描述有点太模糊,不能说 - 没有样本数据,没有&#34的例子;这应该被允许,这不应该"等等。

考虑:

CREATE UNIQUE INDEX some_idx_name
ON some_table (col1, col2, col3) WHERE (col1 != 4 AND col5 IS NOT NULL);

尝试使用检查约束和函数模拟唯一索引注定要失败。它甚至没有#34;你不想做这个",它"它从根本上是行不通的。"

唯一约束和索引部分免于事务可见性规则。尝试将副本插入到唯一索引中,其中创建第一个副本的事务尚未提交但将阻塞,直到第一个事务提交或回滚。这就是为什么即使交易无法看到彼此之后,唯一约束也会起作用的原因。未提交的数据。您无法模拟此,因为PostgreSQL不为事务提供脏读隔离,因此根本无法做到这一点。 (好吧,如果你在C中编写了检查约束函数,你就可以这么做了,但它有令人讨厌的竞争条件)。

只有 方式可以做你想做的事情,如果你在做任何事情之前在你的函数中LOCK TABLE ... IN EXCLUSIVE MODE。如果不这样做,保证会导致与并发相关的错误。但是,如果您执行采取独占锁定,那么所有写入都必须按顺序进行,每次只有一个事务对表有未提交的更改。更糟糕的是,尝试并发写入通常会导致由于锁定升级导致的死锁导致事务中止。

所以,你可以可靠地做到这一点的唯一方法是在事务开始时使用应用程序LOCK TABLE ... IN EXCLUSIVE MODE,然后再获取其他锁,如果它认为可能需要写入它。我相信你可以想象这对于表现有多大的乐趣。

(顺便说一句,在检查约束中调用的函数严格地应该是IMMUTABLE并且不能访问除了它们传递的参数之外的数据.PostgreSQL目前不会因为它&而错过该规则#39;访问几乎总是未更改的查找表等非常方便 - 但它确实意味着如果查看可能发生变化的数据,您可能会从check约束中获得意外结果。)


此外,该功能非常低效 - 当您真的不需要时,您可以循环使用并且可以使用一些简单的SQL。 (另外,请为了那些跟在你后面的人而缩进你的代码。)

这个区块:

FOR i IN array_lower(_columns,1)..array_upper(_columns,1) LOOP
  IF first THEN
    statement = statement || quote_ident(_columns[i]) || '=' || _values[i];
    first = false;
  ELSE
    statement = statement || ' AND '|| quote_ident(_columns[i]) || '=' || _values[i];
  END IF;
END LOOP;

只是一种缓慢的写作方式:

SELECT
  string_agg( 
    format('%I = %L', _columns[i], _values[i]),
    ' AND '
    ORDER BY i
  )
FROM generate_subscripts(_columns, 1) i;

但即便如此,仍有一个错误:如果用户通过NULL,您将生成= NULL,这是完全错误的。您需要使用特殊情况NULL值,或使用IS DISTINCT FROM,例如

format('%I IS DISTINCT FROM %L', _columns[i], _values[i])

但是IS DISTINCT FROM无法使用索引,因此CASE可能更合适:

CASE
  WHEN _values[i] IS NOT NULL THEN
    format('%I = %L', _columns[i], _values[i]),
  ELSE
    format('%I IS NULL', _columns[i])
END