Wie können Sie beim Speichern überprüfen, ob sich ein Feld geändert hat?


293

In meinem Modell habe ich:

class Alias(MyBaseModel):
    remote_image = models.URLField(max_length=500, null=True, help_text="A URL that is downloaded and cached for the image. Only
 used when the alias is made")
    image = models.ImageField(upload_to='alias', default='alias-default.png', help_text="An image representing the alias")


    def save(self, *args, **kw):
        if (not self.image or self.image.name == 'alias-default.png') and self.remote_image :
            try :
                data = utils.fetch(self.remote_image)
                image = StringIO.StringIO(data)
                image = Image.open(image)
                buf = StringIO.StringIO()
                image.save(buf, format='PNG')
                self.image.save(hashlib.md5(self.string_id).hexdigest() + ".png", ContentFile(buf.getvalue()))
            except IOError :
                pass

Welches funktioniert großartig zum ersten Mal die remote_imageÄnderungen.

Wie kann ich ein neues Bild abrufen, wenn jemand remote_imageden Alias geändert hat ? Und zweitens, gibt es eine bessere Möglichkeit, ein Remote-Image zwischenzuspeichern?

Antworten:


423

Im Wesentlichen möchten Sie die __init__Methode von überschreiben , models.Modeldamit Sie eine Kopie des ursprünglichen Werts behalten. Dies macht es so, dass Sie keine weitere DB-Suche durchführen müssen (was immer gut ist).

class Person(models.Model):
    name = models.CharField()

    __original_name = None

    def __init__(self, *args, **kwargs):
        super(Person, self).__init__(*args, **kwargs)
        self.__original_name = self.name

    def save(self, force_insert=False, force_update=False, *args, **kwargs):
        if self.name != self.__original_name:
            # name changed - do something here

        super(Person, self).save(force_insert, force_update, *args, **kwargs)
        self.__original_name = self.name

24
Anstatt init zu überschreiben, würde ich das post_init-Signal docs.djangoproject.com/de/dev/ref/signals/#post-init
vikingosegundo

22
Das Überschreiben von Methoden wird in der Django-Dokumentation empfohlen: docs.djangoproject.com/de/dev/topics/db/models/…
Colonel Sponsz

10
@callum Wenn Sie also Änderungen am Objekt vornehmen, es speichern, dann weitere Änderungen vornehmen und save()es WIEDER aufrufen , funktioniert es weiterhin ordnungsgemäß.
Philfreo

17
@ Josh wird es kein Problem geben, wenn Sie mehrere Anwendungsserver haben, die mit derselben Datenbank arbeiten, da sie nur Änderungen im Speicher verfolgt
Jens Alm

13
@lajarre, ich denke dein Kommentar ist etwas irreführend. Die Dokumente schlagen vor, dass Sie vorsichtig sein, wenn Sie dies tun. Sie empfehlen nicht dagegen.
Josh

199

Ich benutze folgendes Mixin:

from django.forms.models import model_to_dict


class ModelDiffMixin(object):
    """
    A model mixin that tracks model fields' values and provide some useful api
    to know what fields have been changed.
    """

    def __init__(self, *args, **kwargs):
        super(ModelDiffMixin, self).__init__(*args, **kwargs)
        self.__initial = self._dict

    @property
    def diff(self):
        d1 = self.__initial
        d2 = self._dict
        diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
        return dict(diffs)

    @property
    def has_changed(self):
        return bool(self.diff)

    @property
    def changed_fields(self):
        return self.diff.keys()

    def get_field_diff(self, field_name):
        """
        Returns a diff for field if it's changed and None otherwise.
        """
        return self.diff.get(field_name, None)

    def save(self, *args, **kwargs):
        """
        Saves model and set initial state.
        """
        super(ModelDiffMixin, self).save(*args, **kwargs)
        self.__initial = self._dict

    @property
    def _dict(self):
        return model_to_dict(self, fields=[field.name for field in
                             self._meta.fields])

Verwendungszweck:

>>> p = Place()
>>> p.has_changed
False
>>> p.changed_fields
[]
>>> p.rank = 42
>>> p.has_changed
True
>>> p.changed_fields
['rank']
>>> p.diff
{'rank': (0, 42)}
>>> p.categories = [1, 3, 5]
>>> p.diff
{'categories': (None, [1, 3, 5]), 'rank': (0, 42)}
>>> p.get_field_diff('categories')
(None, [1, 3, 5])
>>> p.get_field_diff('rank')
(0, 42)
>>>

Hinweis

Bitte beachten Sie, dass diese Lösung nur im Kontext der aktuellen Anfrage funktioniert. Somit ist es vor allem für einfache Fälle geeignet. In einer gleichzeitigen Umgebung, in der mehrere Anforderungen dieselbe Modellinstanz gleichzeitig bearbeiten können, benötigen Sie definitiv einen anderen Ansatz.


4
Wirklich perfekt und führen Sie keine zusätzlichen Abfragen durch. Vielen Dank !
Stéphane

28
+1 für ein using mixin. +1 für keinen zusätzlichen DB-Treffer. +1 für viele nützliche Methoden / Eigenschaften. Ich muss in der Lage sein, mehrmals zu stimmen.
Jake

Ja. Plus eins für die Verwendung von Mixin und keinen zusätzlichen DB-Treffer.
David S

2
Mixin ist großartig, aber diese Version hat Probleme, wenn sie zusammen mit .only () verwendet wird. Der Aufruf von Model.objects.only ('id') führt zu einer unendlichen Rekursion, wenn Model mindestens 3 Felder hat. Um dies zu lösen, sollten wir verzögerte Felder aus dem Speichern in initial entfernen und die Eigenschaft _dict ein wenig
gleb.pitsevich

19
Ähnlich wie Joshs Antwort funktioniert dieser Code auf Ihrem Einzelprozess-Testserver täuschend gut, aber sobald Sie ihn auf einem beliebigen Multi-Processing-Server bereitstellen, führt er zu falschen Ergebnissen. Sie können nicht wissen, ob Sie den Wert in der Datenbank ändern, ohne die Datenbank abzufragen.
rspeer

154

Der beste Weg ist mit einem pre_saveSignal. Vielleicht war es noch keine Option in '09, als diese Frage gestellt und beantwortet wurde, aber jeder, der dies heute sieht, sollte es so machen:

@receiver(pre_save, sender=MyModel)
def do_something_if_changed(sender, instance, **kwargs):
    try:
        obj = sender.objects.get(pk=instance.pk)
    except sender.DoesNotExist:
        pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
    else:
        if not obj.some_field == instance.some_field: # Field has changed
            # do something

6
Warum ist dies der beste Weg, wenn die oben beschriebene Methode keinen zusätzlichen Datenbanktreffer beinhaltet?
Joshcartme

36
1) Diese Methode ist ein Hack. Signale sind im Wesentlichen für Anwendungen wie diese konzipiert. 2) Diese Methode erfordert Änderungen an Ihrem Modell. Dies ist nicht der Fall. 3) Wie Sie in den Kommentaren zu dieser Antwort lesen können, hat dies Nebenwirkungen kann möglicherweise problematisch sein, diese Lösung nicht
Chris Pratt

2
Diese Methode ist ideal, wenn Sie die Änderung nur kurz vor dem Speichern erfassen möchten. Dies funktioniert jedoch nicht, wenn Sie sofort auf die Änderung reagieren möchten. Ich bin oft auf das letztere Szenario gestoßen (und arbeite gerade an einer solchen Instanz).
Josh

5
@Josh: Was meinst du mit "sofort auf die Änderung reagieren"? Auf welche Weise können Sie dadurch nicht "reagieren"?
Chris Pratt

2
Entschuldigung, ich habe den Umfang dieser Frage vergessen und mich auf ein ganz anderes Problem bezogen. Trotzdem denke ich, dass Signale ein guter Weg sind, um hierher zu kommen (jetzt, wo sie verfügbar sind). Ich finde jedoch, dass viele Leute das Überschreiben außer einem "Hack" in Betracht ziehen. Ich glaube nicht, dass dies der Fall ist. Wie aus dieser Antwort hervorgeht ( stackoverflow.com/questions/170337/… ), ist das Überschreiben meiner Meinung nach die beste Vorgehensweise, wenn Sie nicht an Änderungen arbeiten, die "spezifisch für das betreffende Modell" sind. Trotzdem habe ich nicht die Absicht, diesen Glauben irgendjemandem aufzuzwingen.
Josh

138

Und jetzt zur direkten Antwort: Eine Möglichkeit zu überprüfen, ob sich der Wert für das Feld geändert hat, besteht darin, die Originaldaten aus der Datenbank abzurufen, bevor die Instanz gespeichert wird. Betrachten Sie dieses Beispiel:

class MyModel(models.Model):
    f1 = models.CharField(max_length=1)

    def save(self, *args, **kw):
        if self.pk is not None:
            orig = MyModel.objects.get(pk=self.pk)
            if orig.f1 != self.f1:
                print 'f1 changed'
        super(MyModel, self).save(*args, **kw)

Gleiches gilt für die Arbeit mit einem Formular. Sie können es an der Bereinigungs- oder Speichermethode einer ModelForm erkennen:

class MyModelForm(forms.ModelForm):

    def clean(self):
        cleaned_data = super(ProjectForm, self).clean()
        #if self.has_changed():  # new instance or existing updated (form has data to save)
        if self.instance.pk is not None:  # new instance only
            if self.instance.f1 != cleaned_data['f1']:
                print 'f1 changed'
        return cleaned_data

    class Meta:
        model = MyModel
        exclude = []

24
Joshs Lösung ist viel datenbankfreundlicher. Ein zusätzlicher Anruf, um zu überprüfen, was geändert wurde, ist teuer.
dd.

4
Ein zusätzlicher Lesevorgang vor dem Schreiben ist nicht so teuer. Auch die Methode zum Nachverfolgen von Änderungen funktioniert nicht, wenn mehrere Anforderungen vorliegen. Dies würde allerdings unter einer Rennbedingung zwischen Abrufen und Speichern leiden.
Bangalore

1
pk is not NoneHören Sie auf, den Leuten zu sagen, dass sie dies überprüfen sollen. Dies gilt beispielsweise nicht, wenn Sie ein UUIDField verwenden. Das ist nur ein schlechter Rat.
user3467349

2
@dalore Sie können die Rennbedingung vermeiden, indem Sie die Speichermethode mit@transaction.atomic
Frank Pape

2
@dalore, obwohl Sie sicherstellen müssen, dass die Transaktionsisolationsstufe ausreichend ist. In postgresql ist die Standardeinstellung "Festgeschrieben", aber wiederholbares Lesen ist erforderlich .
Frank Pape

58

Seit der Veröffentlichung von Django 1.8 können Sie die Klassenmethode from_db verwenden, um den alten Wert von remote_image zwischenzuspeichern. Anschließend können Sie in der Speichermethode den alten und den neuen Wert des Felds vergleichen, um zu überprüfen, ob sich der Wert geändert hat.

@classmethod
def from_db(cls, db, field_names, values):
    new = super(Alias, cls).from_db(db, field_names, values)
    # cache value went from the base
    new._loaded_remote_image = values[field_names.index('remote_image')]
    return new

def save(self, force_insert=False, force_update=False, using=None,
         update_fields=None):
    if (self._state.adding and self.remote_image) or \
        (not self._state.adding and self._loaded_remote_image != self.remote_image):
        # If it is first save and there is no cached remote_image but there is new one, 
        # or the value of remote_image has changed - do your stuff!

1
Vielen Dank - hier ist ein Verweis auf die Dokumente: docs.djangoproject.com/de/1.8/ref/models/instances/… . Ich glaube, dies führt immer noch zu dem oben genannten Problem, bei dem sich die Datenbank zwischen der Auswertung und dem Vergleich ändern kann, aber dies ist eine nette neue Option.
trpt4him

1
Wäre es nicht schneller und klarer, Werte zu durchsuchen (was O (n) basierend auf der Anzahl der Werte ist) new._loaded_remote_image = new.remote_image?
Bangalore

1
Leider muss ich meinen vorherigen (jetzt gelöschten) Kommentar rückgängig machen. Während von from_dbaufgerufen wird refresh_from_db, werden die Attribute der Instanz (dh geladen oder vorher) nicht aktualisiert. Als Ergebnis kann ich keinen Grund, warum dies ist besser als , __init__wie Sie noch 3 Fälle behandeln müssen: __init__/ from_db, refresh_from_dbund save.
Claytond


18

Wenn Sie ein Formular verwenden, können Sie die geänderten_Daten ( Dokumente ) des Formulars verwenden :

class AliasForm(ModelForm):

    def save(self, commit=True):
        if 'remote_image' in self.changed_data:
            # do things
            remote_image = self.cleaned_data['remote_image']
            do_things(remote_image)
        super(AliasForm, self).save(commit)

    class Meta:
        model = Alias



5

Dies funktioniert bei mir in Django 1.8

def clean(self):
    if self.cleaned_data['name'] != self.initial['name']:
        # Do something

4

Sie können Django-Modell-Änderungen verwenden , um dies ohne eine zusätzliche Datenbanksuche zu tun:

from django.dispatch import receiver
from django_model_changes import ChangesMixin

class Alias(ChangesMixin, MyBaseModel):
   # your model

@receiver(pre_save, sender=Alias)
def do_something_if_changed(sender, instance, **kwargs):
    if 'remote_image' in instance.changes():
        # do something

4

Noch eine späte Antwort, aber wenn Sie nur versuchen zu sehen, ob eine neue Datei in ein Dateifeld hochgeladen wurde, versuchen Sie Folgendes: (angepasst aus Christopher Adams 'Kommentar unter dem Link http://zmsmith.com/2010/05/django -check-wenn-ein-Feld-geändert hat / in Zachs Kommentar hier)

Aktualisierter Link: https://web.archive.org/web/20130101010327/http://zmsmith.com:80/2010/05/django-check-if-a-field-has-changed/

def save(self, *args, **kw):
    from django.core.files.uploadedfile import UploadedFile
    if hasattr(self.image, 'file') and isinstance(self.image.file, UploadedFile) :
        # Handle FileFields as special cases, because the uploaded filename could be
        # the same as the filename that's already there even though there may
        # be different file contents.

        # if a file was just uploaded, the storage model with be UploadedFile
        # Do new file stuff here
        pass

Das ist eine großartige Lösung, um zu überprüfen, ob eine neue Datei hochgeladen wurde. Viel besser als den Namen mit der Datenbank zu vergleichen, da der Name der Datei der gleiche sein könnte. Sie können es auch im pre_saveEmpfänger verwenden. Danke, dass du das geteilt hast!
DataGreed

1
Hier ist ein Beispiel für die Aktualisierung der Audiodauer
DataGreed

3

Die optimale Lösung ist wahrscheinlich eine, die weder eine zusätzliche Datenbankleseoperation vor dem Speichern der Modellinstanz noch eine weitere Django-Bibliothek enthält. Aus diesem Grund sind die Lösungen von laffuste vorzuziehen. Im Kontext einer Admin-Site kann man einfach save_modeldie has_changed-Methode überschreiben und dort die Methode des Formulars aufrufen , genau wie in der obigen Antwort von Sion. Sie kommen zu so etwas, indem Sie sich auf Sions Beispieleinstellung stützen, aber verwenden changed_data, um jede mögliche Änderung zu erhalten:

class ModelAdmin(admin.ModelAdmin):
   fields=['name','mode']
   def save_model(self, request, obj, form, change):
     form.changed_data #output could be ['name']
     #do somethin the changed name value...
     #call the super method
     super(self,ModelAdmin).save_model(request, obj, form, change)
  • Überschreiben save_model:

https://docs.djangoproject.com/de/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model

  • Eingebaute changed_dataMethode für ein Feld:

https://docs.djangoproject.com/de/1.10/ref/forms/api/#django.forms.Form.changed_data


2

Obwohl dies Ihre Frage nicht wirklich beantwortet, würde ich dies auf eine andere Weise tun.

Löschen Sie einfach das remote_imageFeld, nachdem Sie die lokale Kopie erfolgreich gespeichert haben. Dann können Sie in Ihrer Speichermethode das Bild immer aktualisieren, wenn remote_imagees nicht leer ist.

Wenn Sie einen Verweis auf die URL behalten möchten, können Sie ein nicht bearbeitbares boolesches Feld verwenden, um das Caching-Flag anstelle des remote_imageFelds selbst zu behandeln.


2

Ich hatte diese Situation, bevor meine Lösung darin bestand, die pre_save()Methode der Zielfeldklasse zu überschreiben. Sie wird nur aufgerufen, wenn das Feld geändert wurde
, was mit dem FileField-Beispiel nützlich ist:

class PDFField(FileField):
    def pre_save(self, model_instance, add):
        # do some operations on your file 
        # if and only if you have changed the filefield

Nachteil:
Nicht nützlich, wenn Sie eine (post_save) Operation wie die Verwendung des erstellten Objekts in einem Job ausführen möchten (wenn sich ein bestimmtes Feld geändert hat).


2

Verbesserung der @ josh-Antwort für alle Bereiche:

class Person(models.Model):
  name = models.CharField()

def __init__(self, *args, **kwargs):
    super(Person, self).__init__(*args, **kwargs)
    self._original_fields = dict([(field.attname, getattr(self, field.attname))
        for field in self._meta.local_fields if not isinstance(field, models.ForeignKey)])

def save(self, *args, **kwargs):
  if self.id:
    for field in self._meta.local_fields:
      if not isinstance(field, models.ForeignKey) and\
        self._original_fields[field.name] != getattr(self, field.name):
        # Do Something    
  super(Person, self).save(*args, **kwargs)

Nur um dies zu verdeutlichen, arbeitet der getattr daran, Felder wie person.namemit Strings zu erhalten (dhgetattr(person, "name")


Und es macht immer noch keine zusätzlichen Datenbankabfragen?
Andilabs

Ich habe versucht, Ihren Code zu implementieren. Es funktioniert in Ordnung, indem Felder bearbeitet werden. Aber jetzt habe ich Probleme beim Einfügen neuer. Ich bekomme DoesNotExist für mein FK-Feld in der Klasse. Einige Hinweise zur Lösung werden geschätzt.
Andilabs

Ich habe gerade den Code aktualisiert. Jetzt werden die Fremdschlüssel übersprungen, sodass Sie diese Dateien nicht mit zusätzlichen Abfragen abrufen müssen (sehr teuer). Wenn das Objekt nicht vorhanden ist, wird die zusätzliche Logik übersprungen.
Hassek

1

Ich habe das Mixin von @livskiy wie folgt erweitert:

class ModelDiffMixin(models.Model):
    """
    A model mixin that tracks model fields' values and provide some useful api
    to know what fields have been changed.
    """
    _dict = DictField(editable=False)
    def __init__(self, *args, **kwargs):
        super(ModelDiffMixin, self).__init__(*args, **kwargs)
        self._initial = self._dict

    @property
    def diff(self):
        d1 = self._initial
        d2 = self._dict
        diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
        return dict(diffs)

    @property
    def has_changed(self):
        return bool(self.diff)

    @property
    def changed_fields(self):
        return self.diff.keys()

    def get_field_diff(self, field_name):
        """
        Returns a diff for field if it's changed and None otherwise.
        """
        return self.diff.get(field_name, None)

    def save(self, *args, **kwargs):
        """
        Saves model and set initial state.
        """
        object_dict = model_to_dict(self,
               fields=[field.name for field in self._meta.fields])
        for field in object_dict:
            # for FileFields
            if issubclass(object_dict[field].__class__, FieldFile):
                try:
                    object_dict[field] = object_dict[field].path
                except :
                    object_dict[field] = object_dict[field].name

            # TODO: add other non-serializable field types
        self._dict = object_dict
        super(ModelDiffMixin, self).save(*args, **kwargs)

    class Meta:
        abstract = True

und das DictField ist:

class DictField(models.TextField):
    __metaclass__ = models.SubfieldBase
    description = "Stores a python dict"

    def __init__(self, *args, **kwargs):
        super(DictField, self).__init__(*args, **kwargs)

    def to_python(self, value):
        if not value:
            value = {}

        if isinstance(value, dict):
            return value

        return json.loads(value)

    def get_prep_value(self, value):
        if value is None:
            return value
        return json.dumps(value)

    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return self.get_db_prep_value(value)

Sie können es verwenden, indem Sie es in Ihren Modellen erweitern. Beim Synchronisieren / Migrieren wird ein _dict-Feld hinzugefügt, in dem der Status Ihrer Objekte gespeichert wird


1

Wie wäre es mit der Lösung von David Cramer:

http://cramer.io/2010/12/06/tracking-changes-to-fields-in-django/

Ich hatte Erfolg damit:

@track_data('name')
class Mode(models.Model):
    name = models.CharField(max_length=5)
    mode = models.CharField(max_length=5)

    def save(self, *args, **kwargs):
        if self.has_changed('name'):
            print 'name changed'

    # OR #

    @classmethod
    def post_save(cls, sender, instance, created, **kwargs):
        if instance.has_changed('name'):
            print "Hooray!"

2
Wenn Sie super (Mode, self) .save (* args, ** kwargs) vergessen, deaktivieren Sie die Speicherfunktion. Denken Sie also daran, dies in die Speichermethode einzufügen.
Max

Der Link des Artikels ist veraltet, dies ist der neue Link: cra.mr/2010/12/06/tracking-changes-to-fields-in-django
GoTop

1

Eine Modifikation der Antwort von @ ivanperelivskiy:

@property
def _dict(self):
    ret = {}
    for field in self._meta.get_fields():
        if isinstance(field, ForeignObjectRel):
            # foreign objects might not have corresponding objects in the database.
            if hasattr(self, field.get_accessor_name()):
                ret[field.get_accessor_name()] = getattr(self, field.get_accessor_name())
            else:
                ret[field.get_accessor_name()] = None
        else:
            ret[field.attname] = getattr(self, field.attname)
    return ret

Dies verwendet stattdessen die öffentliche Methode von django 1.10 get_fields. Dies macht den Code zukunftssicherer, enthält aber vor allem auch Fremdschlüssel und Felder, in denen editierbar = Falsch ist.

Als Referenz ist hier die Implementierung von .fields

@cached_property
def fields(self):
    """
    Returns a list of all forward fields on the model and its parents,
    excluding ManyToManyFields.

    Private API intended only to be used by Django itself; get_fields()
    combined with filtering of field properties is the public API for
    obtaining this field list.
    """
    # For legacy reasons, the fields property should only contain forward
    # fields that are not private or with a m2m cardinality. Therefore we
    # pass these three filters as filters to the generator.
    # The third lambda is a longwinded way of checking f.related_model - we don't
    # use that property directly because related_model is a cached property,
    # and all the models may not have been loaded yet; we don't want to cache
    # the string reference to the related_model.
    def is_not_an_m2m_field(f):
        return not (f.is_relation and f.many_to_many)

    def is_not_a_generic_relation(f):
        return not (f.is_relation and f.one_to_many)

    def is_not_a_generic_foreign_key(f):
        return not (
            f.is_relation and f.many_to_one and not (hasattr(f.remote_field, 'model') and f.remote_field.model)
        )

    return make_immutable_fields_list(
        "fields",
        (f for f in self._get_fields(reverse=False)
         if is_not_an_m2m_field(f) and is_not_a_generic_relation(f) and is_not_a_generic_foreign_key(f))
    )

1

Hier ist eine andere Möglichkeit, dies zu tun.

class Parameter(models.Model):

    def __init__(self, *args, **kwargs):
        super(Parameter, self).__init__(*args, **kwargs)
        self.__original_value = self.value

    def clean(self,*args,**kwargs):
        if self.__original_value == self.value:
            print("igual")
        else:
            print("distinto")

    def save(self,*args,**kwargs):
        self.full_clean()
        return super(Parameter, self).save(*args, **kwargs)
        self.__original_value = self.value

    key = models.CharField(max_length=24, db_index=True, unique=True)
    value = models.CharField(max_length=128)

Gemäß Dokumentation: Objekte validieren

"Der zweite Schritt, den full_clean () ausführt, ist das Aufrufen von Model.clean (). Diese Methode sollte überschrieben werden, um eine benutzerdefinierte Validierung für Ihr Modell durchzuführen. Diese Methode sollte verwendet werden, um eine benutzerdefinierte Modellvalidierung bereitzustellen und Attribute für Ihr Modell zu ändern, falls dies gewünscht wird Sie können es beispielsweise verwenden, um automatisch einen Wert für ein Feld bereitzustellen oder um eine Validierung durchzuführen, die den Zugriff auf mehr als ein einzelnes Feld erfordert: "


1

Es gibt ein Attribut __dict__, das alle Felder als Schlüssel und den Wert als Feldwerte enthält. Wir können also nur zwei davon vergleichen

Ändern Sie einfach die Speicherfunktion des Modells in die folgende Funktion

def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
    if self.pk is not None:
        initial = A.objects.get(pk=self.pk)
        initial_json, final_json = initial.__dict__.copy(), self.__dict__.copy()
        initial_json.pop('_state'), final_json.pop('_state')
        only_changed_fields = {k: {'final_value': final_json[k], 'initial_value': initial_json[k]} for k in initial_json if final_json[k] != initial_json[k]}
        print(only_changed_fields)
    super(A, self).save(force_insert=False, force_update=False, using=None, update_fields=None)

Anwendungsbeispiel:

class A(models.Model):
    name = models.CharField(max_length=200, null=True, blank=True)
    senior = models.CharField(choices=choices, max_length=3)
    timestamp = models.DateTimeField(null=True, blank=True)

    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
        if self.pk is not None:
            initial = A.objects.get(pk=self.pk)
            initial_json, final_json = initial.__dict__.copy(), self.__dict__.copy()
            initial_json.pop('_state'), final_json.pop('_state')
            only_changed_fields = {k: {'final_value': final_json[k], 'initial_value': initial_json[k]} for k in initial_json if final_json[k] != initial_json[k]}
            print(only_changed_fields)
        super(A, self).save(force_insert=False, force_update=False, using=None, update_fields=None)

liefert nur mit den geänderten Feldern eine Ausgabe

{'name': {'initial_value': '1234515', 'final_value': 'nim'}, 'senior': {'initial_value': 'no', 'final_value': 'yes'}}

1

Sehr spät zum Spiel, aber dies ist eine Version von Chris Pratts Antwort , die vor Rennbedingungen schützt und gleichzeitig die Leistung beeinträchtigt, indem sie einen transactionBlock und verwendetselect_for_update()

@receiver(pre_save, sender=MyModel)
@transaction.atomic
def do_something_if_changed(sender, instance, **kwargs):
    try:
        obj = sender.objects.select_for_update().get(pk=instance.pk)
    except sender.DoesNotExist:
        pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
    else:
        if not obj.some_field == instance.some_field: # Field has changed
            # do something

0

Als Erweiterung der Antwort von SmileyChris können Sie dem Modell für last_updated ein Datum / Uhrzeit-Feld hinzufügen und eine Grenze für das maximale Alter festlegen, das Sie erreichen dürfen, bevor Sie nach einer Änderung suchen


0

Das Mixin von @ivanlivski ist großartig.

Ich habe es erweitert

  • Stellen Sie sicher, dass es mit Dezimalfeldern funktioniert.
  • Stellen Sie Eigenschaften bereit, um die Verwendung zu vereinfachen

Der aktualisierte Code ist hier verfügbar: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py

Um Menschen zu helfen, die Python oder Django noch nicht kennen, werde ich ein vollständigeres Beispiel geben. Diese spezielle Verwendung dient dazu, eine Datei von einem Datenanbieter zu entnehmen und sicherzustellen, dass die Datensätze in der Datenbank die Datei widerspiegeln.

Mein Modellobjekt:

class Station(ModelDiffMixin.ModelDiffMixin, models.Model):
    station_name = models.CharField(max_length=200)
    nearby_city = models.CharField(max_length=200)

    precipitation = models.DecimalField(max_digits=5, decimal_places=2)
    # <list of many other fields>

   def is_float_changed (self,v1, v2):
        ''' Compare two floating values to just two digit precision
        Override Default precision is 5 digits
        '''
        return abs (round (v1 - v2, 2)) > 0.01

Die Klasse, die die Datei lädt, verfügt über folgende Methoden:

class UpdateWeather (object)
    # other methods omitted

    def update_stations (self, filename):
        # read all existing data 
        all_stations = models.Station.objects.all()
        self._existing_stations = {}

        # insert into a collection for referencing while we check if data exists
        for stn in all_stations.iterator():
            self._existing_stations[stn.id] = stn

        # read the file. result is array of objects in known column order
        data = read_tabbed_file(filename)

        # iterate rows from file and insert or update where needed
        for rownum in range(sh.nrows):
            self._update_row(sh.row(rownum));

        # now anything remaining in the collection is no longer active
        # since it was not found in the newest file
        # for now, delete that record
        # there should never be any of these if the file was created properly
        for stn in self._existing_stations.values():
            stn.delete()
            self._num_deleted = self._num_deleted+1


    def _update_row (self, rowdata):
        stnid = int(rowdata[0].value) 
        name = rowdata[1].value.strip()

        # skip the blank names where data source has ids with no data today
        if len(name) < 1:
            return

        # fetch rest of fields and do sanity test
        nearby_city = rowdata[2].value.strip()
        precip = rowdata[3].value

        if stnid in self._existing_stations:
            stn = self._existing_stations[stnid]
            del self._existing_stations[stnid]
            is_update = True;
        else:
            stn = models.Station()
            is_update = False;

        # object is new or old, don't care here            
        stn.id = stnid
        stn.station_name = name;
        stn.nearby_city = nearby_city
        stn.precipitation = precip

        # many other fields updated from the file 

        if is_update == True:

            # we use a model mixin to simplify detection of changes
            # at the cost of extra memory to store the objects            
            if stn.has_changed == True:
                self._num_updated = self._num_updated + 1;
                stn.save();
        else:
            self._num_created = self._num_created + 1;
            stn.save()

0

Wenn Sie kein Interesse an einer überschreibenden saveMethode haben, können Sie dies tun

  model_fields = [f.name for f in YourModel._meta.get_fields()]
  valid_data = {
        key: new_data[key]
        for key in model_fields
        if key in new_data.keys()
  }

  for (key, value) in valid_data.items():
        if getattr(instance, key) != value:
           print ('Data has changed')

        setattr(instance, key, value)

 instance.save()
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.