Each Answer to this Q is separated by one/two green lines.
I have a setup like this (simplified for this question):
class Employee(models.Model): name = models.CharField(name, unique=True) class Project(models.Model): name = models.CharField(name, unique=True) employees = models.ManyToManyField(Employee)
When an Employee is about to get deleted, I want to check whether or not he is connected to any projects. If so, deletion should be impossible.
I know about signals and how to work them. I can connect to the
pre_delete signal, and make it throw an exception like
ValidationError. This prevents deletion but it is not handled gracefully by forms and such.
This seems like a situation that other will have run into. I’m hoping someone can point out a more elegant solution.
I was looking for an answer to this problem, was not able to find a good one, which would work for both models.Model.delete() and QuerySet.delete(). I went along and, sort of, implementing Steve K’s solution. I used this solution to make sure an object (Employee in this example) can’t be deleted from the database, in either way, but is set to inactive.
It’s a late answer.. just for the sake of other people looking I’m putting my solution here.
Here is the code:
class CustomQuerySet(QuerySet): def delete(self): self.update(active=False) class ActiveManager(models.Manager): def active(self): return self.model.objects.filter(active=True) def get_queryset(self): return CustomQuerySet(self.model, using=self._db) class Employee(models.Model): name = models.CharField(name, unique=True) active = models.BooleanField(default=True, editable=False) objects = ActiveManager() def delete(self): self.active = False self.save()
Employee.objects.active() # use it just like you would .all()
or in the admin:
class Employee(admin.ModelAdmin): def queryset(self, request): return super(Employee, self).queryset(request).filter(active=True)
For those referencing this questions with the same issue with a
ForeignKey relationship the correct answer would be to use Djago’s
on_delete=models.PROTECT field on the
ForeignKey relationship. This will prevent deletion of any object that has foreign key links to it. This will NOT work for for
ManyToManyField relationships (as discussed in this question), but will work great for
So if the models were like this, this would work to prevent the deletion of
Employee object that has one or more
Project object(s) associated with it:
class Employee(models.Model): name = models.CharField(name, unique=True) class Project(models.Model): name = models.CharField(name, unique=True) employees = models.ForeignKey(Employee, on_delete=models.PROTECT)
Documentation can be found HERE.
This would wrap up solution from the implementation in my app. Some code is form LWN’s answer.
There are 4 situations that your data get deleted:
- SQL query
delete()on Model instance:
delete()on QuerySet innstance:
- Deleted by ForeignKey field on other Model
While there is nothing much you can do with the first case, the other three can be fine grained controlled.
One advise is that, in most case, you should never delete the data itself, because those data reflect the history and usage of our application. Setting on
active Boolean field is prefered instead.
delete() on Model instance, subclass
delete() in your Model declaration:
def delete(self): self.active = False self.save(update_fields=('active',))
delete() on QuerySet instance needs a little setup with a custom object manager as in LWN’s answer.
Wrap this up to a reusable implementation:
class ActiveQuerySet(models.QuerySet): def delete(self): self.save(update_fields=('active',)) class ActiveManager(models.Manager): def active(self): return self.model.objects.filter(active=True) def get_queryset(self): return ActiveQuerySet(self.model, using=self._db) class ActiveModel(models.Model): """ Use `active` state of model instead of delete it """ active = models.BooleanField(default=True, editable=False) class Meta: abstract = True def delete(self): self.active = False self.save() objects = ActiveManager()
Usage, just subclass
class Project(ActiveModel): ...
Still our object can still be deleted if any one of its ForeignKey fields get deleted:
class Employee(models.Model): name = models.CharField(name, unique=True) class Project(models.Model): name = models.CharField(name, unique=True) manager = purchaser = models.ForeignKey( Employee, related_name="project_as_manager") >>> manager.delete() # this would cause `project` deleted as well
This can be prevented by adding on_delete argument of Model field:
class Project(models.Model): name = models.CharField(name, unique=True) manager = purchaser = models.ForeignKey( Employee, related_name="project_as_manager", on_delete=models.PROTECT)
CASCADE which will cause your instance deleted, by using
PROTECT instead which will raise a
ProtectedError (a subclass of
IntegrityError). Another purpose of this is that the ForeignKey of data should be kept as a reference.
If you know there will never be any mass employee delete attempts, you could just override
delete on your model and only call
super if it’s a legal operation.
Unfortunately, anything that might call
queryset.delete() will go straight to SQL:
But I don’t see that as much of a problem because you’re the one writing this code and can ensure there are never any
queryset.delete() on employees. Call
I hope deleting employees is relatively rare.
def delete(self, *args, **kwargs): if not self.related_query.all(): super(MyModel, self).delete(*args, **kwargs)
I would like to propose one more variation on LWN and anhdat’s answers wherein we use a
deleted field instead of an
active field and we exclude “deleted” objects from the default queryset, so as to treat those objects as no longer present unless we specifically include them.
class SoftDeleteQuerySet(models.QuerySet): def delete(self): self.update(deleted=True) class SoftDeleteManager(models.Manager): use_for_related_fields = True def with_deleted(self): return SoftDeleteQuerySet(self.model, using=self._db) def deleted(self): return self.with_deleted().filter(deleted=True) def get_queryset(self): return self.with_deleted().exclude(deleted=True) class SoftDeleteModel(models.Model): """ Sets `deleted` state of model instead of deleting it """ deleted = models.NullBooleanField(editable=False) # NullBooleanField for faster migrations with Postgres if changing existing models class Meta: abstract = True def delete(self): self.deleted = True self.save() objects = SoftDeleteManager() class Employee(SoftDeleteModel): ...
Employee.objects.all() # will only return objects that haven't been 'deleted' Employee.objects.with_deleted() # gives you all, including deleted Employee.objects.deleted() # gives you only deleted objects
As stated in anhdat’s answer, make sure to set the
on_delete property on ForeignKeys on your model to avoid cascade behavior, e.g.
class Employee(SoftDeleteModel): latest_project = models.ForeignKey(Project, on_delete=models.PROTECT)
I have a suggestion but I’m not sure it is any better than your current idea. Taking a look at the answer here for a distant but not unrelated problem, you can override in the django admin various actions by essentially deleting them and using your own. So, for example, where they have:
def really_delete_selected(self, request, queryset): deleted = 0 notdeleted = 0 for obj in queryset: if obj.project_set.all().count() > 0: # set status to fail notdeleted = notdeleted + 1 pass else: obj.delete() deleted = deleted + 1 # ...
If you’re not using django admin like myself, then simply build that check into your UI logic before you allow the user to delete the object.
For anyone who finds this and wants to know how you can add PROTECT to you model fields, but have it ignore any soft deleted objects, you can do this by simply overriding the PROTECT that comes with Django:
def PROTECT(collector, field, sub_objs, using): if sub_objs.filter(deleted=False).count() > 0: raise ProtectedError( "Cannot delete some instances of model '%s' because they are " "referenced through a protected foreign key: '%s.%s'" % ( field.remote_field.model.__name__, sub_objs.__class__.__name__, field.name, ), sub_objs.filter(deleted=False), )
This will check whether there are any objects that have not been soft deleted, and only return those objects in the error. This has not been optimized.
Can’t believe it’s been 10 years since I asked this question. A similar issue came up again, and we ended up packing our solution in a small toolkit we use internally. It adds a
ProtectedModelMixin which is related to the question asked here. See https://github.com/zostera/django-marina