Batasan - satu baris boolean benar, semua baris lainnya salah

13

Saya punya kolom: standard BOOLEAN NOT NULL

Saya ingin menegakkan satu baris Benar, dan semua lainnya Salah. Tidak ada FK atau apa pun tergantung pada kendala ini. Saya tahu saya bisa mencapainya dengan plpgsql, tapi ini sepertinya palu godam. Saya lebih suka sesuatu seperti CHECKatau UNIQUEkendala. Semakin sederhana semakin baik.

Satu baris harus Benar, semuanya tidak bisa Salah (jadi baris pertama yang dimasukkan harus Benar).

Baris perlu diperbarui, yang berarti saya harus menunggu untuk memeriksa batasan sampai pembaruan selesai, karena semua baris mungkin ditetapkan Salah dulu dan satu baris Benar setelahnya.

Ada FK antara products.tax_rate_iddan tax_rate.id, tetapi tidak ada hubungannya dengan tarif pajak standar atau standar, yang dapat dipilih pengguna untuk memudahkan menciptakan produk baru ..

PostgreSQL 9.5 jika itu penting.

Latar Belakang

Tabelnya adalah tarif pajak. Salah satu tarif pajak adalah default ( standardkarena default adalah perintah Postgres). Ketika produk baru ditambahkan, tarif pajak standar diterapkan ke produk. Jika tidak ada standard, database harus melakukan tebakan atau semua jenis cek yang tidak diperlukan. Solusi sederhana, saya pikir, adalah untuk memastikan ada standard.

Secara "default" di atas, maksud saya untuk lapisan presentasi (UI). Ada opsi pengguna untuk mengubah tarif pajak standar. Saya juga perlu menambahkan pemeriksaan tambahan untuk memastikan GUI / pengguna tidak mencoba untuk mengatur tax_rate_id ke NULL, atau kemudian hanya menetapkan tarif pajak default.

theGtknerd
sumber
Jadi, apakah Anda punya jawaban?
Erwin Brandstetter
Ya saya punya jawaban, terima kasih banyak atas masukan Anda, @ErwinBrandstetter. Saya condong ke arah pemicu untuk saat ini. Ini adalah proyek open source di waktu saya sendiri. Ketika saya benar-benar mengimplementasikannya, saya akan menandai jawaban yang diterima yang saya gunakan.
theGtknerd

Jawaban:

15

Varian 1

Karena yang Anda butuhkan adalah satu kolom dengan standard = true, tetapkan standar ke NULL di semua baris lainnya. Maka UNIQUEkendala polos berfungsi, karena nilai NULL tidak melanggarnya:

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

DEFAULTadalah pengingat opsional bahwa baris pertama yang dimasukkan harus menjadi default. Itu tidak menegakkan apa pun. Meskipun Anda tidak dapat mengatur lebih dari satu baris standard = true, Anda masih dapat mengatur semua baris NULL. Tidak ada cara bersih untuk mencegah hal ini dengan hanya kendala dalam satu tabel. CHECKkendala tidak mempertimbangkan baris lain (tanpa trik kotor).

Terkait:

Untuk memperbaharui:

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

Untuk mengizinkan perintah seperti (di mana batasan hanya dipenuhi di akhir pernyataan):

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

.. UNIQUEkendala harus DEFERRABLE. Lihat:

Aku di sini

Varian 2

Memiliki tabel kedua dengan satu baris seperti:

Buat ini sebagai 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);

Sekarang selalu ada satu baris yang menunjuk ke standar (dalam kasus sederhana ini juga mewakili tingkat standar secara langsung). Hanya superuser yang bisa memecahkannya. Anda mungkin melarang itu juga, dengan pemicu BEFORE DELETE.

Aku di sini

Terkait:

Anda dapat menambahkan VIEWuntuk melihat sama seperti dalam varian 1 :

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

Dalam kueri di mana yang Anda inginkan adalah kurs standar, gunakan (hanya) taxrate_standard.taxratesecara langsung.


Anda kemudian menambahkan:

Ada FK antara products.tax_rate_iddantax_rate.id

Sebuah implementasi orang miskin dari varian 2 akan hanya menambahkan baris ke products(atau tabel serupa) menunjuk ke tarif pajak standar; produk tiruan yang Anda sebut "Tarif pajak standar" - jika pengaturan Anda memungkinkan.

Kendala FK memberlakukan integritas referensial. Untuk menyelesaikannya, tegakkan tax_rate_id IS NOT NULLuntuk baris (jika itu tidak berlaku untuk kolom secara umum). Dan melarang penghapusannya. Keduanya bisa dimasukkan ke dalam pemicu. Tidak ada meja tambahan, tapi kurang elegan dan tidak bisa diandalkan.

Erwin Brandstetter
sumber
2
Sangat merekomendasikan pendekatan dua tabel. Saya juga menyarankan menambahkan contoh permintaan untuk variasi itu sehingga OP dapat melihat bagaimana CROSS JOINmelawan standar, LEFT JOINke spesifik, dan kemudian di COALESCEantara keduanya.
jpmc26
2
+1, saya memiliki ide yang sama tentang tabel tambahan tetapi tidak ada waktu untuk menulis jawaban dengan benar. Tentang tabel pertama dan CONSTRAINT standard_only_1_true UNIQUE (standard): Saya kira tabel tidak akan besar sehingga tidak masalah banyak tetapi karena kendala akan menentukan indeks pada seluruh tabel, bukankah sebagian indeks unik dengan WHERE (standard)menggunakan lebih sedikit ruang?
ypercubeᵀᴹ
@ ypercubeᵀᴹ: Ya, indeks di seluruh tabel lebih besar, itu adalah kelemahan untuk varian ini. Tapi seperti yang Anda katakan: itu jelas sebuah meja kecil, jadi itu tidak masalah. Saya mengincar solusi standar paling sederhana dengan hanya kendala. Bukti dari konsep. Secara pribadi, saya menggunakan jpmc26 dan sangat menyukai varian 2.
Erwin Brandstetter
9

Anda dapat menggunakan indeks yang difilter

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);
GALAT: nilai kunci duplikat melanggar batasan unik "only_one_row_with_column_true_uix"
DETAIL: Kunci (foo) = (t) sudah ada.

Aku di sini


Tapi seperti yang Anda katakan, baris pertama harus benar, maka Anda bisa menggunakan PERIKSA kendala, tetapi bahkan menggunakan fungsi Anda dapat menghapus baris pertama nanti.

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);
GALAT: baris baru untuk hubungan "test" melanggar batasan periksa "ck_one_true"
RINCIAN: Baris gagal berisi (5, t).

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

Aku di sini


Anda bisa menyelesaikannya dengan menambahkan pemicu SEBELUM DELETE untuk memastikan baris pertama (foo benar) tidak pernah dihapus.

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;

GALAT: Tidak dapat menghapus baris di mana foo benar.

Aku di sini

McNets
sumber