Какой самый эффективный способ проверки на наличие сирот при удалении объектов в Django?

Допустим, у меня есть модель с таблицей Things и таблицей отношений между вещами, называемой ThingRelations. Не должно быть возможности удалить Thing, если есть ThingRelations, которые указывают на него, и когда больше нет ThingRelations, указывающих на данный Thing, он должен быть удален. В настоящее время я пытаюсь реализовать это с помощью signals.post_delete следующим образом:

from django.db import models

class ThingRelation(models.Model):
    first_thing = models.ForeignKey('Thing', on_delete=models.PROTECT)
    second_thing = models.ForeignKey('Thing', on_delete=models.PROTECT)

class Thing(models.Model):
    name = CharField(max_length=260)

@receiver(models.signals.post_delete, sender=ThingRelation)
def check_relation_and_delete(sender, instance, *args, **kwargs):
    for thing_id in [instance.first_thing, instance.second_thing]:
        first_thing_match = ThingRelation.objects.filter(first_thing=thing_id).exists()
        second_thing_match = ThingRelation.objects.filter(second_thing=thing_id).exists()
        if not first_thing_match and not second_thing_match:
            Thing.objects.get(pk=thing_id).delete()

Является ли это наиболее эффективным способом поиска и удаления осиротевших Things? Я очень новичок в базах данных в целом, но не будет ли фильтрация (потенциально довольно большая) таблицы Things четыре раза для каждого удаленного ThingRelation медленной при удалении многих объектов одновременно? Есть ли какой-то SQL или функциональность Django, которая позволяет не запускать этот код для каждого объекта в массовой операции?

signals.py не предназначены для массовых операций. Также их часто считают анти-паттерном, из-за сложности их отслеживания при попытке отладки некоторой логики.

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

def remove_orphaned_things():
    orphaned_things = Thing.objects.filter(
        Q(id__in=ThingRelation.objects.values_list('first_thing_id').filter(
            first_thing_id=OuterRef('pk')
        ) | Q(id__in=ThingRelation.objects.values_list('second_thing_id').filter(
            second_thing_id=OuterRef('pk')
        )
    ).delete()

Эта функция удаляет все осиротевшие Thing. Осталось только правильно ее вызвать.

Самый простой и понятный способ - организовать его в бесконечную while-true рутину с некоторыми sleep и запускать ее как демон. Например:

from datetime import time
from django.core.management import BaseCommand
# Also some import of `remove_orphaned_things`

class Command(BaseCommand):
    def handle(self, *args, **options):
        while True:
            remove_orphaned_things()
            time.sleep(300)

Таким образом, он будет выполняться почти раз в 5 минут. Его можно запускать с чем-то вроде supervisor (лучше) или tmux (хуже), чтобы вы были уверены, что он всегда выполняется.

Лучше использовать что-то вроде periodic для оркестровки. Вот руководство по установке (вам также понадобится RabbitMQ) и небольшой пример того, как это организовать:

@dramatiq.actor(periodic=cron('*/5 * * * *'), max_retries=0)
def remove_orphaned_things():
    # Exact the same code of `remove_orphaned_things`

Удачи!

Вернуться на верх