Схема с автоматическим приращением на основе 2 столбцов и частично уникальным логическим значением - PullRequest
0 голосов
/ 03 января 2019

Есть 3 таблицы: Пользователь , Вещь , Вещество .

Вещь может быть activeи inactive.Один Пользователь может иметь много вещей, но только один может быть active в то время.Когда вещь становится inactive, она не может быть активирована снова.Пользователь может использовать active Thing, что должно привести к созданию ThingUse с его собственным уникальным идентификатором использования.Я должен, вероятно, отметить, что в конечном итоге будут миллионы, если не миллиарды ThingUse *

Пример:

  1. user1(id: 1) создает thing_1(id: 1, owner: 1, active: true)
  2. user1 использует thing_1, что приводит к thing_use_1(id: 1, thingId: 1, useId: 1)
  3. user1 использует thing_1, что приводит к thing_use_2(id: 2, thingId: 1, useId: 2)
  4. user1 создает thing_2(id: 2, owner: 1, active: true), теперь thing_1 должно стать thing_1(id: 1, owner: 1, active: false)
  5. , с этого момента thing_1 неактивно и больше не может быть использовано / реактивировано
  6. user1 создает thing_use_3(id: 3, thingId: 2, useId: <strong>1</strong>) <- уведомление <code>useId был установлен на 1
  7. user1 создает thing_use_4(id: 4, thingId: 2, useId: 2)
  8. user2(id: 2) создает thing_3(id: 3, owner: 2, active: true)
  9. user2создает thing_use_5(id: 5, thingId: 3, useId: <strong>1</strong>) <- уведомление <code>useId было установлено на 1

На этом этапе БД должна выглядеть следующим образом:

user_1(id: 1)
    thing_1(id: 1, owner: 1, active: false)
        thing_use_1(id: 1, thingId: 1, useId: 1)
        thing_use_2(id: 2, thingId: 1, useId: 2)
    thing_2(id: 2, owner: 1, active: true)
        thing_use_3(id: 3, thingId: 2, useId: 1)
        thing_use_4(id: 4, thingId: 2, useId: 2)

user_2(id: 2)
    thing_3(id: 3, owner: 2, active: true)
        thing_use_5(id: 5, thingId: 3, useId: 1)

Другие примечания:

  • Каждое ThingUse в пределах одного Thing должно иметь уникальный (с автоматическим приращением) useId.
  • Если это не было очевидно из приведенного вышепример отношений между User to Thing и Thing до ThingUse - это один ко многим.

Таким образом, в основном есть 2 проблемы:

Проблема A:

Одиночный Пользователь может иметь только 1 active Вещь в данный момент.Из того, что я прочитал, в MySQL невозможно создать специальную логическую строку, которая допускает один TRUE и несколько FALSES.Однако я придумал 2 «обходных пути».Они оба кажутся немного странными, хотя, возможно, есть лучшие решения:

  1. Создать uniqueIndex(owner, active) на Вещь и использовать true / null как active /inactive флагов, поскольку MySQL не рассматривает пустые значения как дублированные значения.
  2. Вторая идея состоит в том, чтобы рассматривать самого последнего пользователя Thing как active.Проблема в этом случае заключается в том, что это не на 100% прямолинейно по отношению к происходящему + Я предполагаю, что будет сложно / медленно писать запросы типа find all active things across all users.
  3. (добавлено в @ Edit1) Третья идея - удалить столбец active из Thing и добавить столбец activeThingId в таблицу User .Таким образом, для одного пользователя будет невозможно иметь несколько активных вещей одновременно, и будет легко запрашивать «все активные вещи».

Проблема B:

Один Thing не может иметь несколько ThingUse с одним и тем же идентификатором использования.Для этого я могу установить uniqueIndex(thingId, useId) на ThingUse .Это предотвратит случайное использование дубликатов.Однако все еще существует проблема с получением правильного следующего идентификатора использования / установки.Я мог бы технически закодировать эту логику на уровне приложения, но я бы предпочел, чтобы БД обрабатывал ее для меня, если это возможно.

(добавлено @ Edit2 )

Спасибо, @JairSnow, мне удалось частично решить проблему B , используя триггер, основанный на вашей процедуре:

CREATE TRIGGER triggerName
    BEFORE INSERT ON ThingUse
    FOR EACH ROW BEGIN
        SET @actual_value = (SELECT COUNT(*) FROM ThingUse WHERE thingId=NEW.thingId);
        IF (@actual_value IS NULL) THEN
            SET @actual_value = 0;
        END IF;
        SET NEW.useId=@actual_value;
    END

Однако мне пришлось использовать COUNT(*) вместо MAX(useId), потому что по какой-то причинес последним я получал дубликат useId.

Единственная проблема сейчас заключается в том, что если я добавлю ThingUse быстро ex.не дожидаясь завершения других / предыдущих запросов, я получаю страшную тупиковую ошибку: ER_LOCK_DEADLOCK: Deadlock found when trying to get lock; try restarting transaction.

@ Edit1 : добавлено A.3.

@Edit2 : частично решено Проблема B .

1 Ответ

0 голосов
/ 03 января 2019

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

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

Для задачи A вы можете сделать что-то вроде этого:

DELIMITER $$
CREATE PROCEDURE `add_new_thing`(IN `owner` INT)
begin
    INSERT INTO Thing(owner, active) VALUES(owner, 1);
    UPDATE Thing SET active=false WHERE owner=owner AND active=1 AND id != LAST_INSERT_ID();
end $$
DELIMITER ;

Для задача B Вы можете сделать что-то вроде этого:

DELIMITER $$
CREATE PROCEDURE `use_thing`(IN `thing_id` INT)
begin
    SET @actual_value = (SELECT max(useId) FROM ThingUse WHERE thingId=thing_id);
    IF (@actual_value IS NULL) THEN 
        SET @actual_value = 0; 
    END IF;
    INSERT INTO ThingUse(thingId, useId) VALUES(thing_id, @actual_value+1);
end $$
DELIMITER ;

Примечание : Для решения проблемы B обратите внимание, что числовсегда уникален (потому что если вы берете максимальное число и делаете +1 уникальным по логике), но не является автоинкрементом (если вы создаете новую запись, а useId равен 17, вы удаляете эту новую запись и создаете другую запись, для которой useIdпоследняя запись снова будет 17)

@ Edit : Оптимизированный запрос , задача A

...