За последние 24 часа нас посетили 22753 программиста и 1217 роботов. Сейчас ищут 769 программистов ...

Soft deletion and uniqueness

Тема в разделе "Laravel", создана пользователем artoodetoo, 20 июл 2021.

Метки:
  1. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.076
    Симпатии:
    1.237
    Адрес:
    там-сям
    Предлагаю обсудить тему . Ограничимся Laravel и MySQL для определённости.

    Суть проблемы:
    Допустим у нас есть таблица с профилями пользователей `users` и мы хотим чтобы данные не удалялись совсем, а только помечались как удалённые (поле deleted_at типа timestamp содержит непустое значение). Сложность возникает когда нам нужно уникальное поле, например email. С точки зрения пользователя или бизнеса, будет очень странным если система будет отказывать в регистрации пользователя с неким правильным имейл адресом, хотя такого адреса в системе [ уже ] нет. То есть он мог быть, но был "удалён" и сейчас его как бы нет.

    Я убеждён, что органичения базы данных должны служить последним рубежом валидации и гарантии непротиворечивости данных. Уникальный индекс даёт бо́льшую защиту, чем валидатор на бекенде и тем более на фронтенде. В хорошей системе будут все три барьера.

    Если добавить буквально уникальный индекс на поле email, то "удалённые" и рабочие записи будут равноправны. Не получится добавить запись с email, который уже был, хотя он был "удалён". Получается, что нам надо чтобы активные записи имели уникальные значения, а "удалённые" могли бы быть неуникальны.

    Можно попытаться добавить композитный индекс на email+deleted_at, но тут нас поджидает сюрприз: так как deleted_at может содержать NULL, то записи с пустым полем deleted_at (не удалённые) не будут проверяться на уникальность! По крайней мере в MySQL это работает так. Проверьте или поверьте.

    Есть такое решение: объявить deleted_at как TIMESTAMP NOT NULL или INT NOT NULL. И использовать свой перекрытый трейт MySoftDeletes который вместо null работает с unix epoch, например. Таким образом записи будут уникальными для неудалённых версий и не обязательно уникальными для удалённых, т.к. отметка времени у удаленных разная.

    Другой вариант: использовать отдельное поле deleted INT NOT NULL DEFAULT 0 и при удалении копировать в него значение id. Мягкое удаление оставить как есть, а уникальный индекс объявить по email+deleted.

    И то и другое имеет свои минусы. Ваши предложения?
     
  2. don.bidon

    don.bidon Активный пользователь

    С нами с:
    28 мар 2021
    Сообщения:
    863
    Симпатии:
    132
    Голосую за email+deleted, но я бы предпочёл удалённых пользователей и прочую требуху переносить в таблицы с префиксом "deleted_", если есть такая возможность, иначе работа с актуальными данными будет со временем замедляться.
     
  3. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.076
    Симпатии:
    1.237
    Адрес:
    там-сям
    Это тоже вариант со своими издержками.
     
  4. miketomlin

    miketomlin Старожил

    С нами с:
    9 авг 2016
    Сообщения:
    3.794
    Симпатии:
    650
    Когда пользователь удаляется, предполагается, что его персональная инфа (мыло) удаляется вместе с ним сразу или через опред. промежуток времени. Ну, меняйте при удалении его мыло на <user_id>@yourhost.ru и не парьтесь ;)
    --- Добавлено ---
    P.S. А в течение этого «опред. промежутка времени» пользователю можно давать возможность восстановить акк (не давать возможности регаться с этим мылом, что юник на поле с мылом уже обеспечивает).
    --- Добавлено ---
    Или @example.com :D
     
    #4 miketomlin, 20 июл 2021
    Последнее редактирование: 20 июл 2021
  5. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.076
    Симпатии:
    1.237
    Адрес:
    там-сям
    Ох, нет. Это не вариант. ))) Мягкое удаление придумано как раз для того, чтобы данные сохранялись. users.email здесь просто для примера. Проблема куда более общая.
     
  6. miketomlin

    miketomlin Старожил

    С нами с:
    9 авг 2016
    Сообщения:
    3.794
    Симпатии:
    650
    @artoodetoo, вам никто не запрещает «тырить» данные молча :D

    Как выше написали, удаленные все равно надолго не остаются в осн. таблице ;)
     
  7. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.076
    Симпатии:
    1.237
    Адрес:
    там-сям
    Не знаю откуда такие предположения. Временность не является условием задачи. Мягкое удаление нужно чтобы сохранять данные о "неактивных" или возможно "ошибочных" записях, если важно сохранять о них информацию или иметь возможность "реактивации".
    --- Добавлено ---
    Например, на форуме ты не откроешь профиль забаненного пользователя, но на него по прежнему могут ссылаться его посты, у него есть история и она может пригодиться (компетентным органам lol). Допускаю что великий Админ может даже разбанить (мы не можем).

    Не надо предполагать что "удаленная" информация имеет какой-то срок хранения! И наоборот, срок хранения, если такой понадобится, не надо увязывать с мягким удаленим. Не зря придумали принцип единственной ответственности.

    --- Добавлено ---
    Вот что за манера заваливать топик побочкой! :D
     
  8. miketomlin

    miketomlin Старожил

    С нами с:
    9 авг 2016
    Сообщения:
    3.794
    Симпатии:
    650
    @artoodetoo, меня по-прежнему не покидает ощущение, что ты путаешь какой-то «плэйсхолдер» в таблице (которого если не во всех, то во многих случаях может просто не быть) с записью для хранения какой-то полезной инфы. Но ОК, не буду больше «заливать топик побочкой». Даже если эта «побочка» – бочка, из которой все в основном и пьют :)
    --- Добавлено ---
    Вот мне прям оч. интересно, как «всемогущий Админ» будет «разбанивать» пользователя, чье мыло уже заюзано др. пользователем :)
    – Веришь, что раньше это было мое мыло (акк)?
    – Смотря сколько дашь.

    Я уже молчу о том, что бан и удаление – это немного разные вещи. ЧЁ реально, если ты меня забанишь, я смогу повторно зарегаться с тем же мылом? :)
     
    #8 miketomlin, 20 июл 2021
    Последнее редактирование: 20 июл 2021
  9. mkramer

    mkramer Суперстар
    Команда форума Модератор

    С нами с:
    20 июн 2012
    Сообщения:
    8.555
    Симпатии:
    1.754
    Когда-то делал подмену email на uniqid() при удалении, с сохранением бывшего мыла в другом поле. На том проекте прокатило, хотя и вызывает вопросы решение
     
  10. miketomlin

    miketomlin Старожил

    С нами с:
    9 авг 2016
    Сообщения:
    3.794
    Симпатии:
    650
    Зачем был нужен еще один uniqid, причем сомнительный, когда есть user_id? :) В любом случае это для ТСа «побочка». Он идет своим путем.
    --- Добавлено ---
    Вообще держать «архивные» данные можно где угодно. Но по-моему самое прямое решение – это в спец. таблицах. Там можно и мыло спокойно не делать юником, и оригинальные id при необходимости сохранить (можно даже их использовать в первичном ключе архива), и время удаления, чтобы не захламлять осн. таблицу, и т.д. ЧЁ хоШь, то и храни. У нас везде так.
     
    #10 miketomlin, 20 июл 2021
    Последнее редактирование: 20 июл 2021
  11. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.076
    Симпатии:
    1.237
    Адрес:
    там-сям
    @miketomlin не понял, что за плейсхолдер.

    Я не предполагал что ты так заведешся. :) И недостаточно знаю движок зенфоро, не знаю есть ли в нем более близкий аналог мягкого удаления. Просто пытался найти наглядный пример, наверное неудачно.

    Про архивные таблицы всё ясно, это вариант.
    --- Добавлено ---
    Восстановить пользователя должно быть невозможно, пока есть другой активный аккаунт с таким же адресом. Сначала надо деактивировать тот другой акк.

    Думаю такие пробоемы в публичном сервисе можно решать через подтверждение мыла. Кто сумел, тот и прав.
     
  12. mkramer

    mkramer Суперстар
    Команда форума Модератор

    С нами с:
    20 июн 2012
    Сообщения:
    8.555
    Симпатии:
    1.754
    Чтоб чем-то заполнить поле email, и оно не пересеклось с другими, примерно так:
    PHP:
    1. $deleted->emailBeforeDeletion = $user->email;
    2. $user->email = uniqid();
    А при восстановлении можно обратно переписать правильный майл
     
  13. miketomlin

    miketomlin Старожил

    С нами с:
    9 авг 2016
    Сообщения:
    3.794
    Симпатии:
    650
    Если удаляемый пользователь наплодил какие-то сущности, и ты не хочешь удалять их «каскадом» вслед за пользователем, переносить это творчество в архив или сливать в какой-то спец. акк, ты на месте записи пользователя оставляешь заглушку, уникализированную для всех пользователей, в том числе и будущих, причем так, чтобы она не порождала конфликтов по персональным учетным данным (мыло, телефон и т.п.). Оставляешь разве что ник, если он у тебя есть, чтобы никто не пытался прикидываться удаленным пользователем. Хотя ник тоже можно уникализировать.

    Не хочу, чтобы ты выдумывал более сложную конструкцию. Но если сильно охота использовать групповой ключ, для удаленных сохраняй в deleted вместо возведенного флага ненулевое уник. число (тот же user_id). При большом желании deleted_at тоже можно уникализировать подобным образом (есть фрактальная часть, при нехватке можно и секунды под эти цели задействовать).
     
    #13 miketomlin, 21 июл 2021
    Последнее редактирование: 21 июл 2021
  14. MouseZver

    MouseZver Суперстар

    С нами с:
    1 апр 2013
    Сообщения:
    7.752
    Симпатии:
    1.322
    Адрес:
    Лень
    лучше +unix метку вместе. Иначе история как с md5 приключится, значения разные, а хэш одинаков.
     
  15. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.076
    Симпатии:
    1.237
    Адрес:
    там-сям
    Эта тема будет неполной без правила валидации.
    Это при добавлении новой записи. При правке надо первый NULL заменить на текущее значение id
     
  16. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.076
    Симпатии:
    1.237
    Адрес:
    там-сям
    У меня на работе встретилась подобная реализация и она имеет неожиданный побочный эффект. Там не email, а некий уникальный по природе код, но суть та же: уникальное поле, при мягком удалении значение поля заменяется на новое значение чтобы не мешать создавать новые актуальные записи с тем же кодом.
    Всё это наложилось на процесс синхронизации с партнерской базой. Описываю кейс:

    Процесс синхронизации односторонний - из главной базы в дочерние. Дочка периодически запрашивает изменения, произошедшие с даты D. То есть с прошлой синхронизации. Главный сервер в ответ выдает все записи у которых updated_at > D, в том числе помеченные как удаленные. А на дочке эти записи применятся как Insert or Update в зависимости от присутствия того самого уникального кода. Ибо оно служит идентификатором при синхронизации, а не автоинкрементный ID! Просто и эффективно.
    Вот только с удаленными записями лажа: так как уникальное поле изменилось, то на дочке создается новая запись с новым фиктивным кодом сразу помеченная как удаленная и остается запись со старым кодом как была, неудаленная. Две вместо одной ))) Вывод: нельзя изменять уникальное поле.
     
  17. mkramer

    mkramer Суперстар
    Команда форума Модератор

    С нами с:
    20 июн 2012
    Сообщения:
    8.555
    Симпатии:
    1.754
    @artoodetoo Ну у меня всё проще было. Но даже в таком случае можно всё согласовать, и удалённые проверять по другому полю
     
  18. artoodetoo

    artoodetoo Суперстар
    Команда форума Модератор

    С нами с:
    11 июн 2010
    Сообщения:
    11.076
    Симпатии:
    1.237
    Адрес:
    там-сям
    Ещё один вариант решения: вычисляемое поле not_deleted , которое автоматически становится NULL для удалённых записей. Уникальный индекс по email + not_deleted.
    Код (Text):
    1.  
    2. ALTER TABLE mytable ADD not_deleted int (1) GENERATED ALWAYS AS (IF(deleted_at IS NULL,  1, NULL)) VIRTUAL;
    3. ALTER TABLE mytable ADD CONSTRAINT UNIQUE (email, not_deleted);
    Вычисляемые поля доступны в MySQL с версии 5.7.6
    В MariaDB индекс по вычисляемому виртуальному полю доступен с версии 10.2.3