django - Vergleich des alten und neuen Feldwerts vor dem Speichern

71

Ich habe ein Django-Modell und muss vor dem Speichern alte und neue Werte des Feldes vergleichen.

Ich habe die Vererbung save () und das Signal pre_save ausprobiert. Es wurde korrekt ausgelöst, aber ich kann die Liste der tatsächlich geänderten Felder nicht finden und alte und neue Werte nicht vergleichen. Da ist ein Weg? Ich brauche es zur Optimierung von vorgespeicherten Aktionen.

Vielen Dank!

YN
quelle
1
Was ist mit dem Abrufen eines alten Werts aus der Datenbank in der saveMethode und dem Überprüfen jedes Felds auf Gleichheit?
J0HN
Welche Art von Optimierung möchten Sie?
Leonardo.Z
@ J0HN Der da zwischen hat sich während des Abruf-, Vergleichs- und Speicherprozesses geändert.
Leonardo.Z
Ich denke, es kann und muss die Leistung verlangsamen?
YN

Antworten:

70

Es gibt einen sehr einfachen Django-Weg, dies zu tun.

Merken Sie sich die Werte in model init wie folgt:

def __init__(self, *args, **kwargs):
    super(MyClass, self).__init__(*args, **kwargs)
    self.initial_parametername = self.parametername
    ---
    self.initial_parameternameX = self.parameternameX

Beispiel aus dem wirklichen Leben:

Im Unterricht:

def __init__(self, *args, **kwargs):
    super(MyClass, self).__init__(*args, **kwargs)
    self.__important_fields = ['target_type', 'target_id', 'target_object', 'number', 'chain', 'expiration_date']
    for field in self.__important_fields:
        setattr(self, '__original_%s' % field, getattr(self, field))

def has_changed(self):
    for field in self.__important_fields:
        orig = '__original_%s' % field
        if getattr(self, orig) != getattr(self, field):
            return True
    return False

Und dann in Modellform Speichermethode:

def save(self, force_insert=False, force_update=False, commit=True):
    # Prep the data
    obj = super(MyClassForm, self).save(commit=False)

    if obj.has_changed():

        # If we're down with commitment, save this shit
        if commit:
            obj.save(force_insert=True)

    return obj
Odif Yltsaeb
quelle
5
Ich bevorzuge den Weg des Odif, weil ich die Aktionen für das Modell ohne Formulare auslösen muss (nachdem Änderungen von der API oder von der Admin-Site stammen)
YN
wann __init__heißt es Funktioniert es nur für die Ersterstellung oder nachfolgende Updates?
Wasabigeek
Init wird jedes Mal aufgerufen, wenn eine Modellinstanz erstellt wird. Wenn die Instanz über ihre Lebensdauer mehrere Male aktualisiert wird, __init__wird sie nur am Anfang aufgerufen.
Odif Yltsaeb
Dies gilt nicht, wenn das Modell an anderen Orten mit saveoderbulk_create
Julio Marins
Nein, das wird es nicht. Das Speichern von Modellen geschieht jedoch fast nie aus heiterem Himmel ohne vorherige Interaktion. Die has_changed-Methode kann an all diesen Stellen verwendet werden. Und wenn Sie das Objekt erstellen, müssen Sie nicht überprüfen, ob es sich trotzdem geändert hat ...
Odif Yltsaeb
48

Es ist besser, dies auf ModelForm-Ebene zu tun .

Dort erhalten Sie alle Daten, die Sie zum Vergleich in der Speichermethode benötigen:

  1. self.data : Tatsächliche Daten, die an das Formular übergeben werden.
  2. self.cleaned_data : Nach Validierungen bereinigte Daten. Enthält Daten, die im Modell gespeichert werden können
  3. self.changed_data : Liste der Felder, die geändert wurden. Dies ist leer, wenn sich nichts geändert hat

Wenn Sie dies auf Modellebene tun möchten, können Sie die in Odifs Antwort angegebene Methode befolgen.

Sahil Kalra
quelle
1
Ich stimme Ihrer Antwort zu, auch self.instance kann in dieser Ausgabe von Nutzen sein.
Lehins
@AlexeyKuleshevich stimmte zu, jedoch nur vor dem _post_clean( is_valid->errors->full_clean->_post_clean) eines Formulars. Danach wird die Instanz aktualisiert, um die neuen Werte einzuschließen . Zugriff in form.clean_fieldname()und form.clean()scheint in Ordnung zu sein, vorausgesetzt, es ist ihr erster Anruf.
jozxyqk
2
Nun, das funktioniert, aber NUR, wenn Sie mit einem Formular speichern, was nicht immer der Fall ist.
Guival
Ja stimmt. Wenn Sie kein Formular verwenden, können Sie dies nicht tun. Die Verwendung eines Formulars ist jedoch der ideale Weg.
Sahil Kalra
self.changed_dataist neu für mich
Mohammed Shareef C
34

Sie können auch verwenden FieldTracker von django-Modell-utils für diese:

  1. Fügen Sie Ihrem Modell einfach ein Trackerfeld hinzu:

    tracker = FieldTracker()
    
  2. Jetzt können Sie in pre_save und post_save Folgendes verwenden:

    instance.tracker.previous('modelfield')     # get the previous value
    instance.tracker.has_changed('modelfield')  # just check if it is changed
    
psl
quelle
4
Ja, ich liebe es einfach, wie sauber das ist ... Eine weitere Linie zu den Anforderungen!
Kevin Parker
Aber dieses Trackerfeld ist eine echte Spalte in der Tabelle? Oder ist nur ein falsches Feld?
toscanelli
3
@toscanelli, es wird keine Spalte zur Tabelle hinzugefügt.
Texnic
1
Nur eine Erinnerung, um sicherzustellen, dass Sie Auswanderungen vornehmen und erneut migrieren, da sonst ein Attributfehler wie "Tracker" nicht gefunden wird.
Amoroso
3
Dieser ist so verlockend , aber jemand ein Leistungsproblem berichten hier . Und es gibt kein Update oder Follow-up vom Team. Überprüfen Sie also den Quellcode von tracker.py. Es sieht nach vielen Arbeiten und Signalen aus. Es kommt also darauf an, ob es sich lohnt - oder der Anwendungsfall zu begrenzt war, dass Sie nur ein oder zwei Felder verfolgen müssen.
John Pang
4

Ab Django 1.8+ (einschließlich Django 2.x und 3.x) gibt es eine from_dbKlassenmethode, mit der die Erstellung von Modellinstanzen beim Laden aus der Datenbank angepasst werden kann.

Hinweis : Wenn Sie diese Methode verwenden, gibt es KEINE zusätzliche Datenbankabfrage.

Hier ist der Link zur offiziellen docs- Modellinstanz - Modellladen anpassen

from django.db import Model

class MyClass(models.Model):
    
    @classmethod
    def from_db(cls, db, field_names, values):
        instance = super().from_db(db, field_names, values)
        
        # save original values, when model is loaded from database,
        # in a separate attribute on the model
        instance._loaded_values = dict(zip(field_names, values))
        
        return instance

Die ursprünglichen Werte sind jetzt im _loaded_valuesAttribut des Modells verfügbar . Sie können auf dieses Attribut in Ihrer saveMethode zugreifen , um zu überprüfen, ob ein Wert aktualisiert wird.

class MyClass(models.Model):
    field_1 = models.CharField(max_length=1)

    @classmethod
    def from_db(cls, db, field_names, values):
        ...
        # use code from above

    def save(self, *args, **kwargs):

        # check if a new db row is being added
        # When this happens the `_loaded_values` attribute will not be available
        if not self._state.adding:

            # check if field_1 is being updated
            if self._loaded_values['field_1'] != self.field_1:
                # do something

        super().save(*args, **kwargs)
            
            
GunnerFan
quelle
Das ist ziemlich cool, aber es gibt dir keine M2M-Beziehungen. Wenn Sie beispielsweise versuchen, Änderungen an den Gruppen zu verfolgen, denen ein Benutzer zugeordnet ist, scheint es mit dieser Technik keine Möglichkeit zu geben, dies zu tun.
Shacker
3

Mein Anwendungsfall dafür war, dass ich einen denormalisierten Wert im Modell festlegen musste, wenn ein Feld seinen Wert änderte. Da es sich bei dem überwachten Feld jedoch um eine m2m-Beziehung handelt, wollte ich diese DB-Suche nicht bei jedem Aufruf von save durchführen müssen, um zu überprüfen, ob das denormalisierte Feld aktualisiert werden muss. Also schrieb ich stattdessen dieses kleine Mixin (unter Verwendung der Antwort von @Odif Yitsaeb als Inspiration), um das denormalisierte Feld nur bei Bedarf zu aktualisieren.

class HasChangedMixin(object):
    """ this mixin gives subclasses the ability to set fields for which they want to monitor if the field value changes """
    monitor_fields = []

    def __init__(self, *args, **kwargs):
        super(HasChangedMixin, self).__init__(*args, **kwargs)
        self.field_trackers = {}

    def __setattr__(self, key, value):
        super(HasChangedMixin, self).__setattr__(key, value)
        if key in self.monitor_fields and key not in self.field_trackers:
            self.field_trackers[key] = value

    def changed_fields(self):
        """
        :return: `list` of `str` the names of all monitor_fields which have changed
        """
        changed_fields = []
        for field, initial_field_val in self.field_trackers.items():
            if getattr(self, field) != initial_field_val:
                changed_fields.append(field)

        return changed_fields
Bobby
quelle
3

So etwas funktioniert auch:

class MyModel(models.Model):
    my_field = fields.IntegerField()

    def save(self, *args, **kwargs):
       # Compare old vs new
       if self.pk:
           obj = MyModel.objects.values('my_value').get(pk=self.pk)
           if obj['my_value'] != self.my_value:
               # Do stuff...
               pass
       super().save(*args, **kwargs)
Windschatten
quelle
9
Das Durchführen einer Suche vor jedem Speichern scheint nicht sehr performant zu sein.
Ian E
1
"Das Durchführen einer Suche vor jedem Speichern scheint nicht sehr performant zu sein", stimme ich zu. Aber es kommt auf den Kontext an. Was schlagen Sie auf jeden Fall vor?
Akhorus
2
@IanE Ich habe eine Antwort hinzugefügt, die eine DB-Suche vermeidet. Stackoverflow.com/a/64116052/3446669
GunnerFan
2

Hier ist eine App, mit der Sie direkt vor dem Speichern des Modells auf den vorherigen und aktuellen Wert eines Felds zugreifen können: django-smartfields

So kann dieses Problem in einem schönen deklarativen Mai gelöst werden:

from django.db import models
from smartfields import fields, processors
from smartfields.dependencies import Dependency

class ConditionalProcessor(processors.BaseProcessor):

    def process(self, value, stashed_value=None, **kwargs):
        if value != stashed_value:
            # do any necessary modifications to new value
            value = ... 
        return value

class MyModel(models.Model):
    my_field = fields.CharField(max_length=10, dependencies=[
        Dependency(processor=ConditionalProcessor())
    ])

Außerdem wird dieser Prozessor nur dann aufgerufen, wenn der Wert dieses Feldes ersetzt wurde

Lehins
quelle
2

Ich stimme Sahil zu, dass es mit ModelForm besser und einfacher ist, dies zu tun. Sie würden jedoch die Bereinigungsmethode von ModelForm anpassen und dort eine Validierung durchführen. In meinem Fall wollte ich Aktualisierungen der Instanz eines Modells verhindern, wenn ein Feld im Modell festgelegt ist.

Mein Code sah folgendermaßen aus:

from django.forms import ModelForm

class ExampleForm(ModelForm):
    def clean(self):
        cleaned_data = super(ExampleForm, self).clean()
        if self.instance.field:
            raise Exception
        return cleaned_data
erika_dike
quelle
0

Eine andere Möglichkeit, dies zu erreichen, besteht darin, die Signale post_initund zu post_saveverwenden, um den Anfangszustand des Modells zu speichern.

@receiver(models.signals.post_init)
@receiver(models.signals.post_save)
def _set_initial_state(
    sender: Type[Any],
    instance: Optional[models.Model] = None,
    **kwargs: Any,
) -> None:
    """
    Store the initial state of the model
    """

    if isinstance(instance, MyModel):
        instance._initial_state = instance.state
Danielle Madeley
quelle
0

Im modernen Django ist es sehr wichtig , den Inhalt der Antwort zu ergänzen, die unter den obigen Antworten akzeptiert wird . Sie können in einen fallen unendliche Rekursion , wenn Sie verwenden deferoder onlyQuerySet API.

__get__()Methode der django.db.models.query_utils.DeferredAttributeAufrufe refresh_from_db()Methode von django.db.models.Model. Es gibt eine Zeile db_instance = db_instance_qs.get()in refresh_from_db(), und diese Zeile ruft die __init__()Methode der Instanz rekursiv auf.

Es muss also hinzugefügt werden, um sicherzustellen, dass die Zielattribute nicht zurückgestellt werden.

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

    deferred_fields = self.get_deferred_fields()
    important_fields = ['target_type', 'target_id', 'target_object', 'number', 'chain', 'expiration_date']

    self.__important_fields = list(filter(lambda x: x not in deferred_fields, important_fields))
    for field in self.__important_fields:
        setattr(self, '__original_%s' % field, getattr(self, field))
Youngkwang Kim
quelle