Update only specific fields in a models.Model
Asked Answered
T

3

147

I have a model

class Survey(models.Model):
    created_by = models.ForeignKey(User)
    question = models.CharField(max_length=150)
    active = models.NullBooleanField()
    def __unicode__(self):
        return self.question

and now I want to update only the active field. So I do this:

survey = get_object_or_404(Survey, created_by=request.user, pk=question_id)
survey.active = True
survey.save(["active"]) 

Now I get an error IntegrityError: PRIMARY KEY must be unique.

Am I right with this method to update?

Tahr answered 16/12, 2012 at 12:21 Comment(0)
T
279

To update a subset of fields, you can use update_fields:

survey.save(update_fields=["active"]) 

The update_fields argument was added in Django 1.5. In earlier versions, you could use the update() method instead:

Survey.objects.filter(pk=survey.pk).update(active=True)
Tyranny answered 16/12, 2012 at 12:59 Comment(3)
While this answer makes .update() sound deprecated, both update methods are still available and non-deprecated as of Django 4.0. Great answer for mentioning both options, though :-)Alston
This won't update the auto_now field modified_at = models.DateTimeField(auto_now=True) have to specify like below survey.save(update_fields=["active", "modified_at"])Konyn
.update() may not be deprecated, but it apparently does not trigger model_observer, which is important in async implementations. In that case, save(update_fields=[...]) is neededRefugiorefulgence
O
22

Usually, the correct way of updating certain fields in one or more model instances is to use the update() method on the respective queryset. Then you do something like this:

affected_surveys = Survey.objects.filter(
    # restrict your queryset by whatever fits you
    # ...
    ).update(active=True)

This way, you don't need to call save() on your model anymore because it gets saved automatically. Also, the update() method returns the number of survey instances that were affected by your update.

Ornithopter answered 16/12, 2012 at 12:36 Comment(7)
Thanks. I tried it with .get instead of .filter and this doesn't work. But with filter it works nice. Do you know what is wrong with my code above?Tahr
Your problem might be related to question_id. Where does this value come from? And which exact line does raise the IntegrityError?Ornithopter
question_id comes from urls (?P<question_id>\d+). My fault was that on the working server django 1.4 is installed and my code is 1.5. But with your code it's working fine.Tahr
@RegisteredUser, looks like there's no "update" method on objects, just on querysets. When you use .filter(), you get a queryset (holding zero or more objects) back. When you use .get() you get a single object.Gebler
By default, calling save() (@Tyranny solution) is a safer solution, because this method may triggers things like validation, or any custom code, than update() does not.Panslavism
This won't update the auto_now field modified_at = models.DateTimeField(auto_now=True) have to specify like below survey.save(update_fields=["active", "modified_at"])Konyn
.update() apparently does not trigger model_observer, which is important in async implementations. In that case, save(update_fields=[...]) is neededRefugiorefulgence
T
0

You can use this mixin that can be used to detect which fields have changed in a model instance and pass that information to the save method. Here is a description of the mixin:

  • In the init method, the mixin initializes a dictionary called old_instance_data which stores the initial values of the fields in the model instance.
  • In the save method, the mixin compares the current values of the fields with the initial values stored in old_instance_data to determine which fields have changed. It creates a set of update_fields containing the names of the changed fields.
  • If the model instance has a primary key (self.pk is not None), the mixin automatically determines the changed fields if update_fields is not explicitly provided.
  • The mixin then calls the save method of the parent class, passing the update_fields to it, along with any other keyword arguments that may be provided.
  • If you pass your own update_fields parameter when saving, it use this one. Thus you can partially use other update_fields anywhere in your code.

By using this mixin in a model, you can conveniently track and save only the fields that have been modified, which can be helpful for optimizing database updates and reducing unnecessary database queries.

from django.db import models


class UpdateFieldsMixin(models.Model):
    """ Detects which field changed and passes it to save method """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        fields = (field.name for field in self._meta.fields)
        self.old_instance_data = {name: getattr(self, name, None)
                                  for name in fields}

    def save(self, update_fields=None, **kwargs):
        if self.pk:
            update_fields = update_fields or {
                k for k, v in self.old_instance_data.items()
                if getattr(self, k, None) != v}
        return super().save(update_fields=update_fields, **kwargs)

    class Meta:
        abstract = True

Use it like:

class YourModel(UpdateFieldsMixin, models.Model):
    # Any code

    def save(**kwargs):
        # Any code
        return super().save(**kwargs)

That's all. If you have a custom save method, make sure you call UpdateFieldsMixin.save. I'd suggest using super() like in the example above.

Tripetalous answered 6/7, 2023 at 18:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.