HTF
HTF

Reputation: 7260

Django: disable binary logs for delete() method

I have a MySQL master-slave configuration and I want to delete some old records on master only and keep them on slave:

class MonitorQuerySet(models.QuerySet):
    def delete(self, *args, **kwargs):
        with connection.cursor() as cur:
            cur.execute('SET @@session.sql_log_bin = 0')
            ret = super().delete(*args, **kwargs)
            cur.execute('SET @@session.sql_log_bin = 1')
        return ret

class Monitor(models.Model):
...
    objects = models.Manager()
    nobinlog = MonitorQuerySet.as_manager()

The problem with this approach is that now one can do Monitor.nobinlog.delete() and this will wipe out the whole table.

I though about pre_delete and post_delete signals but that will run on each call to delete().

Is there a better solution for this?

Upvotes: 1

Views: 151

Answers (1)

Daniel Hepper
Daniel Hepper

Reputation: 29967

I think you are referring to the fact that you usually have to explicitly spell out Monitor.objects.all().delete(). You can get that behavior by implementing your manager and queryset separately instead of creating the manager from the queryset with as_manager, like this:

class NoBinLogQuerySet(models.QuerySet):
    def delete(self, *args, **kwargs):
        with connection.cursor() as cur:
            cur.execute('SET @@session.sql_log_bin = 0')
            ret = super().delete(*args, **kwargs)
            cur.execute('SET @@session.sql_log_bin = 1')
        return ret

class NoBinLogManager(models.Manager):
    def get_queryset(self):
        return NoBinLogQuerySet(self.model, using=self._db)

class Monitor(models.Model):
    ...
    objects = models.Manager()
    nobinlog = NoBinLogManager()

That doesn't protect you from getting the arguments to .filter() wrong and accidentally deleting your data.

If you have a fixed set of conditions, like "records older than 30 days", I would write a utility method instead of creating an extra manager. In this method, you can hard-code the condition. As an additional safety-measure you can add a dry_run parameter that will return the records that will be deleted. Something like this:

def delete_old_records(dry_run=True):
    kwargs = {'updated_at__lt': now() - timedelta(days=30)}
    if dry_run:
        ret = Monitor.objects.filter(**kwargs)
    else:
        with connection.cursor() as cur:
            cur.execute('SET @@session.sql_log_bin = 0')
            ret = Monitor.objects.delete(**kwargs)
            cur.execute('SET @@session.sql_log_bin = 1')
    return ret

Depending how you execute this method, consider creating a custom management command.

Upvotes: 1

Related Questions