5

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.

clwainwright
  • 1,624
  • 17
  • 21
  • Why adding `id` to `difference_update` is a mess? For m2m fields you can always iterate over all `get_fields()` and also exclude them. – Ivan Oct 19 '15 at 22:37
  • @Ivan Sure, that works well for the simple case, but once I start referencing this model as a `ForeignKey` in some other model, then I'll need to add all of those field names into `difference_update` too. This quickly violates DRY. – clwainwright Oct 19 '15 at 22:41
  • I mean generically iterate over all fields and see if they are reverse relationships. I have done this as far as I remember, I will check how exactly. – Ivan Oct 19 '15 at 22:43
  • You can use `hasattr(field,'column')`. It will be `False` if a field is a reverse relationship. I myself actually used the `auto_created` attribute, but the goal was a bit different. – Ivan Oct 19 '15 at 23:03
  • It looks like using `_meta.get_concrete_fields_with_model()` instead of `_meta.get_all_field_names()` might be the way to go, although it's deprecated in Django 1.8. They've got a [migration guide](https://docs.djangoproject.com/en/1.8/ref/models/meta/#migrating-from-the-old-api) that'll help replace it though. – clwainwright Oct 19 '15 at 23:03
  • @Ivan thanks, I'll give it a shot and see if that works. – clwainwright Oct 19 '15 at 23:05
  • Although I am pretty sure that it will do the trick, I am curious about feedback on what you'll actually use. – Ivan Oct 19 '15 at 23:09

2 Answers2

2

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.

clwainwright
  • 1,624
  • 17
  • 21
0

I created a mixin class to make it easy to add to a model, inspired by clwainwright's answer. Though it uses a second mixin class to track which fields have been changed, inspired by this answer.

https://gitlab.com/snippets/1746711

gitaarik
  • 42,736
  • 12
  • 98
  • 105