繁体   English   中英

使用前缀的PostgreSQL约束

[英]PostgreSQL constraint using prefixes

假设我有以下PostgreSQL表:

id | key
---+--------
1  | 'a.b.c'

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

  • 'abb'

但是不应接受以下密钥:

  • 'a.b'
  • 'abc'
  • 'abcd'

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

此解决方案基于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 $$; 
  2. 为索引创建运算符

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

     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模块还有很多比我展示的更多,如果你需要一些不同的东西来看看它的文档,也许你会在那里找到答案。

您可以尝试以下触发器。 请注意, key是sql reserve word。 所以我建议你不要在表中使用它作为列名。 我还添加了我的create table语法用于测试目的:

CREATE TABLE my_table
(myid INTEGER, mykey VARCHAR(50));

CREATE FUNCTION check_key_prefix() RETURNS TRIGGER AS $check_key_prefix$
  DECLARE
    v_match_keys INTEGER;
  BEGIN
    v_match_keys = 0;
    SELECT COUNT(t.mykey) INTO v_match_keys
    FROM my_table t
    WHERE t.mykey LIKE CONCAT(NEW.mykey, '%')
     OR NEW.mykey LIKE CONCAT(t.mykey, '%');

    IF v_match_keys > 0 THEN 
      RAISE EXCEPTION 'Prefix Key Error occured.';
    END IF;

    RETURN NEW;
  END;
$check_key_prefix$ LANGUAGE plpgsql;

CREATE TRIGGER check_key_prefix
 BEFORE INSERT OR UPDATE ON my_table
 FOR EACH ROW
 EXECUTE PROCEDURE check_key_prefix();

这是一个基于CHECK的解决方案 - 它可以满足您的需求。

CREATE TABLE keys ( id serial primary key, key text );

CREATE OR REPLACE FUNCTION key_check(text)
RETURNS boolean
STABLE STRICT
LANGUAGE SQL AS $$
  SELECT NOT EXISTS (
    SELECT 1 FROM keys
      WHERE key ~ ( '^' || $1 )
         OR $1 ~ ( '^' || key )
  );
$$;

ALTER TABLE keys
  ADD CONSTRAINT keys_cannot_have_common_prefix
  CHECK ( key_check(key) );

PS。 不幸的是,它在一点上失败了(多行插入)。

SQL是一种非常强大的语言。 通常你可以通过普通的select语句完成大部分工作。 即如果您不喜欢触发器,则可以对插入使用此方法。

唯一的假设是表中至少存在一行。 (*)

桌子:

create table my_table
(
    id integer primary key,
    key varchar(100)
);

由于这个假设,我们至少有一行。(*)

insert into my_table (id, key) values (1, 'a.b.c');

现在神奇的sql。 诀窍是用要插入的键值替换p_key值。 我故意不将该语句放入存储过程中。 因为如果你想将它带到你的应用程序端,我希望它是直截了当的。 但通常将sql放入存储过程更好。

insert into my_table (id, key)
    select (select max(id) + 1 from my_table), p_key
        from my_table
        where not exists (select 'p' from my_table where key like p_key || '%' or p_key like key || '%')
        limit 1;

现在测试:

-- 'a.b.b' => Inserts
insert into my_table (id, key)
    select (select max(id) + 1 from my_table), 'a.b.b'
        from my_table
        where not exists (select 'p' from my_table where key like 'a.b.b' || '%' or 'a.b.b' like key || '%')
        limit 1;


-- 'a.b' => does not insert
insert into my_table (id, key)
    select (select max(id) + 1 from my_table), 'a.b'
        from my_table
        where not exists (select 'p' from my_table where key like 'a.b' || '%' or 'a.b' like key || '%')
        limit 1;


-- 'a.b.c' => does not insert
insert into my_table (id, key)
    select (select max(id) + 1 from my_table), 'a.b.c'
        from my_table
        where not exists (select 'p' from my_table where key like 'a.b.c' || '%' or 'a.b.c' like key || '%')
        limit 1;

-- 'a.b.c.d' does not insert
insert into my_table (id, key)
    select (select max(id) + 1 from my_table), 'a.b.c.d'
        from my_table
        where not exists (select 'p' from my_table where key like 'a.b.c.d' || '%' or 'a.b.c.d' like key || '%')
        limit 1;

(*)如果您希望通过引入类似双表的Oracle来摆脱单行的存在。 如果您希望修改insert语句是直截了当的。 如果您愿意,请告诉我。

一种可能的解决方案是创建一个包含密钥前缀的辅助表,然后使用唯一和排除约束的组合以及插入触发器来强制实现所需的唯一性语义。

在高级别,这种方法将每个密钥分解为前缀列表并应用类似于读取器 - 写入器锁定语义的东西:只要没有密钥等于前缀,任何数量的密钥都可以共享前缀。 为了实现这一点,前缀列表包括密钥本身,其标志将其标记为终端前缀。

辅助表看起来像这样。 我们使用CHAR而不是BOOLEAN作为标志,因为稍后我们将添加一个对布尔列不起作用的约束。

CREATE TABLE prefixes (
    id INTEGER NOT NULL,
    prefix TEXT NOT NULL,
    is_terminal CHAR NOT NULL,

    CONSTRAINT prefixes_id_fk
    FOREIGN KEY (id)
    REFERENCES your_table (id)
    ON DELETE CASCADE,

    CONSTRAINT prefixes_is_terminal
    CHECK (is_terminal IN ('t', 'f'))
);

现在我们需要在插入到your_table定义一个触发器,以便将行插入到prefixes ,这样就可以了

INSERT INTO your_table (id, key) VALUES (1, ‘abc');

原因

INSERT INTO prefixes (id, prefix, is_terminal) VALUES (1, 'a', ‘f’);
INSERT INTO prefixes (id, prefix, is_terminal) VALUES (1, 'ab', ‘f’);
INSERT INTO prefixes (id, prefix, is_terminal) VALUES (1, 'abc', ’t’);

触发器功能可能如下所示。 我这里只讨论INSERT案例,但是可以通过删除旧前缀然后插入新前缀来使函数处理UPDATE DELETE案例由prefixes上的级联外键约束涵盖。

CREATE OR REPLACE FUNCTION insert_prefixes() RETURNS TRIGGER AS $$
DECLARE
  is_terminal CHAR := 't';
  remaining_text TEXT := NEW.key;
BEGIN
  LOOP
    IF LENGTH(remaining_text) <= 0 THEN
      EXIT;
    END IF;

    INSERT INTO prefixes (id, prefix, is_terminal)
        VALUES (NEW.id, remaining_text, is_terminal);

    is_terminal := 'f';
    remaining_text := LEFT(remaining_text, -1);
  END LOOP;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

我们以通常的方式将此函数作为触发器添加到表中。

CREATE TRIGGER insert_prefixes
AFTER INSERT ON your_table
FOR EACH ROW
  EXECUTE PROCEDURE insert_prefixes();

排除约束和部分唯一索引将强制执行is_terminal = 't'行不能与同一前缀的另一行冲突,无论其is_terminal值如何,并且只有一行is_terminal = 't'

ALTER TABLE prefixes ADD CONSTRAINT prefixes_forbid_conflicts
  EXCLUDE USING gist (prefix WITH =, is_terminal WITH <>);

CREATE UNIQUE INDEX ON prefixes (prefix) WHERE is_terminal = 't';

这允许新行不冲突但防止发生冲突的行,包括在多行INSERT中。

db=# INSERT INTO your_table (id, key) VALUES (1, 'a.b.c');
INSERT 0 1

db=# INSERT INTO your_table (id, key) VALUES (2, 'a.b.b');
INSERT 0 1

db=# INSERT INTO your_table (id, key) VALUES (3, 'a.b');
ERROR:  conflicting key value violates exclusion constraint "prefixes_forbid_conflicts"

db=# INSERT INTO your_table (id, key) VALUES (4, 'a.b.c');
ERROR:  duplicate key value violates unique constraint "prefixes_prefix_idx"

db=# INSERT INTO your_table (id, key) VALUES (5, 'a.b.c.d');
ERROR:  conflicting key value violates exclusion constraint "prefixes_forbid_conflicts"

db=# INSERT INTO your_table (id, key) VALUES (6, 'a.b.d'), (7, 'a');
ERROR:  conflicting key value violates exclusion constraint "prefixes_forbid_conflicts"

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM