簡體   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