2017-06-21 25 views
14

Hãy nói rằng tôi có bảng PostgreSQL sau:PostgreSQL hạn chế sử dụng tiền tố

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

tôi cần phải ngăn chặn hồ sơ chèn với một chìa khóa đó là một tiền tố của phím khác. Ví dụ, tôi sẽ có thể chèn:

  • 'a.b.b'

Nhưng các phím sau đây không nên được chấp nhận:

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

Có cách nào để đạt được điều này - hoặc bằng một ràng buộc hoặc bởi một cơ chế khóa (kiểm tra sự tồn tại trước khi chèn)?

Trả lời

11

Giải pháp này dựa trên PostgreSQL user-defined operators và các ràng buộc loại trừ (base syntax, more details).

LƯU Ý: nhiều thử nghiệm hơn cho thấy giải pháp này không hoạt động (chưa). Xem đáy.

  1. Tạo hàm has_common_prefix (văn bản, văn bản) sẽ tính toán một cách hợp lý những gì bạn cần. Đánh dấu chức năng là 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. Tạo một nhà điều hành cho các chỉ số

    CREATE OPERATOR <~> (
        PROCEDURE = has_common_prefix, 
        LEFTARG = text, 
        RIGHTARG = text, 
        COMMUTATOR = <~> 
    ); 
    
  3. Tạo trừ chế

    CREATE TABLE keys (key text); 
    
    ALTER TABLE keys 
        ADD CONSTRAINT keys_cannot_have_common_prefix 
        EXCLUDE (key WITH <~>); 
    

Tuy nhiên, điểm cuối cùng tạo ra lỗi này:

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. 

Điều này là do tạo chỉ mục PostgreSQL cần các toán tử logic được ràng buộc với phương thức lập chỉ mục vật lý, thông qua các thực thể gọi là "các toán tử". Vì vậy, chúng ta cần phải cung cấp logic rằng:

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 <~>); 

Bây giờ, nó hoạt động:

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). 

LƯU Ý: thử nghiệm nhiều hơn cho thấy giải pháp này không làm việc được nêu: Các INSERT cuối cùng sẽ thất bại.

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

Tôi đã đi theo cách này, sau đó tôi phát hiện ra công ty Postgres rất cũ, không hỗ trợ các ràng buộc 'EXCLUDE'! Hàm bạn muốn trông đơn giản như vị trí ($ 1 trong $ 2)> 0 hoặc vị trí ($ 2 trong $ 1)> 0. –

+0

Bảng có thể có nhiều bản ghi. Cách tiếp cận này có thể sử dụng bất kỳ chỉ mục nào không? –

+0

@Juraj có, tính năng EXCLUDE này luôn đòi hỏi một chỉ mục, do đó ràng buộc là khá nhanh. BTW - giải pháp hiện đã hoàn thành nên hãy kiểm tra nó (nên làm việc trên 9.1+) – filiprem

2

Đây là giải pháp dựa trên KIỂM TRA - nó có thể đáp ứng nhu cầu của bạn.

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. Thật không may, nó không thành công trong một điểm (chèn nhiều hàng).

4

Bạn có thể sử dụng mô-đun ltree để đạt được điều này, nó sẽ cho phép bạn tạo cấu trúc giống cây phân cấp. Cũng sẽ giúp bạn ngăn chặn việc sáng tạo lại bánh xe, tạo ra các biểu thức chính quy phức tạp và v.v. Bạn chỉ cần cài đặt gói postgresql-contrib. Hãy xem:

--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'); 

Tất nhiên tôi không bận tâm đến việc tạo khóa chính và các ràng buộc khác cho thử nghiệm này. Nhưng đừng quên làm như vậy. Ngoài ra, có nhiều hơn nữa trên mô-đun ltree hơn tôi đang hiển thị, nếu bạn cần một cái gì đó khác nhau hãy xem trên tài liệu của nó, có lẽ bạn sẽ tìm thấy câu trả lời ở đó.

4

Bạn có thể thử kích hoạt bên dưới. Xin lưu ý rằng key là từ dự trữ sql. Vì vậy, tôi sẽ đề nghị bạn tránh sử dụng nó như là tên cột trong bảng của bạn. Tôi đã thêm my tạo cú pháp bảng còn cho mục đích thử nghiệm:

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 là một ngôn ngữ rất mạnh mẽ. Thông thường bạn có thể thực hiện hầu hết mọi thứ bằng các câu lệnh chọn đơn giản. I E. nếu bạn không thích trình kích hoạt, bạn có thể sử dụng phương pháp này để chèn của bạn.

Giả thiết duy nhất là tồn tại ít nhất 1 hàng trong bảng. (*)

Bảng:.

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

Do giả định, chúng tôi sẽ có ít nhất 1 hàng (*)

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

Bây giờ sql kỳ diệu. Bí quyết sẽ thay thế giá trị khóa p_key bằng giá trị khóa của bạn để chèn. Tôi đã cố ý không đưa tuyên bố đó vào một thủ tục được lưu trữ. Bởi vì tôi muốn nó được thẳng về phía trước nếu bạn muốn mang nó đến bên ứng dụng của bạn. Nhưng thường đặt sql vào thủ tục lưu trữ là tốt hơn.

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; 

Bây giờ kiểm tra:

-- '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; 

(*) Nếu bạn muốn bạn có thể thoát khỏi sự tồn tại này của hàng duy nhất bằng cách giới thiệu một Oracle như bảng kép. Nếu bạn muốn sửa đổi câu lệnh chèn thì thẳng về phía trước. Hãy cho tôi biết nếu bạn muốn làm như vậy.

0

Một giải pháp khả thi là tạo bảng phụ chứa tiền tố khóa của bạn, sau đó sử dụng kết hợp các ràng buộc duy nhất và loại trừ với trình kích hoạt chèn để thực thi ngữ nghĩa duy nhất mà bạn muốn. Ở mức cao, cách tiếp cận này chia nhỏ mỗi khóa thành danh sách tiền tố và áp dụng điều gì đó tương tự như ngữ nghĩa khóa của người đọc: bất kỳ số nào có thể chia sẻ tiền tố miễn là không có khóa nào = tiếp đầu ngữ. Để thực hiện điều đó, danh sách các tiền tố bao gồm chính khóa đó với một lá cờ đánh dấu nó là tiền tố đầu cuối.

Bảng phụ trông như thế này. Chúng tôi sử dụng một số CHAR thay vì BOOLEAN cho cờ vì sau này chúng tôi sẽ thêm một ràng buộc không hoạt động trên các cột 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')) 
); 

Bây giờ chúng ta sẽ cần phải xác định một kích hoạt trên chèn vào your_table cũng để chèn hàng vào prefixes, chẳng hạn rằng

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

gây

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’); 

Chức năng kích hoạt có thể trông giống như điều này. Tôi chỉ bao gồm trường hợp INSERT ở đây, nhưng chức năng có thể được thực hiện để xử lý UPDATE cũng bằng cách xóa các tiền tố cũ và sau đó chèn các tiền tố mới. Trường hợp DELETE được bao gồm bởi ràng buộc khóa ngoài khóa trên 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; 

Chúng tôi thêm hàm này vào bảng làm trình kích hoạt theo cách thông thường.

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

Một hạn chế loại trừ và một chỉ số duy nhất một phần sẽ thi hành mà liên tiếp nơi is_terminal = ’t’ không thể va chạm với một hàng tiền tố như nhau bất kể giá trị is_terminal của nó, và rằng chỉ có một hàng với 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'; 

Điều này cho phép các hàng mới không xung đột nhưng ngăn những hàng xung đột, bao gồm trong INSERT nhiều hàng.

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" 
Các vấn đề liên quan