Обмеження - один булевий рядок є істинним, а всі інші рядки помилковими


13

У мене стовпець: standard BOOLEAN NOT NULL

Я хотів би застосувати один ряд True, а всі інші False. Залежно від цього обмеження немає ФК або нічого іншого. Я знаю, що можу досягти цього за допомогою plpgsql, але це здається кувалдою. Я б хотів щось подібне CHECKчи UNIQUEобмеження. Чим простіше, тим краще.

Один рядок повинен бути True, вони не можуть бути помилковими (тому перший рядок, який вставив, повинен бути True).

Рядок потрібно буде оновити, що означає, що мені доведеться чекати, щоб перевірити обмеження, доки не буде зроблено оновлення, оскільки всі рядки можуть бути встановлені помилковими першими, а один рядок True після цього.

Існує FK між products.tax_rate_idі tax_rate.id, але це не має нічого спільного з типовою або стандартною ставкою податку, яку обирає користувач, щоб полегшити створення нових продуктів.

PostgreSQL 9.5, якщо це має значення.

Фон

Таблиця - ставка податку. Однією із ставок податку є дефолт ( standardоскільки за замовчуванням - це команда Postgres). Коли додається новий продукт, до товару застосовується стандартна ставка податку. Якщо немає standard, база даних повинна або робити здогадки, або всілякі зайві перевірки. Просте рішення, я думав, було переконатися в наявності standard.

Під "за замовчуванням" вище я маю на увазі для шару презентації (UI). Існує варіант користувача для зміни ставки податку за замовчуванням. Мені або потрібно додати додаткові чеки, щоб переконатися, що GUI / користувач не намагається встановити tax_rate_id на NULL, а потім просто встановити ставку податку за замовчуванням.


То ти маєш свою відповідь?
Ервін Брандстеттер

Так, я маю свою відповідь, велике спасибі за ваш внесок, @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

Майте другу таблицю з одним рядком, наприклад:

Створіть це як суперпользователь:

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

Тепер завжди є один рядок, який вказує на стандарт (у цьому простому випадку також безпосередньо представлений стандартний показник). Порушити його міг лише супервайзер. Ви також можете заборонити це за допомогою тригера 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безпосередньо.


Пізніше ви додали:

Існує ФК між products.tax_rate_idтаtax_rate.id

А реалізація бідняка варіанту 2 буде просто додати рядок в products(або будь-яку аналогічну таблицю) , що вказує на стандартні ставки податку; фіктивний товар, який ви можете назвати "Стандартна ставка податку" - якщо ваша установка дозволяє.

Обмеження ФК застосовує референтну цілісність. Щоб завершити його, застосуйте tax_rate_id IS NOT NULLдля рядка (якщо це взагалі не стосується стовпця). І заборонити її видалення. Обидва можуть бути запущені в тригери. Без зайвого столу, але менш елегантний і не такий надійний.


2
Дуже рекомендую підхід з двох таблиць. Я б також запропонував до цього варіанту додати приклад запиту, щоб ОП бачила, як CROSS JOINпроти стандартного, LEFT JOINконкретного, а потім COALESCEміж двома.
jpmc26

2
+1, у мене була така ж думка щодо додаткової таблиці, але немає часу, щоб правильно написати відповідь. Щодо першої таблиці та CONSTRAINT standard_only_1_true UNIQUE (standard): Я вважаю, що таблиця не буде великою, тому вона не має великого значення, але оскільки обмеження визначатиме індекс для всієї таблиці, чи не буде частковим унікальним індексом з WHERE (standard)використанням менше місця?
ypercubeᵀᴹ

@ ypercubeᵀᴹ: Так, індекс у всій таблиці більший, це недолік для цього варіанту. Але, як ви сказали: це, очевидно, крихітний стіл, тому він навряд чи має значення. Я прагнув до найпростішого стандартного рішення з лише обмеженнями. Доказ концепції. Особисто я з jpmc26 і наголошу на варіанті 2.
Ервін Брандстеттер,

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 тут


Але, як ви сказали, перший рядок повинен бути істинним, тоді ви можете використовувати обмеження CHECK, але навіть використовуючи функцію, ви можете видалити перший рядок пізніше.

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);
ПОМИЛКА: новий рядок для відношення "test" порушує обмеження перевірки "ck_one_true"
ДЕТАЛ: Невдалий рядок містить (5, t).

select * from test;
id | foo
-: | : -
 1 | т  
 2 | f  
 3 | f  
 4 | f  
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 тут

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.