Django - как обработать исключение для ошибки целостности при удалении экземпляра?

У меня есть две модели, транспортное средство и актив, где они связаны отношениями один-к-одному.

Модель транспортного средства:

class Vehicle(commModels.Base):
   
    vehicle_no = models.CharField(max_length=16, unique=True)
    vehicle_type = models.ForeignKey(VehicleType, related_name='%(class)s_vehicle_type', on_delete=models.SET_NULL, default=None, null=True)
    tonage = models.DecimalField(max_digits=4, decimal_places=1, blank=True, default=None, null=True)
    asset = models.OneToOneField(
                        assetModels.Asset,
                        related_name='vehicle',
                        on_delete=models.CASCADE,
                        null=True,
                    )
.....

Модель активов:

reg_no = models.CharField(max_length=100, unique=True)
    name = models.CharField(max_length=255, null=True, blank=True)
    type = models.ForeignKey(AssetType, related_name='%(class)s_type', on_delete=models.SET_NULL, null=True)
   
    user = models.ForeignKey(
        orgModels.Company,
        related_name='%(class)s_user',
        on_delete=models.CASCADE,
        blank=True,
        null=True,
    )

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

Мой код для удаления транспортного средства:

@login_required
def vehicle_delete_view(request, id=None):
        
    obj = get_object_or_404(Vehicle, id=id)

    if request.method == "DELETE":
        try:    
            
            obj.delete() # this code causing an issue
         
            messages.success(request, 'Vehicle has been deleted!')
        except:
            print('exception triger-------')
            messages.error(request, 'Vehicle cannot be deleted!')
            return HTTPResponseHXRedirect(request.META.get('HTTP_REFERER'))

    return HTTPResponseHXRedirect(reverse_lazy('vehicle_list'))

Теперь я создал сигнал, который удаляет актив, связанный с автомобилем, после успешного удаления.

signal.py:

@receiver(post_delete, sender=models.Vehicle)
def delete_asset(sender, instance, *args, **kwargs):
    print('signal is trigger')
    if instance.asset:
        asset = asset_models.Asset.objects.get(id=instance.asset_id)
        if asset: 
            print('asset is being delete')
            asset.delete()

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

Однако проблема, с которой я сталкиваюсь, возникает, если я пытаюсь удалить автомобиль, на который ссылаются другие модели, кроме Asset, тогда это становится проблемой. Проблема заключается в том, что возникает ошибка внешнего ограничения, но мой блок try catch не смог ее поймать. Поскольку он не удалил успешно экземпляр из Vehicle, но все равно срабатывает сигнал. Когда срабатывает сигнал, он просто печатает asset is being delete и остается там навсегда

Я не совсем понимаю, почему post_delete срабатывает в сигнале, поскольку он вообще не удалил автомобиль. Даже когда я пытаюсь вручную удалить его из своей базы данных, я получаю ошибку:

/* ERROR:  canceling statement due to statement timeout
CONTEXT:  while locking tuple (27,15) in relation "vehicle_vehicle" */

Я пытаюсь найти способ, чтобы, если obj.delete() выбросит ошибку (что, очевидно, и произошло), я смог поймать ее и обработать, даже не вызывая сигнал, поскольку автомобиль не удаляется. И поэтому он должен быть в состоянии перенаправить запрос, но вместо этого на моей странице не было никакого ответа, и все равно срабатывает сигнал.

То, что я пытался поймать конкретное исключение, например:

from django.db import IntegrityError
        try:    
            # delete vehicle that are not referenced by other models
            obj.delete()
         
            messages.success(request, 'Vehicle has been deleted!')
        except IntegrityError as e :
            print('exception triger-------')
            print(e.__cause__)
            messages.error(request, 'Vehicle cannot be deleted!')
            return HTTPResponseHXRedirect(request.META.get('HTTP_REFERER'))

Приведенное выше решение не дает никакого результата, он просто запускает мой сигнал и "застревает" там, как обычно.

Как мне справиться с этой проблемой?

Ваш OneToOneField неправильно расположен. Если я правильно понимаю ваши модели, вы сначала создаете транспортное средство, а затем создаете связанный с ним актив. Модель OneToOneField гораздо лучше подходит под модель Asset.

class Asset(models.Model):
    vehicle = models.OneToOneField(
                    Vehicle,
                    related_name='asset',
                    on_delete=models.CASCADE,
                )

Это имеет множество преимуществ:

  1. Вам не нужно создавать Vehicle с помощью asset = None и делать еще один запрос, чтобы установить asset на вновь созданный актив.
  2. Когда вы удаляете Vehicle, удаление Asset будет автоматически обработано Django, поскольку вы определили on_delete=CASCADE.

Таким образом, вам не придется делать странную гимнастику с сигналами, чтобы сохранить целостность между Vehicle и Asset. Сигналы довольно ненадежны для такого рода вещей. Например, ваш сигнал не будет вызван на такое утверждение Vehicle.objects.filter(...).delete(). В вашем случае, я подозреваю, что дело в каких-то круговых зависимостях, которые приводят к бесконечным блокировкам SQL, или сигнал был послан и выполнен в другой транзакции, в то время как первая еще не была зафиксирована.


Если вы действительно хотите придерживаться своего определения OneToOneField в модели Vehicle, я бы рекомендовал вместо использования сигнала выполнять удаление следующим образом:

if obj.asset:
    obj.asset.delete()
    # Deleting the asset will automatically delete the Vehicle as you set on_delete=CASCADE
else:
    obj.delete()

Однако, это всего лишь грязное обходное решение, и перенос вашей модели OneToOneField на модель Asset будет работать гораздо более гладко.

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