Deletion objects that is used as foreign key

2019-02-13 13:24发布

问题:

I have next models:

class Target(models.Model):
    name = models.CharField(max_length=100, blank=False)


class SubTarget(models.Model):
    target = models.ForeignKey(Target, related_name='sub_targets')
    name = models.CharField(max_length=100, blank=True, null=True, default='')

For example, I run next code:

target = Target(name='test-target')
target.save()
sub_target = SubTarget(name='test-sub-target, target=target)
sub_target.save()

So now I have sub_target object with foreign key.

My serializer for target looks like:

class TargetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Target
        fields = ('id', 'name', 'sub_targets')
        depth = 1
        read_only_fields = ('sub_targets',)

and appropriate view:

class TargetDetail(generics.RetrieveUpdateDestroyAPIView):
    model = Target
    serializer_class = TargetSerializer

So, nothing prevents me from deletion just target created object with foreign key. Moreover, this operation delete also related sub_target object. How can I avoid this behaviour ?

回答1:

I'm not sure, but I think you're asking how to prevent SubTarget objects from being deleted when you delete Target objects. By default Django emulates ON DELETE CASCADE. You can control this behavior with the on_delete key word.

So:

class Target(models.Model):
    name = models.CharField(max_length=100, blank=False)


class SubTarget(models.Model):
    target = models.ForeignKey(Target, related_name='sub_targets', 
                               null=True, on_delete=models.SET_NULL)
    name = models.CharField(max_length=100, blank=True, null=True, default='')

Documentation



回答2:

After looking for a way to check if a model instance can be deleted in django , i came across many sample, but was not working as expected. Hope this solution can help.

Let start by creating an Abstract model class which can be inherited by other model

class ModelIsDeletable(models.Model):
    name = models.CharField(max_length=200, blank=True, null=True, unique=True)
    description = models.CharField(max_length=200, blank=True, null=True)
    date_modified = models.DateTimeField(auto_now_add=True)

    def is_deletable(self):
        # get all the related object
        for rel in self._meta.get_fields():
            try:
                # check if there is a relationship with at least one related object
                related = rel.related_model.objects.filter(**{rel.field.name: self})
                if related.exists():
                    # if there is return a Tuple of flag = False the related_model object
                    return False, related
            except AttributeError:  # an attribute error for field occurs when checking for AutoField
                pass  # just pass as we dont need to check for AutoField
        return True, None

     class Meta:
        abstract = True

Example

So let say we have three model Organization and Department and StaffType So many Department can be in an Organization And an Organization has a particular StaffType

class StaffType(ModelIsDeletable):
    pensionable = models.BooleanField(default=False)

class Organization(ModelIsDeletable):
    staff_type = models.ForeignKey(to=StaffType)


class Department(ModelIsDeletable):
    organization = models.ForeignKey(to=Organization, to_field="id")

so let say after adding some information you want to remove an organization model instance that is already tied to a Department

For instance we have Organization Table => (name = Engineering, pk = 1) Department Table => (name=Developer, organization_fk=1, pk=1)

Now when you try to delete an organization after get it with the pk

a_org = Organization.objects.get(pk=1)

With this at hand you can check if it deletable

deletable, related_obj = a_org.is_deletable()

if not deletable:
    # do some stuff with the related_obj list

else:
    # call the delete function
    a_org.delete()


回答3:

You can override the model's delete operation like:

class Target(models.Model):
    name = models.CharField(max_length=100, blank=False)

    def delete(self, *args, **kwargs):

        for robject in self._meta.get_all_related_objects():
            if robject is not None:
                q = Q(**{"%s__id" % robject.field.name: self.id})
                if robject.model.objects.filter(q).exists():
                    raise Exception("Item has active reference.")

        try:
            with transaction.atomic():
                super(Target, self).delete(*args, **kwargs)
        except Exception, exp:
            raise exp

Note that it does not work if related object has no reverse relation to the other one.



回答4:

Maybe you can try another way, I just got it for my project, using Django 1.8

instance = get_object_or_404(MyModel, pk=pk)
eliminate = True
for robject in instance._meta.get_all_related_objects():
    if robject is not None:
        if robject.related_model.objects.filter(**{robject.field.name: instance}).exists() and eliminate:
            eliminate = False

if eliminate:
    instance.delete()
    # additional code
else:
    # additional code
    pass


回答5:

Late reply but this can also be avoided using model.PROTECT on the ForeignKey

target = models.ForeignKey( Target, related_name='sub_targets', on_delete=models.PROTECT )

class BaseModel(models.Model):

    def can_delete(self):
        # get all the related object to be deleted
        for related in get_candidate_relations_to_delete(self._meta):
            field = related.field
            if field.remote_field.on_delete == models.PROTECT:
                # check for relationship with at least one related object
                related = related.related_model.objects.filter(**{related.field.name: self})
                if related.exists():
                    return False, related
        return True, None

    class Meta:
        abstract = True