PostgreSQL: обеспечение уникальности записи с проверкой даты валидности

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Как бы вы решали такую задачу? Предположим, есть таблица с купонами, и у купонов есть некая дата устаревания valid_until. Вам надо обеспечить такое ограничение (constraint) на уровне БД, чтобы у одного человека не могло быть одновременно больше одного неустаревшего купона.


Т.е., таблица изначально выглядит так:


CREATE TABLE coupons (
    id  bigint primary key generated by default as identity,
    user_id bigint not null,
    created_at timestamp not null,
    valid_until timestamp not null
)

Попытка решения в лоб


Думаю, всем известно, что есть ключевое слово UNIQUE, с помощью которого можно обеспечить уникальность какого-то поля/набора полей. Но напрямую оно нам не подойдёт, так как в базе могут остаться устаревшие купоны, которые будут конфликтовать со свежими.


Также многим известно, что UNIQUE может идти с условием. Казалось бы, добавить просто к констрейнту WHERE (valid_until > NOW()), и дело с концом. Однако так нельзя: невозможно сделать констрейт по функции, которую заранее не вычислить. Т.е. WHERE (is_valid) бы прокатило, но не сравнение с NOW().


Можно выкрутиться с помощью триггера. Но есть способ поэлегантнее.


Решение через EXCLUDE и tsrange


В постгресе есть тип данных tsrange (диапазон времени), а также ключевое слово EXCLUDE, с помощью которого можно запретить вставку строк, где диапазоны пересекаются. Это можно объединить:


EXCLUDE USING GIST (tsrange(created_at, valid_until) WITH &&)


Но такое ограничение действует на всю таблицу, а не в рамках юзера. Поэтому надо немного расширить выражение, добавив user_id:


EXCLUDE USING GIST (user_id WITH =, tsrange(created_at, valid_until) WITH &&)


Чтобы это работало, нужно сначала включить расширение btree_gist.


Итого проверяем:


CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE coupons (
    id  bigint primary key generated by default as identity,
    user_id bigint not null,
    created_at timestamp not null,
    valid_until timestamp not null,
    EXCLUDE USING GIST (
        user_id WITH =, 
        tsrange(created_at, valid_until) WITH &&
    )
);

INSERT INTO coupons(user_id, created_at, valid_until)
VALUES
(100500, now(), now() + interval '1 day');

INSERT INTO coupons(user_id, created_at, valid_until)
VALUES
    (100501, now(), now() + interval '10 day');

INSERT INTO coupons(user_id, created_at, valid_until)
VALUES
    (100500, now(), now() + interval '10 day');

Последний insert не пройдёт: "ERROR: conflicting key value violates exclusion constraint"


Т.е. по сути решение идет в одну-единственную строчку EXCLUDE.


Решение через триггеры


В проектах Каруны мне иногда доводится писать триггеры, и они всегда многословны. Для данной задачи это выглядело бы примерно так:


CREATE OR REPLACE FUNCTION check_uniq() RETURNS trigger
    LANGUAGE plpgsql AS
$$
BEGIN
    IF (SELECT true
        FROM coupons
        WHERE id != NEW.id
          AND user_id = NEW.user_id
          AND valid_until > NEW.created_at) THEN
        RAISE EXCEPTION 'Coupon for this user already exists';
    END IF;
    RETURN NEW;
END
$$;

CREATE TRIGGER coupon_unique_trigger
    AFTER INSERT
    ON coupons
    FOR EACH ROW
EXECUTE PROCEDURE check_uniq();

В общем, долго и неприятно. Можно еще извратиться и по условному крону явно обновлять состояние валидности купонов (is_valid), тогда можно было бы сделать unique с условием, но это тоже писанина еще та.


Статья написана на основе постов канала Cross Join

Источник: https://habr.com/ru/companies/karuna/articles/794468/


Интересные статьи

Интересные статьи

Недавно столкнулся с лицемерием GitHub, который якобы под предлогом беспокойства о безопасности обычных пользователей начал вводить обязательную двухфакторную аутентификацию. Сперва выборочно только д...
В поисках учетных записей, полных игр и предметов, киберпреступники готовы пойти на любые методы. Начиная от банальной лапши на уши доверчивым игрокам. Продолжая рассылкой сообщений с ссылками, обещая...
Всем привет! После весьма продолжительного периода мы, наконец, подготовили новый релиз SiteAnalyzer, который, надеемся, оправдает ваши ожидания и станет незаменимым помощником в SEO-прод...
Армия США вкладывает средства в нейробиологические исследования, пытаясь расшифровать смысл различных сигналов мозга. Финальная цель исследования (вероятно, она ещё в далеком будущем) сос...
23 ноября в питерских Ткачах прошел DartUP — единственная в России конференция по Dart и Flutter. Это уже третье большое событие, организованное Wrike при поддержке Google. Все началось три г...