Как притворяться, что записи упорядочены так же, как номера строк редактора - PullRequest
0 голосов
/ 07 февраля 2020

Речь идет о хранении команд в базе данных вместо текстового файла или электронной таблицы. Я ищу решение, которое имитирует поведение номеров строк в текстовом редакторе.

Например, учитывая таблицу ниже со столбцом «Порядок» в качестве первичного ключа, когда я удаляю вторую строку (Порядок = 2) я бы закончил с пробелом в столбце заказа (1, 3), который нужно исправить до (1, 2)

| Order | Command      |     | Order | Command      |     | Order | Command      |
|-------|--------------|     |-------|--------------|     |-------|--------------|
|   1   | CAM - ON     | ==> |   1   | CAM - ON     | ==> |   1   | CAM - ON     |
|   2   | Turn left    |     |   3   | Take picture |     |   2   | Take picture |
|   3   | Take picture |                                                              

Я уже экспериментировал с триггерами. Перед удалением записи триггер обновляет соответствующие порядковые номера других записей. У меня также есть триггеры для добавления или добавления новой записи «до» существующей.

Я знаю, что физический порядок на диске отличается и не имеет значения. Итак, я просто манипулирую столбцом «Порядок», чтобы имитировать поведение номеров строк

. То же самое отлично работает с обновлением записей. Например, если я хочу «переместить» команду «Повернуть влево» на первую позицию, реализация триггера перед обновлением для изменения порядка других записей тоже помогает. Например, установите порядок команды «Повернуть влево» на 1, и триггер сначала обновит другие записи:

| Order | Command      |     | Order | Command      |
|-------|--------------|     |-------|--------------|
|   1   | CAM - ON     | ==> |   2   | CAM - ON     |
|   2   | Turn left    |     |   1   | Turn left    |
|   3   | Take picture |     |   3   | Take picture |

Однако эта проблема создает проблему. Удаление или вставка записи запускает процедуру, которая обновляет записи, которая теперь запускает процедуру обновления. Это, кажется, делает вещи рекурсивными и сложными.

Я пытался отключить и включить триггер обновления из других процедур триггера, но (в postgresl) это требует изменения таблицы, что недопустимо во время запроса. .

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

Ответы [ 4 ]

2 голосов
/ 08 февраля 2020

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

create table data (order_no integer not null, command text);

alter table data 
   add constraint pk_data 
   primary key (order_no) 
   deferrable initially immediate;

Чтобы удалить строку, используйте:

with removed_row as (
  delete from data
  where ord = 1
  returning ord
)
update data
  set order_no = order_no - 1
where order_no > (select order_no from removed_row);

Чтобы вставить новую строку в посередине вы можете использовать это:

with new_row as (
   insert into data values (3, 'Tilt up')
   returning order_no
)
update data
  set order_no = order_no + 1
where order_no >= (select order_no from new_row);

Перемещение строки также может быть сделано следующим образом:

Чтобы переместить строку вниз:

with to_move(old_no, new_no) as (
  values (5,2)
), move_row as (   
  update data
    set order_no = new_no
  from to_move
  where order_no = old_no
)  
update data
   set order_no = order_no + 1
from to_move
where order_no >= new_no
  and order_no < old_no
;

И переместить строка вверх:

with to_move(old_no, new_no) as (
  values (2,4)
), move_row as (   
  update data
    set order_no = new_no
  from to_move
  where order_no = old_no
)
update data
   set order_no = order_no - 1
from to_move
where order_no > old_no
  and order_no <= new_no 
;
2 голосов
/ 07 февраля 2020

Не беспокойтесь о триггерах: то, что вы ищете, является производным значением, которое вы можете вычислить на лету при необходимости, используя row_number(). Кроме того, обычно не рекомендуется играть с первичным ключом таблицы (именно так выглядит ваш столбец order).

select
    ord original_order,
    row_number() over(order by ord) real_order,
    command
from mytable

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

create view as 
select 
    ord original_order,
    row_number() over(order by ord) real_order
    command
from mytable

Обратите внимание, что order не является разумным выбором для имени столбца, поскольку оно конфликтует с зарезервированным словом. Я переименовал это в ord это вышеупомянутые запросы.

1 голос
/ 15 февраля 2020

Ваши данные не очень подходят для реляционной базы данных - если бы это был я, а число команд не слишком велико, я бы просто сохранил их как JSON или XML clob.

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

Я бы предложил не хранить фактическое число, но столбец ord определяет относительный порядок. Как @GMB уже написал , вы можете использовать row_number для получения последовательных чисел, начинающихся с 1.

Чтобы вставить новые строки без нумерации существующих, оставьте "дыры" в нумерация:

Пусть MIN и MAX будут минимальным и максимальным числом, которое вы хотите использовать в столбце ord. Тогда самая первая вставляемая строка должна получить

ord = (MIN + MAX) / 2

Чтобы вставить новую строку между двумя существующими строками с порядковыми номерами o1 и o2, используйте

ord = (o1 + o2) / 2

В зависимости от ожидаемое количество строк и количество вставок / обновлений, может возникнуть коллизия (т. е. без пробелов между o1 и o2), поэтому для этого случая также должна быть процедура перенумерации. Например, если вы вставляете уже заказанные элементы, это будет очень быстро (после вставки журнала (макс. - мин.)).

Вот псевдокод, чтобы получить номер ord для вставки после данной строки с ord == o1:

let next = SELECT MIN(ord) FROM commands WHERE ord > :o1
if next IS NULL then
    if o1 == MAX     then panic_or_renumber
    if o1 == MAX - 1 then MAX
    else (o1 + MAX) / 2
else
    if next == o1 + 1 then panic_or_renumber
    else (o1 + next) / 2

(Обратите внимание, что (a + b) / 2 может переполниться при использовании арифметики со знаком c. Выберите соответствующие границы или используйте более безопасную арифметику c, если вам нужно огромное количество строк).

0 голосов
/ 11 февраля 2020

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

Хотя это все еще верно, этот вопрос SO показывает, что вы действительно можете использовать функция pg_trigger_depth() для проверки глубины запуска до вызывается процедура запуска. Таким образом, проверка по глубине триггера = 0 позволяет избежать вызова триггера другой процедурой. Например,

CREATE TRIGGER insert_commands_trigger BEFORE INSERT ON commands FOR EACH ROW WHEN (pg_trigger_depth() = 0) EXECUTE PROCEDURE insert_commands();

Возвращаясь к идее использования триггеров, имеет важное преимущество, заключающееся в том, что ни ошибки пользователя, ни ошибки приложения не могут повредить базу данных. Поэтому я хотел бы поделиться своим обновленным решением (с использованием postgresql версии 11.6), которое до сих пор успешно предотвращало все мои попытки создания недействительных данных.

Я переименовал «Порядок», чтобы перейти в соответствии с рекомендациями, но придерживайтесь номера шага в качестве первичного ключа:

CREATE TABLE commands (
    step integer primary key NOT NULL DEFAULT 1 CONSTRAINT positive_order CHECK (step >= 0),
    command character varying
);
ALTER TABLE commands OWNER TO kagan;

CREATE OR REPLACE FUNCTION update_commands()
RETURNS TRIGGER AS $$
DECLARE max_step integer;
DECLARE rec RECORD;
BEGIN
    select max(step) into max_step from commands;
    if NEW.step is null then RAISE EXCEPTION 'step must have a value'; end if;
    if NEW.step < 1 then RAISE EXCEPTION 'step (%) must be >= 1', NEW.step; end if;
    if NEW.step > max_step then RAISE EXCEPTION 'step (%) must be <= max(step) (%)', NEW.step, max_step; end if;

    -- Temporarily, move the current record at the old position "out of the way"
    -- Don't forget the other columns
    UPDATE commands set step = 0, command = NEW.command where step = OLD.step;

    if NEW.step > OLD.step then
        FOR rec IN
            select step from commands
            where step > OLD.step and step <= NEW.step
            order by step ASC
        LOOP
            UPDATE commands set step =  step - 1 where step = rec.step;
        END LOOP;
    else
        FOR rec IN
            select step from commands
            where step >= NEW.step and step < OLD.step
            order by step DESC
        LOOP
            UPDATE commands set step =  step + 1 where step = rec.step;
        END LOOP;
    end if;

    -- Put the current row back to the new position
    UPDATE commands set step = NEW.step where step = 0;
    RETURN NULL;    -- DO NOT PROCEED 
END;
$$ language 'plpgsql';

CREATE OR REPLACE FUNCTION insert_commands()
RETURNS TRIGGER AS $$
DECLARE max_step integer;
DECLARE rec RECORD;
BEGIN
    if NEW.step < 1 then RAISE EXCEPTION 'step (%) must be >= 1)', NEW.step; end if;

    select max(step) into max_step from commands;
    if max_step is null then
        NEW.step = 1;
    elsif NEW.step > max_step + 1 then
        RAISE EXCEPTION 'step (%) must be <= max(step) + 1 (%)', NEW.step, max_step + 1;
    else
        FOR rec IN select step from commands where step >= NEW.step order by step DESC LOOP
            UPDATE commands set step =  step + 1 where step = rec.step;
        END LOOP;
    end if;
    RETURN NEW;
END;
$$ language 'plpgsql';

CREATE OR REPLACE FUNCTION delete_commands()
RETURNS TRIGGER AS $$
DECLARE rec RECORD;
BEGIN
        FOR rec IN select step from commands where step > OLD.step order by step ASC LOOP
            UPDATE commands set step =  step - 1 where step = rec.step;
        END LOOP;
    RETURN OLD;
END;
$$ language 'plpgsql';

CREATE TRIGGER insert_commands_trigger BEFORE INSERT ON commands FOR EACH ROW WHEN (pg_trigger_depth() = 0) EXECUTE PROCEDURE insert_commands();
CREATE TRIGGER delete_commands_trigger AFTER DELETE ON commands FOR EACH ROW WHEN (pg_trigger_depth() = 0) EXECUTE PROCEDURE delete_commands();
CREATE TRIGGER update_commands_trigger BEFORE UPDATE ON Commands FOR EACH ROW WHEN (pg_trigger_depth() = 0) EXECUTE PROCEDURE update_commands();

COPY commands (step, command) FROM stdin;
1   CAM - ON
2   Turn left
3   Take picture
\.
...