Каковы различные способы добавления ограничения, чтобы можно было вставить только те товары, которые доступны на дату заказа?

#sql #postgresql #constraints

Вопрос:

order.date должен находиться между item.date_from и item.date_to… каковы различные способы сделать это?

 CREATE TABLE "item" (
  "id" SERIAL PRIMARY KEY,
  "date_from" DATE NOT NULL,
  "date_to" DATE NOT NULL
);

CREATE TABLE "order" (
  "id" SERIAL PRIMARY KEY,
  "date" DATE NOT NULL
);

CREATE TABLE "order_item" (
  "order" INTEGER NOT NULL REFERENCES "order",
  "item" INTEGER NOT NULL REFERENCES "item" 
);
 

Комментарии:

1. Обычно это задание для триггера, а не ограничение

2. Как бы я добавил для этого триггер?

3. Замена date_from и date_to на один daterange сделает сравнение проще. Также избегайте date в качестве имени столбца, это ключевое слово. Рассмотрим более описательные ordered_on . Избегайте цитирования имен столбцов в определениях, это делает их чувствительными к регистру, что приведет к труднодоступным проблемам.

4. Для триггера см. раздел Создание триггера , и, поскольку вы, вероятно, в конечном итоге будете использовать plpgsql триггер plpgsql. Другая возможность заключается в том, чтобы ваш внешний код отображал только те товары, которые находятся в диапазоне на дату заказа.

Ответ №1:

Проверяющие ограничения работают с простыми выражениями. Например, простая проверка на вменяемость заказа: check( date > '2010-01-01') . Существуют также ограничения исключения, которые проверяют, что никакие две строки не имеют одного и того же значения, определенного исключением. Но, за исключением ограничений внешнего ключа, ограничения не запрашивают другие таблицы.

Вы можете решить эту проблему с помощью триггера insert и update , и я расскажу об этом ниже, но лучше решать такого рода проблемы с помощью ссылочной целостности. Однако я не могу придумать, как это сделать.


Вы можете просмотреть доступные товары для заказа. Вот $1 дата заказа.

 create temporary view items_available_to_order
  select *
  -- pluralize table names to avoid conflicting with keywords and columns
  from items
  -- date_from and date_to has become a single daterange when_available
  where items.when_available @> $1
 

Затем вставляйте только элементы из этого представления.


Если вы хотите пройти маршрут запуска (вы можете сделать и то, и другое), напишите функцию, которая проверяет, является ли товар заказа действительным. Он либо вызывает исключение, либо возвращает триггер. new является вставленной строкой или строкой после обновления.

Я изменил некоторые имена и типы таблиц и столбцов, чтобы избежать распространенных ошибок.

 create function check_item_order_is_valid()
  returns trigger
  language 'plpgsql'
as $body$
declare
  item_is_available boolean;
begin
  select
    items.when_available @> orders.ordered_on into item_is_available
  from item_orders
  join items on items.id = new.order_id
  join orders on orders.id = new.item_id;

  if( not item_is_available) then
    raise exception 'Item #% is not available for order #%',
      new.item_id, new.order_id;
  end if;

  return new;
end
$body$
 

Затем определите триггер для вызова функции при вставке или обновлении строк в таблице номенклатура/заказ.

 create trigger check_item_orders
  before insert or update
  on item_orders
  for each row
  execute function check_item_order_is_valid();
 

Демонстрация.

Что делать, если допустимый диапазон элемента изменится? Вам нужен триггер обновления товара, чтобы проверить, что его заказы все еще действительны. Может быть. Зависит от вашей бизнес-логики.

Комментарии:

1. Является ли использование маршрута просмотра (без триггера) лучшими практиками? Что делать, если кто-то злонамеренно изменил идентификатор html-формы элемента и добавил элемент, который недоступен?

2. Триггеры @immortal обеспечивают самую сильную защиту. Представление-это моя попытка решить эту проблему с помощью ссылочной целостности, но на самом деле это не сработало.

3. оператор SELECT вызывается для каждой строки. Если одновременно вставляется много строк, есть ли более эффективный способ?

4. @immortal Вы могли бы запустить его for each statement , но тогда функция все равно проверила бы каждую строку. Не думайте об этом, как о запуске select в цикле for в обычной программе, выполнение этого внутри базы данных намного эффективнее. Вы можете добавить when предложение для запуска функции только в случае крайней необходимости. Например, если вы включили триггер обновления order , чтобы перепроверить его элементы, которые вы могли бы добавить when old.ordered_on <> new.ordered_on в «срабатывание» только при ordered_on изменении. Я бы не стал сейчас беспокоиться.

Ответ №2:

Тестовый пример:

 CREATE OR REPLACE FUNCTION public.item_date()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
DECLARE
    order_date date;
    from_date date;
    to_date date;
BEGIN
    select into order_date "date" from "order" where id = new.order;
    select into from_date, to_date date_from, date_to from item where id = new.item;
    --Use date range to test whether order date is in item date range.
    if order_date <@ daterange(from_date, to_date, '[]') then
        return new;
    else
        return null;
    end if;
END;
$function$

create trigger item_date_check before insert or update on order_item for each row execute function item_date();

insert into item values (1, '09/01/2021', '10/31/2021');
insert into item values (2, '07/01/2021', '08/31/2021');
insert into "order" values (1, '09/05/2021');

insert into order_item values (1, 1);
NOTICE:  Order date 2021-09-05, from_date 2021-09-01, to_date 2021-10-31
INSERT 0 1

--Returning NULL causes the INSERT not to happen.
insert into order_item values (1, 2);
NOTICE:  Order date 2021-09-05, from_date 2021-07-01, to_date 2021-08-31
INSERT 0 0


 

Обратите внимание, что мне пришлось процитировать «заказ», так как это тоже зарезервированное слово. Вы можете взглянуть на ключевые(зарезервированные) слова. range Функции/операторы см. в разделе Функция диапазона. Для получения общей информации о range (ов) см. Типы диапазонов