Ràng buộc - một hàng boolean là đúng, tất cả các hàng khác sai


13

Tôi có một cột: standard BOOLEAN NOT NULL

Tôi muốn thực thi một hàng Đúng và tất cả các hàng khác Sai. Không có FK hoặc bất cứ điều gì khác tùy thuộc vào ràng buộc này. Tôi biết tôi có thể hoàn thành nó với plpgsql, nhưng điều này có vẻ giống như một búa tạ. Tôi muốn một cái gì đó như một CHECKhoặc UNIQUEràng buộc. Càng đơn giản càng tốt.

Một hàng phải là True, tất cả chúng không thể là Sai (vì vậy hàng đầu tiên được chèn sẽ phải là True).

Hàng sẽ cần được cập nhật, điều đó có nghĩa là tôi phải chờ kiểm tra các ràng buộc cho đến khi cập nhật xong, vì tất cả các hàng có thể được đặt Sai trước và một hàng Đúng sau đó.

Có một FK giữa products.tax_rate_idtax_rate.id, nhưng nó không liên quan gì đến thuế suất mặc định hoặc thuế tiêu chuẩn, người dùng có thể lựa chọn để dễ dàng tạo ra các sản phẩm mới ..

PostgreQuery 9.5 nếu có vấn đề.

Lý lịch

Bảng là thuế suất. Một trong những mức thuế là mặc định ( standardvì mặc định là lệnh Postgres). Khi một sản phẩm mới được thêm vào, thuế suất tiêu chuẩn được áp dụng cho sản phẩm. Nếu không có standard, cơ sở dữ liệu phải đoán hoặc tất cả các loại kiểm tra không cần thiết. Giải pháp đơn giản, tôi nghĩ, là đảm bảo có một standard.

Theo "mặc định" ở trên, tôi có nghĩa là cho lớp trình bày (UI). Có một tùy chọn người dùng để thay đổi mức thuế mặc định. Tôi cần thêm các kiểm tra bổ sung để đảm bảo GUI / người dùng không cố gắng đặt tax_rate_id thành NULL, hoặc sau đó chỉ đặt mức thuế suất mặc định.


Vì vậy, bạn có câu trả lời của bạn?
Erwin Brandstetter

Vâng, tôi có câu trả lời của mình, cảm ơn rất nhiều về đầu vào của bạn, @ErwinBrandstetter. Bây giờ tôi đang nghiêng về một kích hoạt. Đây là một dự án nguồn mở vào thời gian của riêng tôi. Khi tôi thực sự thực hiện nó, tôi sẽ đánh dấu câu trả lời được chấp nhận mà tôi sử dụng.
theGtknerd

Câu trả lời:


15

Biến thể 1

Vì tất cả những gì bạn cần là một cột duy nhất standard = true, hãy đặt tiêu chuẩn thành NULL trong tất cả các hàng khác. Sau đó, một UNIQUEràng buộc đơn giản hoạt động, vì các giá trị NULL không vi phạm nó:

CREATE TABLE taxrate (
   taxrate int PRIMARY KEY
 , standard bool DEFAULT true
 , CONSTRAINT standard_true_or_null CHECK (standard) -- yes, that's the whole constraint
 , CONSTRAINT standard_only_1_true UNIQUE (standard)
);

DEFAULTlà một lời nhắc tùy chọn rằng hàng đầu tiên được nhập sẽ trở thành mặc định. Nó không thực thi bất cứ điều gì. Mặc dù bạn không thể đặt nhiều hơn một hàng thành standard = true, bạn vẫn có thể đặt tất cả các hàng NULL. Không có cách nào rõ ràng để ngăn chặn điều này chỉ với các ràng buộc trong một bảng duy nhất. CHECKcác ràng buộc không xem xét các hàng khác (không có thủ đoạn bẩn).

Liên quan:

Cập nhật:

BEGIN;
UPDATE taxrate SET standard = NULL WHERE standard;
UPDATE taxrate SET standard = TRUE WHERE taxrate = 2;
COMMIT;

Để cho phép một lệnh như (trong đó ràng buộc chỉ được thỏa mãn ở cuối câu lệnh):

WITH kingdead AS (
   UPDATE taxrate
   SET standard = NULL
   WHERE standard
   )
UPDATE taxrate
SET standard = TRUE
WHERE taxrate = 1;

.. các UNIQUEràng buộc sẽ phải được DEFERRABLE. Xem:

dbfiddle ở đây

Biến thể 2

Có một bảng thứ hai với một hàng như:

Tạo cái này như siêu người dùng:

CREATE TABLE taxrate (
   taxrate int PRIMARY KEY
);

CREATE TABLE taxrate_standard (
   taxrate int PRIMARY KEY REFERENCES taxrate
);

CREATE UNIQUE INDEX taxrate_standard_singleton ON taxrate_standard ((true));  -- singleton

REVOKE DELETE ON TABLE taxrate_standard FROM public;  -- can't delete

INSERT INTO taxrate (taxrate) VALUES (42);
INSERT INTO taxrate_standard (taxrate) VALUES (42);

Bây giờ luôn có một hàng duy nhất trỏ đến tiêu chuẩn (trong trường hợp đơn giản này cũng đại diện trực tiếp cho tỷ lệ tiêu chuẩn). Chỉ có một siêu người dùng có thể phá vỡ nó. Bạn cũng có thể không cho phép điều đó với một kích hoạt BEFORE DELETE.

dbfiddle ở đây

Liên quan:

Bạn có thể thêm một VIEWđể xem giống như trong biến thể 1 :

CREATE VIEW taxrate_combined AS
SELECT t.*, (ts.taxrate = t.taxrate) AS standard
FROM   taxrate t
LEFT   JOIN taxrate_standard ts USING (taxrate);

Trong các truy vấn mà tất cả những gì bạn muốn là tỷ lệ tiêu chuẩn, hãy sử dụng (chỉ) taxrate_standard.taxratetrực tiếp.


Sau này bạn đã thêm:

Có một FK giữa products.tax_rate_idtax_rate.id

Việc thực hiện biến thể 2 của một người nghèo sẽ chỉ là thêm một hàng vào products(hoặc bất kỳ bảng tương tự nào) chỉ vào thuế suất tiêu chuẩn; một sản phẩm giả bạn có thể gọi là "Thuế suất tiêu chuẩn" - nếu thiết lập của bạn cho phép.

Các ràng buộc FK thực thi tính toàn vẹn tham chiếu. Để hoàn thành nó, hãy thi hành tax_rate_id IS NOT NULLcho hàng (nếu đó không phải là trường hợp chung cho cột). Và không cho phép xóa nó. Cả hai có thể được đưa vào kích hoạt. Không có bàn phụ, nhưng kém thanh lịch và không đáng tin cậy.


2
Rất khuyến khích cách tiếp cận hai bảng. Tôi cũng đề nghị thêm một truy vấn mẫu vào biến thể đó để OP có thể thấy cách CROSS JOINchống lại tiêu chuẩn, LEFT JOINcụ thể và sau đó COALESCEgiữa hai.
jpmc26

2
+1, tôi có cùng ý tưởng về bảng phụ nhưng không có thời gian để viết câu trả lời đúng. Về bảng đầu tiên và CONSTRAINT standard_only_1_true UNIQUE (standard): Tôi cho rằng bảng sẽ không lớn nên không quan trọng lắm nhưng vì ràng buộc sẽ xác định một chỉ mục trên toàn bộ bảng, nên một chỉ mục duy nhất có WHERE (standard)sử dụng ít không gian hơn?
ypercubeᵀᴹ

@ ypercubeᵀᴹ: Vâng, chỉ số trên toàn bộ bảng lớn hơn, đó là một nhược điểm của biến thể này. Nhưng như bạn đã nói: đó rõ ràng là một cái bàn nhỏ, vì vậy nó hầu như không quan trọng. Tôi đã nhắm đến giải pháp tiêu chuẩn đơn giản nhất chỉ với các ràng buộc. Bằng chứng của khái niệm. Cá nhân, tôi với jpmc26 và rất thích biến thể 2.
Erwin Brandstetter

9

Bạn có thể sử dụng một chỉ mục được lọc

create table test
(
    id int primary key,
    foo bool
);
CREATE UNIQUE INDEX only_one_row_with_column_true_uix 
    ON test (foo) WHERE (foo);  --> where foo is true
insert into test values (1, false);
insert into test values (2, true);
insert into test values (3, false);
insert into test values (4, false);
insert into test values (5, true);
LRI: giá trị khóa trùng lặp vi phạm ràng buộc duy nhất "only_one_row_with_column_true_uix"
CHI TIẾT: Khóa (foo) = (t) đã tồn tại.

dbfiddle ở đây


Nhưng như bạn đã nói, hàng đầu tiên phải đúng, sau đó bạn có thể sử dụng ràng buộc CHECK, nhưng ngay cả khi sử dụng chức năng bạn có thể xóa hàng đầu tiên sau đó.

create function check_one_true(new_foo bool)
returns int as
$$
begin
    return 
    (
        select count(*) + (case new_foo when true then 1 else 0 end)
        from test 
        where foo = true
    );
end
$$
language plpgsql stable;
alter table test 
    add constraint ck_one_true check(check_one_true(foo) = 1); 
insert into test values (1, true);
insert into test values (2, false);
insert into test values (3, false);
insert into test values (4, false);
insert into test values (5, true);
LRI: hàng mới cho "kiểm tra" quan hệ vi phạm ràng buộc kiểm tra "ck_one_true"
CHI TIẾT: Hàng không chứa (5, t).

select * from test;
id | foo
-: | : -
 1 | t  
 2 | f  
 3 | f  
 4 | f  
delete from test where id = 1;

dbfiddle ở đây


Bạn có thể giải quyết nó bằng cách thêm trình kích hoạt TRƯỚC KHI XÓA để đảm bảo hàng đầu tiên (foo là đúng) không bao giờ bị xóa.

create function dont_delete_foo_true()
returns trigger as
$x$
begin
    if old.foo then
        raise exception 'Can''t delete row where foo is true.';
    end if;
    return old;
end;
$x$ language plpgsql;
create trigger trg_test_delete
before delete on test
for each row 
execute procedure dont_delete_foo_true();
delete from test where id = 1;

LRI: Không thể xóa hàng trong đó foo là đúng.

dbfiddle ở đây

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.