Jur*_*ský 14 sql postgresql locking constraints
假设我有以下PostgreSQL表:
id | key
---+--------
1  | 'a.b.c'
我需要防止使用作为另一个键的前缀的键插入记录.例如,我应该能够插入:
'a.b.b'但是不应接受以下密钥:
'a.b''a.b.c''a.b.c.d'有没有办法实现这一点 - 通过约束或锁定机制(在插入之前检查存在)?
fil*_*rem 10
此解决方案基于PostgreSQL 用户定义的运算符和排除约束(基本语法,更多详细信息).
注意:更多测试显示此解决方案无法正常工作.见底部.
创建一个函数has_common_prefix(text,text),它将逻辑地计算您需要的内容.将该功能标记为IMMUTABLE.
CREATE OR REPLACE FUNCTION
has_common_prefix(text,text)
RETURNS boolean
IMMUTABLE STRICT
LANGUAGE SQL AS $$
  SELECT position ($1 in $2) = 1 OR position ($2 in $1) = 1
$$;
为索引创建运算符
CREATE OPERATOR <~> (
  PROCEDURE = has_common_prefix,
  LEFTARG   = text,
  RIGHTARG  = text,
  COMMUTATOR = <~>
);
创建排除约束
CREATE TABLE keys ( key text );
ALTER TABLE keys
  ADD CONSTRAINT keys_cannot_have_common_prefix
  EXCLUDE ( key WITH <~> ); 
但是,最后一点会产生此错误:
    ERROR:  operator <~>(text,text) is not a member of operator family "text_ops"
    DETAIL:  The exclusion operator must be related to the index operator class for the constraint.
这是因为创建索引PostgreSQL需要逻辑运算符与物理索引方法绑定,通过实体calles"运算符类".所以我们需要提供这样的逻辑:
CREATE OR REPLACE FUNCTION keycmp(text,text)
RETURNS integer IMMUTABLE STRICT
LANGUAGE SQL AS $$
  SELECT CASE
    WHEN $1 = $2 OR position ($1 in $2) = 1 OR position ($2 in $1) = 1 THEN 0
    WHEN $1 < $2 THEN -1
    ELSE 1
  END
$$;
CREATE OPERATOR CLASS key_ops FOR TYPE text USING btree AS
  OPERATOR 3 <~> (text, text),
  FUNCTION 1 keycmp (text, text)
;
ALTER TABLE keys
  ADD CONSTRAINT keys_cannot_have_common_prefix
  EXCLUDE ( key key_ops WITH <~> );
现在,它有效:
INSERT INTO keys SELECT 'ara';
INSERT 0 1
INSERT INTO keys SELECT 'arka';
INSERT 0 1
INSERT INTO keys SELECT 'barka';
INSERT 0 1
INSERT INTO keys SELECT 'arak';
psql:test.sql:44: ERROR:  conflicting key value violates exclusion constraint "keys_cannot_have_common_prefix"
DETAIL:  Key (key)=(arak) conflicts with existing key (key)=(ara).
INSERT INTO keys SELECT 'bark';
psql:test.sql:45: ERROR:  conflicting key value violates exclusion constraint "keys_cannot_have_common_prefix"
DETAIL:  Key (key)=(bark) conflicts with existing key (key)=(barka).
注意:更多测试显示此解决方案尚未运行:最后一次INSERT应该失败.
INSERT INTO keys SELECT 'a';
INSERT 0 1
INSERT INTO keys SELECT 'ac';
ERROR:  conflicting key value violates exclusion constraint "keys_cannot_have_common_prefix"
DETAIL:  Key (key)=(ac) conflicts with existing key (key)=(a).
INSERT INTO keys SELECT 'ab';
INSERT 0 1
您可以使用ltree模块来实现这一点,它将使您创建分层的树状结构。还可以帮助您避免重新发明轮子,创建复杂的正则表达式等。您只需要postgresql-contrib安装软件包。看一看:
--Enabling extension
CREATE EXTENSION ltree;
--Creating our test table with a pre-loaded data
CREATE TABLE test_keys AS 
    SELECT 
        1 AS id, 
        'a.b.c'::ltree AS key_path;
--Now we'll do the trick with a before trigger
CREATE FUNCTION validate_key_path() RETURNS trigger AS $$
    BEGIN
        --This query will do our validation. 
        --It'll search if a key already exists in 'both' directions
        --LIMIT 1 because one match is enough for our validation :)    
        PERFORM * FROM test_keys WHERE key_path @> NEW.key_path OR key_path <@ NEW.key_path LIMIT 1;
        --If found a match then raise a error        
        IF FOUND THEN
            RAISE 'Duplicate key detected: %', NEW.key_path USING ERRCODE = 'unique_violation'; 
        END IF;
        --Great! Our new row is able to be inserted     
        RETURN NEW;
    END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER test_keys_validator BEFORE INSERT OR UPDATE ON test_keys
    FOR EACH ROW EXECUTE PROCEDURE validate_key_path();     
--Creating a index to speed up our validation...            
CREATE INDEX idx_test_keys_key_path ON test_keys USING GIST (key_path);
--The command below will work    
INSERT INTO test_keys VALUES (2, 'a.b.b');
--And the commands below will fail 
INSERT INTO test_keys VALUES (3, 'a.b');
INSERT INTO test_keys VALUES (4, 'a.b.c');
INSERT INTO test_keys VALUES (5, 'a.b.c.d');
当然,我不必为此测试创建主键和其他约束。但是不要忘记这样做。另外,ltree模块上的内容比我展示的要多得多,如果您需要其他功能,请查看其文档,也许您会在这里找到答案。
| 归档时间: | 
 | 
| 查看次数: | 733 次 | 
| 最近记录: |