Атомарное многострочное обновление с уникальным ограничением - PullRequest
12 голосов
/ 23 марта 2011

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

create table label (
  id_label serial not null,
  rank integer not null,
  title text not null,
  constraint pri primary key (id_label),
  constraint unq unique (rank)
)

Не имеет значения, является ли это PostgreSQL или MySQL, они демонстрируют одинаковое поведение.Запрос может выглядеть как select title from label order by rank.Предположим, что таблица содержит:

id_label rank title
1        10   Cow
2        20   Apple
3        45   Horse
4        60   Beer

Теперь предположим, что я хочу изменить порядок двух меток, например, чтобы Apple ранжировался до Cow.Самый простой способ - поменять их значения ранга:

update label
set rank = case when rank = 20 then 10 else 20 end
where id_label in (1,2)

Нет.Ни:

update label
set rank = case when rank = 20 then rank - 10 else rank + 10 end
where id_label in (1,2)

Ни даже:

update label
set rank = 30 - rank
where id_label in (1,2)

Каждый раз, когда уникальное ограничение срабатывает при обновлении первой строки и прерывает операцию.Если бы я мог отложить проверку до конца заявления, я был бы в порядке.Это происходит как в PostgreSQL, так и в MySQL.

Обходной путь, безопасный для ACID:

  1. начать транзакцию
  2. выбрать ранги первой, второй записи и самого высокого (max) ранг в таблице (который, возможно, потребует объединения)
  3. обновить первую запись в ранг = max + 1
  4. обновить вторую запись в ранг первой
  5. обновить в первую очередьзапись в ранг второй
  6. commit

Это просто невероятно некрасиво.Хуже того, чтобы удалить ограничение, обновить, а затем воссоздать ограничение.Предоставление таких привилегий оперативной роли вызывает проблемы.Итак, мой вопрос заключается в следующем: есть ли простой метод, который я упустил, который решает эту проблему, или я SOL?

Ответы [ 3 ]

4 голосов
/ 23 марта 2011

С PostgreSQL это можно решить только «хорошим» способом, используя Версию 9.0, потому что вы можете определить уникальные ограничения, которые будут отсрочиваться там.

С PostgreSQL 9.0 вы бы просто сделали:

create table label (
  id_label serial not null,
  rank integer not null,
  title text not null,
  constraint pri primary key (id_label)
);
alter table label add constraint unique_rank unique (rank) 
      deferrable initially immediate;

Тогда обновление так просто:

begin;
set constraints unique_rank DEFERRED;
update rank
   set rank = case when rank = 20 then 10 else 20 end
   where id_label in (1,2);
commit;

Edit:
Если вы не хотите ставить ограничение на отложенное внутри транзакции, вы можете просто определить ограничение как initially deferred.

0 голосов
/ 02 августа 2012

У меня была похожая проблема, и мое решение было следующим:

  1. START TRANSACTION
  2. SELECT * FROM label WHERE id_label IN(1,2)
  3. Delete FROM label WHERE id_label IN(1,2)
  4. INSERT INTO label(all, columns, of, table) VALUES(all, values, we, selected)
  5. COMMIT TRANSACTION

При любых ошибках откат транзакции.

Это можно сделать без удаления уникального ограничения.

0 голосов
/ 23 марта 2011

Конечно, вы можете просто:

update label set rank = 5 where id_label=2

но проблема здесь, я полагаю, заключается в том, что вам нужно уметь справляться со случаем, когда между последовательными рядами нет разрыва. Для postgres использование numeric вместо integer решает проблему, поскольку имеет почти неограниченную точность

create table label (
  id_label serial not null,
  rank numeric not null,
  title text not null,
  constraint pri primary key (id_label),
  constraint unq unique (rank)
)

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

...