使用前缀的PostgreSQL约束

Jur*_*ský 14 sql postgresql locking constraints

假设我有以下PostgreSQL表:

id | key
---+--------
1  | 'a.b.c'
Run Code Online (Sandbox Code Playgroud)

我需要防止使用作为另一个键的前缀的键插入记录.例如,我应该能够插入:

  • 'a.b.b'

但是不应接受以下密钥:

  • 'a.b'
  • 'a.b.c'
  • 'a.b.c.d'

有没有办法实现这一点 - 通过约束或锁定机制(在插入​​之前检查存在)?

fil*_*rem 10

此解决方案基于PostgreSQL 用户定义的运算符和排除约束(基本语法,更多详细信息).

注意:更多测试显示此解决方案无法正常工作.见底部.

  1. 创建一个函数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
    $$;
    
    Run Code Online (Sandbox Code Playgroud)
  2. 为索引创建运算符

    CREATE OPERATOR <~> (
      PROCEDURE = has_common_prefix,
      LEFTARG   = text,
      RIGHTARG  = text,
      COMMUTATOR = <~>
    );
    
    Run Code Online (Sandbox Code Playgroud)
  3. 创建排除约束

    CREATE TABLE keys ( key text );
    
    ALTER TABLE keys
      ADD CONSTRAINT keys_cannot_have_common_prefix
      EXCLUDE ( key WITH <~> ); 
    
    Run Code Online (Sandbox Code Playgroud)

但是,最后一点会产生此错误:

    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.
Run Code Online (Sandbox Code Playgroud)

这是因为创建索引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 <~> );
Run Code Online (Sandbox Code Playgroud)

现在,它有效:

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).
Run Code Online (Sandbox Code Playgroud)

注意:更多测试显示此解决方案尚未运行:最后一次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
Run Code Online (Sandbox Code Playgroud)


Mic*_*zzi 5

您可以使用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');
Run Code Online (Sandbox Code Playgroud)

当然,我不必为此测试创建主键和其他约束。但是不要忘记这样做。另外,ltree模块上的内容比我展示的要多得多,如果您需要其他功能,请查看其文档,也许您会在这里找到答案。