Auto delete a Django object from the database based on DateTimeField

Viewed 1305

Let's imagine a simple Food model with a name and an expiration date, my goal is to auto delete the object after the expiration date is reached.

I want to delete objects from the database (postgresql in my case) just after exp_date is reached, not filter by exp_date__gt=datetime.datetime.now() in my code then cron/celery once a while a script that filter by exp_date__lt=datetime.datetime.now() and then delete

Food(models.Model):
    name = models.CharField(max_length=200)
    exp_date = models.DateTimeField()

*I could do it with a vanilla view when the object is accessed via an endpoint or even with the DRF like so :

class GetFood(APIView):

    def check_date(self, food):
        """
       checking expiration date   
        """
       
       if food.exp_date <= datetime.datetime.now():
           food.delete()
           return False 

    def get(self, request, *args, **kwargs):

        id = self.kwargs["id"]

        if Food.objects.filter(pk=id).exists():

            food = Food.objects.get(pk=id)
            
            if self.check_date(food) == False:
               return Response({"error": "not found"}, status.HTTP_404_NOT_FOUND)
            else:
                name = food.name 
                return Response({"food":name}, status.HTTP_200_OK)
                
            
        else:
          return Response({"error":"not found"},status.HTTP_404_NOT_FOUND)        

but it would not delete the object if no one try to access it via an endpoint.

*I could also set cronjob with a script that query the database for every Food object which has an expiration date smaller than today and then delete themor even setup Celery. It would indeed just need to run once a day if I was using DateField but as I am using DateTimeField it would need to run every minute (every second for the need of ny project).

*I've also thought of a fancy workaround with a post_save signal with a while loop like :

@receiver(post_save, sender=Food)
def delete_after_exp_date(sender, instance, created, **kwargs):
    if created:
        while instance.exp_date > datetime.datetime.now():
            pass           
     
        else:
           instance.delete()

I don't know if it'd work but it seems very inefficient (if someone could please confirm)

Voila, thanks in advance if you know some ways or some tools to achieve what I want to do, thanks for reading !

1 Answers

I would advice not to delete the objects, or at least not effectively. Sceduling tasks is cumbersome. Even if you manage to schedule this, the time when you remove the items will always be slighlty off the time when you scheduled this from happening. It also means you will make an extra query per element, and not remove the items in bulk. Furthermore scheduling is inherently more complicated: it means you need something to persist the schedule. If later the expiration date of some food is changed, it will require extra logic to "cancel" the current schedule and create a new one. It also makes the system less "reliable": besides the webserver, the scheduler daemon has to run. It can happen that for some reason the daemon fails, and then you will no longer retrieve food that is not expired.

Therefore it might be better to combine filtering the records such that you only retrieve food that did not expire, and remove at some regular interval Food that has expired. You can easily filter the objects with:

from django.db.models.functions import Now

Food.objects.filter(exp_date__gt=Now())

to retrieve Food that is not expired. To make it more efficient, you can add a database index on the exp_date field:

Food(models.Model):
    name = models.CharField(max_length=200)
    exp_date = models.DateTimeField(db_index=True)

If you need to filter often, you can even work with a Manager [Django-doc]:

from django.db.models.functions import Now

class FoodManager(models.Manager):

    def get_queryset(*args, **kwargs):
        return super().get_queryset(*args, **kwargs).filter(
            exp_date__gt=Now()
        )

class Food(models.Model):
    name = models.CharField(max_length=200)
    exp_date = models.DateTimeField(db_index=True)
    
    objects = FoodManager()

Now if you work with Food.objects you automatically filter out all Food that is expired.

Besides that you can make a script that for example runs daily to remove the Food objects that have expired:

from django.db.models import Now

Food._base_manager.filter(exp_date__lte=Now()).delete()
Related