Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to exclude django model fields during a save?

I've got a fairly complicated Django model that includes some fields that should only be saved under certain circumstances. As a simple example,

from django.db import models

class MyModel(models.Model):
    name = models.CharField(max_length=200)
    counter = models.IntegerField(default=0)

    def increment_counter(self):
        self.counter = models.F('counter') + 1
        self.save(update_fields=['counter'])

Here I'm using F expressions to avoid race conditions while incrementing the counter. I'll generally never want to save the value of counter outside of the increment_counter function, as that would potentially undo an increment called from another thread or process.

So the question is, what's the best way to exclude certain fields by default in the model's save function? I've tried the following

def save(self, **kwargs):
    if update_fields not in kwargs:
        update_fields = set(self._meta.get_all_field_names())
        update_fields.difference_update({
            'counter',
        })
        kwargs['update_fields'] = tuple(update_fields)
    super().save(**kwargs)

but that results in ValueError: The following fields do not exist in this model or are m2m fields: id. I could of course just add id and any m2m fields in the difference update, but that then starts to seem like an unmaintainable mess, especially once other models start to reference this one, which will add additional names in self._meta.get_all_field_names() that need to be excluded from update_fields.

For what it's worth, I mostly need this functionality for interacting with the django admin site; every other place in the code could relatively easily call model_obj.save() with the correct update_fields.

like image 565
clwainwright Avatar asked Oct 30 '22 17:10

clwainwright


1 Answers

I ended up using the following:

from django.db import models

class MyModel(models.Model):
    name = models.CharField(max_length=200)
    counter = models.IntegerField(default=0)

    default_save_fields = None

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.default_save_fields is None:
            # This block should only get called for the first object loaded
            default_save_fields = {
                f.name for f in self._meta.get_fields()
                if f.concrete and not f.many_to_many and not f.auto_created
            }
            default_save_fields.difference_update({
                'counter',
            })
            self.__class__.default_save_fields = tuple(default_save_fields)

    def increment_counter(self):
        self.counter = models.F('counter') + 1
        self.save(update_fields=['counter'])    

    def save(self, **kwargs):
        if self.id is not None and 'update_fields' not in kwargs:
            # If self.id is None (meaning the object has yet to be saved)
            # then do a normal update with all fields.
            # Otherwise, make sure `update_fields` is in kwargs.
            kwargs['update_fields'] = self.default_save_fields
        super().save(**kwargs)

This seems to work for my more complicated model which is referenced in other models as a ForeignKey, although there might be some edge cases that it doesn't cover.

like image 158
clwainwright Avatar answered Nov 08 '22 06:11

clwainwright