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[]))
发生了什么:
当与不需要引用的列一起使用时,此结构似乎有效,但否则行为似乎会中断。一旦我插入一行,即使值是唯一的,我也无法插入另一行。我认为问题是列的值可能与自身进行比较,或者正在比较标识符。
有没有人对如何更改我的代码以解决此问题有任何建议?不幸的是,在这种情况下,应对引用的标识符很重要。
答案 0 :(得分:2)
我认为您可能真的在寻找partial unique indexes或exclusion 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