Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 360
guettli Avatar asked Jul 17 '17 14:07

guettli


2 Answers

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'))
like image 113
A. J. Parr Avatar answered Nov 15 '22 03:11

A. J. Parr


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.

like image 30
bakkal Avatar answered Nov 15 '22 05:11

bakkal