Monday, July 31, 2017

MyModel.objects.update_or_create() --> Boolean wether data was updated or not?

Leave a Comment

AFAIK Django does not provide a generic way to see if data was changed by update_or_create()

The boolean created tells me that a row was created. But how can I know if data was changed (SQL UPDATE which changed data)

Example:

obj, created = MyModel.update_or_create(pk=12345,                        defaults=dict(name='Doctor Zhivago')) 

There are three cases:

  1. obj was created. No problem, I have the boolean variable created
  2. obj was not created, it was updated. For example the previous name was "Machuca".
  3. obj was not created and not updated. For example the previous name was already "Doctor Zhivago".

I can't distinguish between case2 and case3 at the moment.

3 Answers

Answers 1

Feeling a bit inspired by @bakkal, I've written a mixin class that you can apply to a custom Manager class, then assign that custom Manager to your MyModel class. This changes the format of the returned tuple to (obj, created, updated). In the loop over the values in defaults.items() I am checking if any of the new values are different from the old values.

class UpdateOrCreateMixin(object):     def update_or_create_v2(self, defaults=None, **kwargs):         """         Look up an object with the given kwargs, updating one with defaults         if it exists, otherwise create a new one.         Return a tuple (object, created, updated), where created and updated are          booleans specifying whether an object was created.         """         defaults = defaults or {}         lookup, params = self._extract_model_params(defaults, **kwargs)         self._for_write = True         with transaction.atomic(using=self.db):             try:                 obj = self.select_for_update().get(**lookup)             except self.model.DoesNotExist:                 obj, created = self._create_object_from_params(lookup, params)                 if created:                     return obj, created, False             updated = False             for k, v in defaults.items():                 oldattr = getattr(obj, k)                 if oldattr != (v() if callable(v) else v):                     updated = True                 setattr(obj, k, v() if callable(v) else v)             obj.save(using=self.db)         return obj, False, updated  class CustomManager(UpdateOrCreateMixin, models.Manager):     pass  class MyModel(models.Model)     field = models.CharField(max_length=32)     objects = CustomManager()  obj, created, updated = MyModel.objects.update_or_create_v2(pk=12345,                    defaults=dict(name='Doctor Zhivago')) 

Answers 2

Given how that interface is implemented, there's no way to tell if the object has been updated or not if it's created. Because it always calls save() in that case.

Here's how update_or_create() is currently implemented:

    with transaction.atomic(using=self.db):         try:             obj = self.select_for_update().get(**lookup)         except self.model.DoesNotExist:             obj, created = self._create_object_from_params(lookup, params)             if created:                 return obj, created          # [below part always runs the same if obj was not created]         for k, v in defaults.items():             setattr(obj, k, v() if callable(v) else v)         obj.save(using=self.db)     return obj, False 

Ref. https://github.com/django/django/blob/master/django/db/models/query.py#L459

As you can see when the object is not created, the way it's implemented can't tell if it was updated or not.

However you could manually verify if your lookup/update values are different from those on the existing object in the DB before caling save(), and then set an updated flag.

Answers 3

I'm currently using mixed approach to handling this type of tasks:

I have last_edited or last_modified field on model without auto_now=True.

Instead of auto_now I use django-dirtyfields and inside save method I do something like this:

def save(*args, **kwargs):     if self.is_dirty():         self.last_modified = now()     super().save(*args, **kwargs) 

If you don't like the mess in save method, you can use pre_save signal and update last_modified there based on dirty fields.

Then after calling update_or_create you can pretty safely rely on last_modified field in order to check if model was indeed modified.

dirtyfield's api is pretty simple, you can play with it without too much setup pain.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment