Why would I still get RestrictedError in Django when related records deleted?

Viewed 19

I have the following code in a unit test:

        serum_sample.msruns.all().delete()
        print(f"THIS SHOULD BE 0: {serum_sample.msruns.count()}")
        serum_sample.delete()

I get out:

THIS SHOULD BE 0: 0

In the code, I've deleted the related records that have the RESTRICT relationship, but I still get this error:

django.db.models.deletion.RestrictedError: ("Cannot delete some instances of model 'Sample' because they are referenced through restricted foreign keys: 'MSRun.sample'.", {<MSRun: MS run of sample BAT-xz971 with Default by anonymous on 2021-04-29>, <MSRun: MS ...

The relation is:

class MSRun(HierCachedModel, MaintainedModel):

    ...

    sample = models.ForeignKey(
        to="DataRepo.Sample",
        on_delete=models.RESTRICT,
        related_name="msruns",
        help_text="The sample that was run on the mass spectrometer.",
    )

I thought that to be able to delete the sample, all I had to do was delete the MSRun records that link to it...

Why would this not be the case?

UPDATE

I previously deleted this question because I thought I'd tracked it down, but apparently not, so I am adding some more insights/context.

My hunch was in relation to the 2 databases we have: default and validation. (The validation database exists to validate user data submissions and should not be used in the context of this test.)

I'd successfully executed the delete in the validation database explicitly, which is what made me think that maybe I had some cross database foreign keys, but it turned out that it actually worked because there existed no Sample or MSRun records in the validation database (as should be the case). So for context, in the shell:

In [21]: Sample.objects.using("validation").last()

In [22]: Sample.objects.using("default").last()
Out[22]: <Sample: serum-xz971>

In [42]: MSRun.objects.using("default").all()
Out[42]: <QuerySet [<MSRun: MS run of sample BAT-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample Br-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample Dia-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample H-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample Kid-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample Liv-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample Lu-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample Pc-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample Q-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample SI-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample Sol-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample Sp-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample gWAT-xz971 with Default by Anonymous on 2021-06-03>, <MSRun: MS run of sample gas-xz971 with Default by Anonymous on 2021-06-03>]>

In [43]: MSRun.objects.using("validation").all()
Out[43]: <QuerySet []>

The other thing is the parent classes: HierCachedModel and MaintainedModel. The tests worked under HierCachedModel, and they were working on my previous branch that added MaintainedModel. But in my current branch, I added the ability to propagate changes to maintained fields in related child models. Previously, it only propagated changes to parent model records (without issue).

I am now assuming that something is awry with the propagation of child model objects, which occur in a DFS order.

Both HierChachedModel and MaintainedModel override .delete():

class MaintainedModel(Model):

    ...

    def delete(self, *args, **kwargs):
        propagate = kwargs.pop("propagate", True)

        super().delete(*args, **kwargs)

        if auto_updates is False:
            self.buffer_parent_update()
            return

        if propagate:
            self.call_dfs_related_updaters()

class HierCachedModel(Model):

    ...

    def delete(self, *args, **kwargs):
        if caching_updates:
            self.delete_related_caches()
        super().delete(*args, **kwargs)

I have tried calling delete with propagate=False -> same exception.

The propagation of updates simply calls save() on all referenced model objects via foreign keys, following the hierarchy. But nowhere in that code is delete() called, so I am confounded as to why the exception mentions all those MSRun records that link to different samples other than the one I'm deleting (serum-xz971).

Similarly, with HierCachedModel, it deletes caches along the hierarchy from the root down the hierarchical tree.

1 Answers

OK. I figured it out, though I still cannot explain the content of the error message. Of course, I hadn't supplied enough information for anyone else to be able to answer the question, because I couldn't determine the source of the problem. It turned out to be 2 foreign keys between the Animal and Sample tables:

class Animal(MaintainedModel, HierCachedModel, ElementLabel):

    ...

    last_serum_sample = models.ForeignKey(
        to="DataRepo.Sample",
        null=True,
        blank=True,
        on_delete=models.CASCADE,
        related_name="animals",
    )

class Sample(MaintainedModel, HierCachedModel):

    ...

    animal = models.ForeignKey(
        to="DataRepo.Animal",
        on_delete=models.CASCADE,
        null=False,
        related_name="samples",
    )

class MSRun(HierCachedModel, MaintainedModel):

    ...

    sample = models.ForeignKey(
        to="DataRepo.Sample",
        on_delete=models.RESTRICT,
        related_name="msruns",
    )

Previously, Animal.last_serum_sample was a @cached_function/@property, but in my current branch, I changed it into a @maintained_field_function named _last_serum_sample() that spits out a value to update the new model field last_serum_sample which, when the record's (or a hierarchically related record's) .save() or .delete was called, it would use the output of ``. Animal.save() is called to update the new _last_serum_sample() to update last_serum_sample. But it's old value was the sample that was deleted. So there were 2 things going on:

  1. During the update, it was trying to get a sample that was in the process of being deleted, which caused an exception.
  2. The delete of the sample record ultimately fails because there are 2 links to the record that were trying to govern how it is supposed to delete (MSRun.sample was set to RESTRICT and Animal.last_serum_sample was set to CASCADE). And one was doing a save during the other's delete operation.

So, I changed Animal.last_serum_sample.on_delete to SET_NULL instead of CASCADE:

class Animal(MaintainedModel, HierCachedModel, ElementLabel):

    ...

    last_serum_sample = models.ForeignKey(
        to="DataRepo.Sample",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="animals",
    )

and now my test passes.

I also added a catch of the exception when the deleted record's foreign key was attempted to be accessed.

I'm still in the debug process, so I might have more to resolve, but I was quite pleased to have figured out the contention.

Now, I'm left wondering why the problem presented as a RestrictedError that referenced links to that sample record from MSRun records that are in no way related to that sample and/or animal (the relationships are hierarchical: Animal->Sample->MSRun).

Regardless, it seem that triggering autoupdates of related records can run afoul when multiple different model records have conflicting on_delete behaviors.

Related