Удаление

Подтверждение удаления подсчитывает, что произойдёт на самом деле — строки, которые будут удалены каскадно, строки, которые останутся с очищенной связью, и строки, которые полностью блокируют удаление.

Доступно с версии v0.1.0

Большинство админок спрашивают «вы уверены?», а последствия узнают уже постфактум. FastFort обходит граф связей до того, как что-либо записано, и сообщает по каждой связанной модели, что произойдёт на самом деле.

Три эффекта

ЭффектКогдаЧто говорит страница
DELETEкаскад ORM или ON DELETE CASCADE«будет удалено тоже»
CLEARдопускающий NULL внешний ключ, или ON DELETE SET NULL«сохранено, без связи»
PROTECTNOT NULL внешний ключ без каскадаблокирует удаление

Каждый эффект считывается из маппера и ограничения (constraint), а не угадывается.

class Invoice(Base):
    # Declared, so the plan reports DELETE.
    lines: Mapped[list[InvoiceLine]] = relationship(
        back_populates="invoice", cascade="all, delete-orphan"
    )


class InvoiceLine(Base):
    # NOT NULL with no database cascade → PROTECT.
    invoice_id: Mapped[int] = mapped_column(sa.ForeignKey("invoices.id"))


class Zone(Base):
    # Nullable with SET NULL → CLEAR.
    hub_id: Mapped[int | None] = mapped_column(
        sa.ForeignKey("hubs.id", ondelete="SET NULL")
    )

Он сообщает, что сделает ORM, а не то, что вы имели в виду

Это самая важная часть. Модель, которая никогда не объявляла cascade="all, delete-orphan", получает поведение SQLAlchemy по умолчанию: дочерние записи обнуляются, а не удаляются.

FastFort сообщает об этом честно, вместо того чтобы заявлять о каскаде, которого ORM не выполнит. Если в подтверждении написано «сохранено, без связи», а вы ожидали каскад, — подтверждение право, а в модели не хватает объявления.

PROTECT отказывает одним предложением

This customer cannot be deleted: 3 invoice lines would be left without an
invoice.

Не IntegrityError с именем ограничения внутри, возникающий уже после того, как попытка записи была предпринята.

Обнаруживается со стороны потомка

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

Связи «многие-ко-многим» намеренно исключены: удаление товара убирает его строки из product_tags, но не сами теги.

Каскады отслеживаются и честно подсчитываются

На глубину до трёх уровней. Более глубокие уровни достигаются через подзапрос, а не через строки, которые случайно попали в выборку уровнем выше, — иначе второе число оказалось бы долей, напечатанной как итог.

Составной внешний ключ останавливает рекурсию.

Deleting 1 customer will affect:

  Invoices          14  kept, without the link
  Shipments         86  kept, without the link
  Invoice lines     41  will be deleted too       ← reached at depth 2, through
                                                    the invoice cascade

В качестве примера называются пять строк. Подсчёт останавливается на тысяче и отображается как «1000+», вместо того чтобы выполнять COUNT(*) по таблице, в которой могут быть миллионы строк.

Проверяется дважды

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

Встроенные кнопки

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

Удаление через действие

Встроенное действие delete выполняет тот же план для каждой выбранной строки. Если хотя бы одна из них защищена, ничего не удаляется, а страница сообщает, какая именно, — наполовину выполненное массовое удаление хуже, чем не выполненное вовсе, потому что оператору придётся разбираться, какая половина была удалена.

Мягкое удаление

Всё, что описано выше, — это поведение delete по умолчанию: строка удаляется, а граф обходится, чтобы сказать, что уйдёт вместе с ней. ModelAdmin.soft_delete_field заменяет это чем-то более узким — записью вместо удаления, и обходить вообще нечего.

from fastfort import admin


class OrderAdmin(admin.ModelAdmin):
    soft_delete_field = "deleted_at"

Назовите nullable-столбец типа boolean или date/datetime, который уже есть у проекта, — и delete перестаёт вызывать delete адаптера: вместо этого он записывает значение в этот столбец. Никакой миграции, кроме самого столбца, никакой второй таблицы: строка, отправленная в корзину, никогда не покидает ту таблицу, в которой всегда была.

deletion_plan вообще не вызывается. Строка, отправленная в корзину, никуда не делась, поэтому ничему, что на неё ссылается, не нужно каскадироваться, очищаться или блокировать удаление — каждый эффект, описанный выше на этой странице, — это вопрос, который данный процесс больше не задаёт. Страница подтверждения отражает это: никаких предупреждений о связанных записях, а «это действие необратимо» становится «это можно восстановить из корзины».

Список получает представление «Корзина» (?trashed=1, и ссылку на неё рядом с «Добавить»), которое показывает только помеченные строки и предлагает Восстановить вместо Удалить. Булев маркер принимает True/False; маркер типа date или datetime — момент, когда строка была отправлена в корзину, и None. Nullable требуется только от второго типа, но не от первого, — потому что у булева значения уже есть оба состояния.

Сам столбец-маркер никогда не появляется в форме создания или редактирования. Это внутренняя учётная запись конвейера удаления, а не значение, которое кто-то должен задавать вручную.

Как это увидеть

В демо:

/admin/billing.invoice/1/delete      DELETE — the lines cascade
/admin/billing.customer/1/delete     CLEAR at two depths, and a DELETE at depth 2
/admin/logistics.hub/1/delete        CLEAR — the zones are kept