Sometimes we need to know who made what changes to which table. This might be required for legal audit purpose or for simple organisational level logging.
There are multiple Django apps available online which can help you log the model changes but there is no fun in doing that.
We will see how to do it without using ready-made app and hence will learn something in the process.
Signals lets a sender notify another receiver that some event have occurred and some action needs to be performed.
For example, we have some data in cache as well in DB. We read data from cache and if not found then goes to DB as fallback. Now whenever a DB is updated, we need to update the cache as well.
But we might update the model from multiple views. Hence it is tough and not clean to write cache update logic in every such view.
Signals comes into picture now. Signal system have two main components. Senders and Receivers.
As name suggests, sender dispatches the signals and receiver receives them to perform some action. To receive a signals, we need to register the receiver function. Every time a model is updates, signal is dispatched and required action is performed by receiver.
Django have a set of built-in signals which sends notification to user code. You can read all about signals in official Django documentation.
For this article, we will require, pre_save
, post_save
and pre_delete
signals which are part of a set of signals sent by Django models. django.db.models.signals
module defines this set of signals.
To log any changes made to some model, we need to use either pre_save
or post_save
signal. We will keep the things simple and then will proceed to specific scenarios slowly.
So for now lets say we need to log changes made to every model.
First create a file signals.py
in your app's root directory.
Create a function save_model_changes
in signals file. This function will work as receiver.
from django.db.models.signals import post_save, pre_delete, pre_save from django.dispatch import receiver # this receiver is executed every-time some data is saved in any table @receiver(pre_save) def audit_log(sender, instance, **kwargs): # code to execute before every model save print("Inside signal code")
To execute the code in receiver function for just one model, define the sender in @receiver
decorator.
@receiver(pre_save, sender=MyModel)
Before we proceed further, we need to tell out app config about the signal we just created. Inside your app.py
file, import the signals.
from django.apps import AppConfig class MyAppConfig(AppConfig): name = 'myapp' def ready(self): # everytime server restarts import myapp.signals
Inside your app's __init__.py
file,
default_app_config = 'myapp.apps.MyAppConfig'
Now our signal is ready. Every-time we save anything to any model, we can see "Inside signal code" being printed in terminal.
Now our signal is ready and working, all we need to do is to write the logic to log the changes in audit table. Create a table where we will be storing the changes.
Table structure may vary as per your application's requirement. My audit table looks something like below.
from django.db import models class ModelChangeLogsModel(models.Model): user_id = models.BigIntegerField(null=False, blank=True, db_index=True) table_name = models.CharField(max_length=132, null=False, blank=True) table_row = models.BigIntegerField(null=False, blank=True) data = models.TextField(null=False, blank=True) action = models.CharField(max_length=16, null=False, blank=True) # saved or deleted timestamp = models.DateTimeField(null=False, blank=True) class Meta: app_label = "myapp" db_table = "model_change_logs"
So I will be storing, which user made the changes to which table and what row number was edited.
Data will contain the snapshot of row in JSON format. Action column will be used to store whether row was updated or deleted.
Now simplest thing will be to store the complete serialised model instance in data column, but that will take a lot of space. You may store only the fields which were changed and can ignore the rest. For this, we need to hit the database once more to retrieve the previous instance of model and compare it with current instance and then save the changed fields.
try: table_pk = instance._meta.pk.name table_pk_value = instance.__dict__[table_pk] query_kwargs = dict() query_kwargs[table_pk] = table_pk_value prev_instance = sender.objects.get(**query_kwargs) # for dynamic column name except ObjectDoesNotExist as e: # this instance is being created and not updated. ignore and return logging.getLogger("info_logger").info("Signals: creating new instance of "+str(sender)) return
We first get the primary key of instance and try to fetch the older instance of model from database.
But if we are not updating the instance, instead we are creating it for first time, we will not find any older instance in DB hence we need to handle it , which is done via try except block above.
You can get the table name as str(instance._meta.db_table)
.
So this was the basics of how to use signals to log the model changes. You can optimise the approach as per your requirement, like tracking only specific/critical models like payments and finances or storing only few specific columns.