ข้อ จำกัด - หนึ่งแถวบูลีนเป็นจริงแถวอื่นทั้งหมดเป็นเท็จ


13

ฉันมีคอลัมน์: standard BOOLEAN NOT NULL

ฉันต้องการบังคับใช้หนึ่งแถวจริงและอื่น ๆ ทั้งหมดเป็นเท็จ ไม่มีของ FK หรือสิ่งอื่นใดขึ้นอยู่กับข้อ จำกัด นี้ ฉันรู้ว่าฉันสามารถทำได้ด้วย plpgsql แต่ดูเหมือนว่าค้อนขนาดใหญ่ ฉันต้องการบางสิ่งบางอย่างเช่นCHECKหรือUNIQUEข้อ จำกัด ง่ายกว่าดีกว่า

แถวหนึ่งต้องเป็นจริงพวกเขาไม่สามารถเป็นเท็จได้ทั้งหมด (ดังนั้นแถวแรกที่แทรกจะต้องเป็นจริง)

แถวจะต้องได้รับการอัปเดตซึ่งหมายความว่าฉันต้องรอตรวจสอบข้อ จำกัด จนกว่าการอัปเดตจะเสร็จสิ้นเนื่องจากแถวทั้งหมดอาจถูกตั้งค่าเป็นเท็จก่อนและหนึ่งแถวจริงหลังจากนั้น

มี FK ระหว่างproducts.tax_rate_idและtax_rate.idแต่ไม่มีอะไรเกี่ยวข้องกับอัตราภาษีเริ่มต้นหรือมาตรฐานซึ่งผู้ใช้สามารถเลือกเพื่อสร้างผลิตภัณฑ์ใหม่ได้อย่างง่ายดาย ..

PostgreSQL 9.5 ถ้าเป็นเรื่องสำคัญ

พื้นหลัง

ตารางคืออัตราภาษี อัตราภาษีอย่างใดอย่างหนึ่งคือค่าเริ่มต้น ( standardเนื่องจากค่าเริ่มต้นคือคำสั่ง Postgres) เมื่อมีการเพิ่มผลิตภัณฑ์ใหม่อัตราภาษีมาตรฐานจะถูกนำไปใช้กับผลิตภัณฑ์ หากไม่มีstandardอยู่ฐานข้อมูลต้องทำการเดาหรือเช็คที่ไม่จำเป็นทุกชนิด standardทางออกที่ง่ายผมคิดว่าคือการทำให้แน่ใจว่ามี

ตามค่าเริ่มต้น "" ด้านบนฉันหมายถึงเลเยอร์การนำเสนอ (UI) มีตัวเลือกผู้ใช้สำหรับการเปลี่ยนแปลงอัตราภาษีเริ่มต้น ฉันต้องเพิ่มการตรวจสอบพิเศษเพื่อให้แน่ใจว่า GUI / ผู้ใช้ไม่พยายามตั้งค่า tax_rate_id เป็น NULL หรือเพียงแค่ตั้งอัตราภาษีเริ่มต้น


คุณมีคำตอบไหม
Erwin Brandstetter

ใช่ฉันมีคำตอบขอบคุณมากสำหรับข้อมูลของคุณ @ErwinBrandstetter ตอนนี้ฉันกำลังมุ่งไปสู่ทริกเกอร์ นี่เป็นโครงการโอเพ่นซอร์สตามเวลาของฉัน เมื่อฉันนำไปใช้จริงฉันจะทำเครื่องหมายคำตอบที่ฉันยอมรับ
theGtknerd

คำตอบ:


15

ตัวแปร 1

เนื่องจากทั้งหมดที่คุณต้องการคือคอลัมน์เดียวstandard = trueให้ตั้งค่ามาตรฐานเป็น NULL ในแถวอื่นทั้งหมด ดังนั้นUNIQUEข้อ จำกัดธรรมดาทำงานได้เนื่องจากค่า NULL ไม่ได้ละเมิด:

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

DEFAULTเป็นการแจ้งเตือนเพิ่มเติมว่าแถวแรกที่ป้อนควรเป็นค่าเริ่มต้น มันไม่ได้บังคับอะไร ในขณะที่คุณไม่สามารถตั้งค่ามากกว่าหนึ่งแถวเป็นstandard = trueคุณยังคงสามารถตั้งค่าทุกแถว NULL ไม่มีวิธีที่สะอาดในการป้องกันสิ่งนี้ด้วยข้อ จำกัดเพียงอย่างเดียวในตารางเดียว CHECKข้อ จำกัด ไม่พิจารณาแถวอื่น (โดยไม่มีลูกเล่นสกปรก)

ที่เกี่ยวข้อง:

วิธีอัปเดต:

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

ในการอนุญาตให้ใช้คำสั่งเช่น (โดยที่ข้อ จำกัด นั้นเป็นที่พอใจในตอนท้ายของคำสั่ง):

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

.. UNIQUEจำกัด DEFERRABLEจะต้องมีการ ดู:

dbfiddle ที่นี่

ตัวแปร 2

มีตารางที่สองที่มีแถวเดียวเช่น:

สร้างสิ่งนี้เป็น superuser:

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

ขณะนี้มีแถวเดียวชี้ไปที่มาตรฐานเสมอ (ในกรณีง่าย ๆ นี้ยังแสดงถึงอัตรามาตรฐานโดยตรงด้วย) มีเพียง superuser เท่านั้นที่สามารถทำลายมันได้ คุณอาจไม่อนุญาตด้วยเช่นBEFORE DELETEกัน

dbfiddle ที่นี่

ที่เกี่ยวข้อง:

คุณอาจเพิ่ม a VIEWเพื่อดูเช่นเดียวกับในตัวแปร 1 :

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

ในแบบสอบถามที่ทุกสิ่งที่คุณต้องการคืออัตรามาตรฐานให้ใช้ (เท่านั้น) taxrate_standard.taxrateโดยตรง


คุณเพิ่มในภายหลัง:

มี FK ระหว่างproducts.tax_rate_idและtax_rate.id

การใช้งานตัวแปรที่น่าสงสารของคนจน 2 คือเพียงเพิ่มแถวไปยังproducts(หรือตารางที่คล้ายกัน) ชี้ไปที่อัตราภาษีมาตรฐาน ผลิตภัณฑ์หุ่นที่คุณอาจเรียกว่า "อัตราภาษีมาตรฐาน" - หากการตั้งค่าของคุณอนุญาต

ข้อ จำกัด FK บังคับใช้ความสมบูรณ์ของการอ้างอิง หากต้องการทำให้เสร็จสมบูรณ์ให้บังคับใช้tax_rate_id IS NOT NULLแถว (หากไม่ใช่ในกรณีของคอลัมน์โดยทั่วไป) และไม่อนุญาตให้ลบทิ้ง ทั้งคู่สามารถนำไปใช้เป็นทริกเกอร์ ไม่มีโต๊ะเสริม แต่หรูหราน้อยกว่าและไม่น่าเชื่อถือ


2
ขอแนะนำแนวทางสองตาราง ฉันขอแนะนำให้เพิ่มตัวอย่างแบบสอบถามลงในรูปแบบนั้นเพื่อให้ OP สามารถดูวิธีCROSS JOINเทียบกับมาตรฐานLEFT JOINไปยังเฉพาะและจากนั้นCOALESCEระหว่างทั้งสอง
jpmc26

2
+1 ฉันมีความคิดเดียวกันกับตารางพิเศษ แต่ไม่มีเวลาเขียนคำตอบอย่างถูกต้อง เกี่ยวกับตารางแรกและCONSTRAINT standard_only_1_true UNIQUE (standard): ฉันคิดว่าตารางจะไม่ใหญ่ดังนั้นมันจึงไม่สำคัญ แต่เนื่องจากข้อ จำกัด จะกำหนดดัชนีบนตารางทั้งหมดจะไม่เป็นดัชนีเฉพาะบางส่วนที่WHERE (standard)ใช้พื้นที่น้อยกว่าหรือไม่
ypercubeᵀᴹ

@ ypercubeᵀᴹ: ใช่ดัชนีในตารางทั้งหมดใหญ่กว่านั่นเป็นข้อเสียเปรียบสำหรับตัวแปรนี้ แต่อย่างที่คุณพูด: มันเห็นได้ชัดว่าเป็นโต๊ะเล็ก ๆ ดังนั้นมันจึงไม่ค่อยสำคัญ ฉันตั้งเป้าหมายกับโซลูชันมาตรฐานที่ง่ายที่สุดโดยมีข้อ จำกัด เพียงอย่างเดียว พิสูจน์แนวคิด โดยส่วนตัวแล้วฉันอยู่กับ jpmc26 และชอบตัวแปร 2 มาก
Erwin Brandstetter

9

คุณสามารถใช้ดัชนีที่กรองได้

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);
ข้อผิดพลาด: ค่าคีย์ที่ซ้ำกันละเมิดข้อ จำกัด ที่ไม่ซ้ำกัน "only_one_row_with_column_true_uix"
รายละเอียด: Key (foo) = (t) มีอยู่แล้ว

dbfiddle ที่นี่


แต่อย่างที่คุณบอกว่าแถวแรกจะต้องเป็นจริงจากนั้นคุณสามารถใช้ข้อ จำกัด การตรวจสอบ แต่แม้จะใช้ฟังก์ชั่นคุณสามารถลบแถวแรกในภายหลัง

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);
ข้อผิดพลาด: แถวใหม่สำหรับความสัมพันธ์ "ทดสอบ" ละเมิดข้อ จำกัด การตรวจสอบ "ck_one_true"
รายละเอียด: ความล้มเหลวของแถวมี (5, t)

select * from test;
id | foo
-: | : -
 1 | เสื้อ  
 2 | ฉ  
 3 | ฉ  
 4 | ฉ  
delete from test where id = 1;

dbfiddle ที่นี่


คุณสามารถแก้ไขได้โดยเพิ่มทริกเกอร์ก่อนที่จะลบเพื่อให้แน่ใจว่าแถวแรก (foo เป็นจริง) จะไม่ถูกลบ

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;

ข้อผิดพลาด: ไม่สามารถลบแถวที่ foo เป็นจริงได้

dbfiddle ที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.