2017-06-21 49 views
14

比方說,我有以下PostgreSQL表:PostgreSQL的使用約束前綴

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

我需要防止插入記錄用鑰匙是另一個關鍵的前綴。舉例來說,我應該能夠插入:

  • 'a.b.b'

但是下面的鍵不應該被接受:

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

有沒有辦法實現這一點 - 無論是由約束或通過鎖定機制(插入前檢查存在)?

回答

11

該解決方案基於PostgreSQL user-defined operators和排除約束(base syntaxmore details)。

注意:更多測試顯示此解決方案無法工作(還)。見底部。

  1. 創建一個函數has_common_prefix(文本,文本),它將邏輯地計算出您需要的。將該功能標記爲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需要邏輯運營商與物理索引方法的約束,通過實體卡列斯「算子類」。因此,我們需要提供一個邏輯:現在

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 
+0

我就這樣走了,後來我發現公司Postgres很老了,不支持'EXCLUDE'約束!您想要的功能看起來與位置($ 2中的$ 1)> 0或位置($ 1中的$ 2)一樣簡單> 0。 –

+0

該表可以有多個記錄。這種方法可以使用任何索引嗎? –

+0

@Juraj是的,這個EXCLUDE特性總是需要一個索引,所以約束速度很快。順便說一句 - 解決方案現在完成,所以請測試它(應該在9.1+以上) – filiprem

2

這是一個基於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。不幸的是,它在一個點上失敗(多行插入)。

4

您可以使用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模塊比我所展示的要多得多,如果你需要不同的東西來看看它的文檔,也許你會在那裏找到答案。

4

你可以嘗試下面的觸發器。請注意,key是sql預留字。所以我建議你避免在表中使用它作爲列名。 我已經加了我也創建表的語法測試目的:

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(); 
0

SQL是一個非常強大的語言。通常你可以用簡單的select語句來完成大部分的事情。即如果你不喜歡觸發器,你可以使用這種方法來插入。

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

表:

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

因爲假設的,我們將有至少1行(*)

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像雙臺擺脫這種存在單列的。如果你希望修改插入語句是直截了當的。讓我知道你是否希望這樣做。

0

一個可能的解決方案是創建一個保存鍵的前綴的輔助表,然後使用插入觸發器的唯一性和排除性約束的組合來強制實現所需的唯一性語義。

在高層次上,這種方法把每一個鍵分解成前綴列表和適用類似的讀者,作家鎖語義的東西:任何數字鍵都可以,只要共享一個前綴沒有按鍵等於的字首。爲了實現這一點,前綴列表包括該標識本身,並將其標記爲終端前綴。

輔助表看起來像這樣。我們使用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的情況,但是也可以通過刪除舊的前綴並插入新的前綴來處理UPDATEDELETE的情況由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"