Мягкое удаление¶
Meta.soft_delete_field превращает delete() в пометку строки удалённой: строка остаётся в таблице, и
каждый запрос пропускает её, если только не просит удалённые строки.
class Book(Model):
title = fields.CharField(max_length=200)
deleted_at = fields.DatetimeField(null=True)
class Meta:
soft_delete_field = "deleted_at"
Поле должно существовать в модели и допускать None.
Удаление: delete()¶
delete()становитсяUPDATE, который записываетdeleted_at = now()и в той же транзакции так же помечает связанные строки по каскаду — вместо настоящегоDELETE. Это относится и кQuerySet.filter(...).delete()(массовому удалению), а не только кdelete()одного объекта: для модели, на которую ничто не ссылается, это один массовыйUPDATE; иначе найденные строки и их каскад помечаются партиями — несколько запросов на волну связанных строк, а не на каждую строку. Все строки, которые помечает одинdelete(), включая каскадные, получают одно и то жеdeleted_at. Удаляемые строки сначала блокируются (SELECT ... FOR NO KEY UPDATEв PostgreSQL): строку, которую другойdelete()пометил после того, как этот объект или запрос её прочитал (устаревший объект или два одновременных удаления), удаление не трогает — она сохраняет своё исходноеdeleted_at, повторно по каскаду не обрабатывается и не считается. Устаревший объект получаетdeleted_atстроки.- Строка, до которой доходит каскад, но у модели которой нет своего
soft_delete_field, сохраняется и по-прежнему ссылается на удалённую строку; так же сохраняются строки автоматических промежуточных таблиц связей «многие-ко-многим». Пока строка удалена, они читаются как отсутствие связанной строки (см. Что видят запросы), а после восстановления снова на неё указывают.Meta.soft_delete_hard_cascade = Trueу модели, которую удаляют мягко, заставляет её мягкое удаление удалять их по-настоящему, как при обычном удалении, — тогдаrestore()их не вернёт. В обоих случаях связиSET_NULL/SET_DEFAULTобновляются, аPROTECT/RESTRICTблокируют удаление. .delete()у запроса помечает только ещё живые строки: уже помеченная строка сохраняет своё исходноеdeleted_atи не считается — так же, какinstance.delete()не трогает уже помеченный объект (поэтомуonly_deleted().delete()возвращает0). Проверка объекта читаетdeleted_atиз базы, если.only()/.defer()его не загрузили.- Каскад доходит до связанных строк всех арендаторов и удаляет их — мягко или настоящим
DELETE; текущийTenancy.scope()проверяется только для объекта, у которого вызванdelete(). Если связанная модель переопределяетdelete(), это переопределение вызывается внутриTenancy.scope()арендатора самой строки. - После
delete()/restore()get_dirty_fields()не показывает поляoptimistic_lock_fieldиauto_now, которые мягкое удаление записало вместе сsoft_delete_field.
Восстановление: restore()¶
restore() отменяет пометку, возвращая полю None. По умолчанию возвращается только эта строка.
restore(cascade=True) восстанавливает и строки, которые её мягкое удаление убрало вместе с ней:
каждую строку, до которой ведёт on_delete=CASCADE и у которой всё ещё стоит время удаления этой
строки, — каскад ставит одно время на всё, что удаляет, поэтому это время и определяет удаление.
Строка, мягко удалённая отдельно, раньше или позже, остаётся удалённой, как и строки ниже неё; значения,
перезаписанные SET_NULL/SET_DEFAULT, не возвращаются.
Настоящее удаление: hard_delete()¶
hard_delete() удаляет строку по-настоящему, живую или уже помеченную (очистка корзины); связанные
строки поступают по своему on_delete, как при настоящем DELETE, — строка по on_delete=CASCADE тоже
удаляется по-настоящему, в том числе модели с soft_delete_field и через связь db_constraint=False.
Удаление модели без soft_delete_field распространяется так же. QuerySet.hard_delete() делает то же
для всех строк запроса:
Что видят запросы¶
- Менеджер по умолчанию добавляет
WHERE deleted_at IS NULLк каждому запросу (.filter(),.all(), ...). - Фильтр из другой модели через обратную связь тоже пропускает помеченные связанные строки — и вложенный
(
Author.objects.filter(books__title=...)), и по самой связи (books__isnull=...,books=book,books__in=[...], а также их варианты вexclude()). - Прямая связь, указывающая на помеченную строку (или на строку другого арендатора, или на строку,
которую отсекает собственный
Meta.manager, — см. Разделение данных по арендаторам), читается как отсутствие связанной строки:book.author,select_related()/prefetch_related()иvalues("author__name")даютNone, фильтр через неё (Book.objects.filter(author__name=...)) с ней не совпадает, и проверки связи наNULLс этим согласны —author__isnull=True(иauthor=None,author__not_isnull=False) находит такую книгу, аauthor__isnull=False— нет; так же иauthor__name__isnull=True(author__name=None), потому что у скрытой цели нет видимых колонок. Сама колонка ключа сравнивает записанное значение:author_id__isnull,author=objиauthor_id=...видят ключ как он записан, удалена цель или нет. Сinclude_deleted()/only_deleted()у фильтруемого запроса цель снова видна, поэтомуauthor__isnullтам — простая проверка ключа.
Model.objects.include_deleted() возвращает QuerySet без этого фильтра:
Model.objects.only_deleted() / QuerySet.only_deleted() возвращают только помеченные строки
(WHERE deleted_at IS NOT NULL) — корзина, из которой можно восстановить:
only_deleted() и include_deleted() заменяют друг друга — действует последний вызов
(Book.objects.only_deleted().include_deleted() видит все строки). Как и include_deleted(), он
сохраняет фильтр Meta.tenant_field (чтобы его убрать, добавьте .all_tenants()) и позволяет связям,
доступным из запроса (select_related()/prefetch_related()/фильтр relation__field), тоже видеть
помеченные строки. Он работает и у запроса связанного менеджера: author.books.all().only_deleted().
Запись soft_delete_field¶
QuerySet.update(...),bulk_update(),save(update_fields=[...]),merge().when_matched(update=...)иbulk_create(update_fields=[...])отказываются записыватьsoft_delete_field(QueryError) — используйте.delete()/.restore(), чтобы сработала логика каскада. Обычныйsave()его тоже никогда не записывает.- Вставка-или-обновление (
bulk_create(update_fields=..., on_conflict=...)), конфликтующая с помеченной строкой, обновляет другие колонки этой строки, но оставляет её удалённой — чтобы вернуть, вызовитеrestore(). Так же поступаютsave()иbulk_update()экземпляров, прочитанных до того, как их строки пометили удалёнными в другом месте: оба находят строку только по ключу, как экземпляр был прочитан; сoptimistic_lock_fieldони вместо этого бросаютStaleObjectError. create()иbulk_create()принимают значенияsoft_delete_fieldиMeta.optimistic_lock_field, поэтому строку можно вставить сразу удалённой или с заданной версией — для импорта или восстановления данных, удалённых или версионированных в другом месте.update(),save()иbulk_update()ни то, ни другое не записывают:update()/bulk_update()даютQueryError,save()даёт ошибку при изменённомsoft_delete_field, аoptimistic_lock_fieldсчитает версией для сверки. Не передавайте данные запроса прямо вcreate(**data)/bulk_create()— сначала выберите разрешённые поля, иначе клиент сможет вставить скрытую (помеченную удалённой) строку или заранее задать её версию.
Уникальность среди живых строк¶
hare не переписывает поле с unique=True или запись Meta.constraints так, чтобы исключить
помеченные строки: удалённая книга по-прежнему занимает свой ISBN. Для «уникальности среди неудалённых»
добавьте условие явно:
from hare import Q
from hare.ddl import UniqueConstraint
class Book(Model):
isbn = fields.CharField(max_length=13)
deleted_at = fields.DatetimeField(null=True)
class Meta:
soft_delete_field = "deleted_at"
constraints = (UniqueConstraint(fields=("isbn",), condition=Q(deleted_at__isnull=True)),)
Вместе с optimistic_lock_field¶
soft_delete_field и optimistic_lock_field
сочетаются в одной модели. Мягкое удаление сверяет каждую строку с версией, с которой удаление её
прочитало, и увеличивает версию один раз; одновременное изменение даёт StaleObjectError с
.model/.pk/.expected_version — см. Исключения. Исключение — строка,
мягко удалённая за это время: повторное удаление ничего не делает, как для любой мягко удалённой строки, —
экземпляр получает время удаления строки, какой бы ни стала её версия.
Объект после неудачной записи¶
save()/delete()/restore() обновляют auto_now/auto_now_add и soft_delete_field в объекте
Python заранее, ещё не зная, удалась ли запись. Если запись затем не удалась — StaleObjectError из-за
конфликта optimistic_lock_field или любое другое исключение посреди записи, — эти значения в памяти
возвращаются к тому, что на самом деле в базе, поэтому объект никогда не выглядит сохранённым или
удалённым, если этого не произошло.