Postgres: как вообще сделать столбец неизменным?

Вот в чем проблема.

create table customer (
  customer_id int generated by default as identity (start with 100) primary key
);
create table cart (
  cart_id int generated by default as identity (start with 100) primary key
);

Я хочу защитить customer_id и cart_id от общего обновления после их вставки. Как?


UPD: Пока писал вопрос, нашел ответ на свой первоначальный вопрос. Вот:

create table cart (
  cart_id int generated by default as identity (start with 100) primary key,
  name text not null,
  at timestamp with time zone
);

create or replace function table_update_guard() returns trigger
language plpgsql immutable parallel safe cost 1 as $body$
begin
  raise exception
    'trigger %: updating is prohibited for %',
    tg_name, tg_argv[0]
    using errcode = 'restrict_violation';
  return null;
end;
$body$;

create or replace trigger cart_update_guard
before update of cart_id, name on cart for each row
-- NOTE: the WHEN clause below is optional
when (
     old.cart_id is distinct from new.cart_id
  or old.name    is distinct from new.name
)
execute function table_update_guard('cart_id, name');

> insert into cart (cart_id, name) values (0, 'prado');
INSERT 0 1
> update cart set cart_id = -1 where cart_id = 0;
ERROR:  trigger cart_update_guard: updating is prohibited for cart_id, name
CONTEXT:  PL/pgSQL function table_update_guard() line 3 at RAISE
> update cart set name = 'nasa' where cart_id = 0;
ERROR:  trigger cart_update_guard: updating is prohibited for cart_id, name
CONTEXT:  PL/pgSQL function table_update_guard() line 3 at RAISE
> update cart set at = now() where cart_id = 0;
UPDATE 1

Предложение WHEN было предложено Belayer в его ответе . Полное объяснение в моем исследовании . Дополнительно я рассмотрел подход к игре с привилегиями. ПРИМЕЧАНИЕ. Некоторые люди говорят, что такие триггеры, как здесь, убивают производительность. Они не правы. Как вы думаете, как postgres реализует внутренние ограничения? — Использование неявных триггеров, как описано здесь.

Все, что вам нужно сделать, это REVOKE привилегию UPDATE от себя, то есть от пользователя, который создал и владеет таблицей.

Laurenz Albe 13.01.2023 09:26

Хммм... 🤔 Похоже, я недостаточно ясно выразился с первой попытки в ответе ниже. Я сделал именно то, что вы говорите, и вуаля, потерпел неудачу.

danissimo 13.01.2023 09:35

Вы отозвали привилегию, которая никогда не предоставлялась (UPDATE в столбце customer_id). Такой REVOKE не имеет никакого эффекта. Вместо этого вам нужно REVOKE UPDATE для всей таблицы, а затем предоставить ее для всех необходимых вам столбцов. Посмотрите на \dp customer в psql, чтобы увидеть фактические разрешения.

Laurenz Albe 13.01.2023 09:41

@LaurenzAlbe, я изучил ваше предложение в новом ответе ниже. Спасибо за вашу точку зрения.

danissimo 16.01.2023 11:15

Тогда вы поняли, что триггер — не лучший способ сделать это, верно? Вам просто нужно не забыть предоставить привилегии для каждого добавляемого столбца. Триггер плохо влияет на производительность и излишне сложен.

Laurenz Albe 16.01.2023 11:24

@LaurenzAlbe, нет, я обнаружил, что триггер - лучший способ.

danissimo 16.01.2023 11:34
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
82
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

TL;DR

Что я пробовал? Отзыв UPDATE привилегии не работает.

# \c danissimo danissimo
You are now connected to database "danissimo" as user "danissimo".

> revoke update (customer_id) on customer from danissimo;
REVOKE
> insert into customer (customer_id) values (0);
INSERT 0 1
> update customer set customer_id = 0 where customer_id = 0;
UPDATE 1
> update customer set customer_id = -1 where customer_id = 0;
UPDATE 1

Ладно, поставим охрану.

create or replace function customer_id_guard() returns trigger
language plpgsql as $body$
begin
  if old.customer_id != new.customer_id then
    raise exception
      'trigger %: updating is prohibited for %',
      tg_name, 'customer_id' using
      errcode = 'restrict_violation';
  end if;
  return new;
end;
$body$;

create or replace trigger customer_id_guard
after update on customer for each row
execute function customer_id_guard();

Теперь давайте дадим им немного работы.

> update customer set customer_id = -1 where customer_id = -1;
UPDATE 1

Правильно, я не менял значение. Как насчет этого:

> update customer set customer_id = 0 where customer_id = -1;
ERROR:  trigger customer_id_guard: updating is prohibited for customer_id
CONTEXT:  PL/pgSQL function customer_id_guard() line 4 at RAISE

Да, вот оно. Хорошо, давайте тоже защитим cart_id. Я не хочу копировать триггерные функции, поэтому попытаюсь обобщить:

create or replace function generated_id_guard() returns trigger
language plpgsql as $body$
declare
  id_col_name text := tg_argv[0];
  equal boolean;
begin
  execute format('old.%1$I = new.%1$I', id_col_name) into equal;
  if not equal then
    raise exception
      'trigger %: updating is prohibited for %',
      tg_name, id_col_name using
      errcode = 'restrict_violation';
  end if;
  return new;
end;
$body$;

create or replace trigger cart_id_guard
after update on cart for each row
execute function generated_id_guard('cart_id');

Как вы могли заметить, я передаю имя столбца функции триггера, генерирую выражение и помещаю результат этого выражения в equal, который затем проверяет.

> insert into cart (cart_id) values (0);
INSERT 0 1
> update cart set cart_id = 0 where cart_id = 0;
ERROR:  syntax error at or near "old"
LINE 1: old.cart_id = new.cart_id
        ^
QUERY:  old.cart_id = new.cart_id
CONTEXT:  PL/pgSQL function generated_id_guard() line 6 at EXECUTE

Хммм... Он прав, что за болтающийся old.cart_id = new.cart_id? Что, если я напишу

execute format('select old.%1$I = new.%1$I', id_col_name) into equal;

> update cart set cart_id = 0 where cart_id = 0;
ERROR:  missing FROM-clause entry for table "old"
LINE 1: select old.cart_id = new.cart_id
               ^
QUERY:  select old.cart_id = new.cart_id
CONTEXT:  PL/pgSQL function generated_id_guard() line 6 at EXECUTE

Правильно, правильно ... Что, если я напишу

declare
  id_old int;
  id_new int;
begin
  execute format('select %I from old', id_col_name) into id_old;
  execute format('select %I from new', id_col_name) into id_new;
  if id_old != id_new then

> update cart set cart_id = 0 where cart_id = 0;
ERROR:  relation "old" does not exist
LINE 1: select cart_id from old
                            ^
QUERY:  select cart_id from old
CONTEXT:  PL/pgSQL function generated_id_guard() line 7 at EXECUTE

Ага, «отношения «старого» не существует»...

Ну и последнее средство:

drop table cart;
create table cart (
  cart_id int generated by default as identity (start with 100) primary key,
  at timestamp with time zone
);
insert into cart (cart_id) values (0);

create or replace function surrogate_id_guard() returns trigger
language plpgsql immutable parallel safe cost 1 as $body$
begin
  raise exception
    'trigger %: updating is prohibited for %',
    tg_name, tg_argv[0] using
    errcode = 'restrict_violation';
  return null;
end;
$body$;

create or replace trigger cart_id_guard
before update of cart_id on cart for each row
execute function surrogate_id_guard('cart_id');

Я просто заставляю его срабатывать при любой попытке обновления cart_id. Давай проверим:

> update cart set cart_id = 0 where cart_id = 0;
ERROR:  trigger cart_id_guard: updating is prohibited for cart_id
CONTEXT:  PL/pgSQL function surrogate_id_guard() line 3 at RAISE
> update cart set at = now() where cart_id = 0;
UPDATE 1

Ну, наконец, я ответил на свой первоначальный вопрос в этот момент. Но все же возникает другой вопрос: как применить тот же алгоритм, закодированный в функции, к столбцам, заданным в аргументах этой функции?

примечание: вы не можете потерять какие-либо права, если являетесь владельцем таблицы. По этой причине отзыв обновления «не работает» в вашем случае. Хорошей практикой является использование разных владельцев объектов базы данных, а не пользователей приложения.

Pavel Stehule 13.01.2023 07:42

@PavelStehule, именно в этом суть. Владелец таблицы по-прежнему может обновлять ее. И да, вы правы лучше послать другого пользователя копаться в БД от имени приложения. Но... есть много других сложных вещей, которые мне нужно решить, так что... пока есть одна

danissimo 13.01.2023 09:14

хорошо. Но ваш дизайн может быть довольно медленным. Наверное, быстрее может быть преобразование новых, старых переменных в jsonb и проверка на равенство через сравнение поля jsonb.

Pavel Stehule 13.01.2023 15:41

Если я правильно понимаю, вы хотите запретить любому пользователю изменять идентификатор таблицы после его установки и чтобы общая функция создавала исключение, при этом разрешая другие обновления. Вы можете добиться этого, изменив триггер, а не функцию. Укажите предикат WHEN для самого триггера. Тогда для таблицы cart:

create or replace trigger cart_id_guard
   before update of cart_id 
       on cart for each row
          when (old.cart_id is distinct from new.cart_id)
       execute function surrogate_id_guard('cart_id');

Для таблицы customer триггером становится:

create or replace trigger customer_id_guard
   before update of customer_id 
       on customer for each row
     when (old.customer_id is distinct from new.customer_id)
  execute function surrogate_id_guard('customer_id');

Сама функция триггера не меняется. (демо здесь)

Большое улучшение, спасибо, @Belayer

danissimo 16.01.2023 10:11

Самая первая попытка в моем предыдущем исследовании заключалась в отзыве привилегий. Как указал Laurenz Albe в своем комментарии, мне пришлось отозвать привилегию на обновление всей таблицы вместо отзыва привилегии на обновление определенного столбца. Вот код:

# \c danissimo danissimo
You are now connected to database "danissimo" as user "danissimo".

create table cart (
  cart_id int generated by default as identity (start with 100) primary key,
  at timestamp with time zone default now()
);
insert into cart default values;

revoke update on cart from danissimo;

Могу ли я обновить таблицу сейчас?

> update cart set at = at - interval '1 day';
ERROR:  permission denied for table cart

Хорошо, давайте предоставим право на обновление столбцов, отличных от cart_id:

> grant update (at) on cart to danissimo;
> update cart set at = at - interval '1 day';
UPDATE 1

Все идет нормально. Теперь время идет, и в конце концов danissimo добавляет еще одну колонку item_ids:

alter table cart add column item_ids int[];

Может ли danissimo обновить новую колонку сейчас? Имейте в виду, что у него была отозвана привилегия на обновление всей таблицы, а привилегия на обновление нового столбца не была предоставлена:

> update cart set item_ids = array[1, 3, 7 ,5];
ERROR:  permission denied for table cart

А если я предоставлю ему привилегию?

> grant update (item_ids) on cart to danissimo;
> update cart set item_ids = array[1, 3, 7 ,5];
UPDATE 1

Что все это значит? Я рассматривал два подхода. Один из них — запретить обновления столбца после того, как ему будет присвоено значение. Другой - играть с привилегиями. В наших проектах обычно мы добавляем новые столбцы по мере развития проектов. Если я придерживаюсь привилегий, я должен предоставлять привилегию для обновления нового столбца каждый раз, когда я добавляю новый. С другой стороны, если я защищаю некоторые столбцы с помощью триггера, я просто добавляю новые столбцы и больше не беспокоюсь.

ВЫВОД: Используйте триггеры, как показано выше 👆🏼.

Другие вопросы по теме