Kiedy zapisujesz, jak możesz sprawdzić, czy pole się zmieniło?


293

W moim modelu mam:

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

Który działa świetnie po raz pierwszy remote_imagezmiany.

Jak mogę pobrać nowy obraz, gdy ktoś zmodyfikował remote_imagealias? Po drugie, czy istnieje lepszy sposób buforowania zdalnego obrazu?

Odpowiedzi:


423

Zasadniczo chcesz zastąpić __init__metodę models.Model, aby zachować kopię oryginalnej wartości. To sprawia, że ​​nie musisz wykonywać kolejnego wyszukiwania DB (co zawsze jest dobrą rzeczą).

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
zamiast nadpisywania init użyłbym dokumentu post_init-signal docs.djangoproject.com/en/dev/ref/signals/#post-init
vikingosegundo


10
@callum, więc jeśli dokonasz zmian w obiekcie, zapisz go, a następnie dokona dodatkowych zmian i ponownie go wywołaj save(), nadal będzie działał poprawnie.
philfreo

17
@Josh nie będzie problemu, jeśli kilka serwerów aplikacji działa na tej samej bazie danych, ponieważ śledzi tylko zmiany w pamięci
Jens Alm

13
@lajarre, myślę, że twój komentarz jest nieco mylący. Dokumenty sugerują, abyś był ostrożny. Nie polecają tego.
Josh

199

Używam następujących 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])

Stosowanie:

>>> 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)
>>>

Uwaga

Należy pamiętać, że to rozwiązanie działa dobrze tylko w kontekście bieżącego żądania. Dlatego nadaje się przede wszystkim do prostych przypadków. W środowisku współbieżnym, w którym wiele żądań może jednocześnie manipulować tym samym wystąpieniem modelu, zdecydowanie potrzebujesz innego podejścia.


4
Naprawdę idealne i nie wykonuj dodatkowych zapytań. Wielkie dzięki !
Stéphane

28
+1 za używanie mixinu. +1 za brak dodatkowego trafienia DB. +1 za wiele przydatnych metod / właściwości. Muszę być w stanie głosować wiele razy.
Jake

Tak. Plus jeden za używanie Mixin i brak dodatkowego trafienia db.
David S

2
Mixin jest świetny, ale ta wersja ma problemy, gdy jest używana razem z .only (). Wywołanie Model.objects.only („id”) doprowadzi do nieskończonej rekurencji, jeśli Model ma co najmniej 3 pola. Aby rozwiązać ten problem, powinniśmy usunąć odroczone pola z początkowego zapisu i nieco
_dict

19
Podobnie jak odpowiedź Josha, ten kod będzie działał zwodniczo na serwerze testowym jednoprocesowym, ale w momencie wdrożenia go na dowolnym serwerze obsługującym wiele procesów będzie on dawał nieprawidłowe wyniki. Nie możesz wiedzieć, czy zmieniasz wartość w bazie danych bez zapytania do bazy danych.
rspeer

154

Najlepszym sposobem jest pre_savesygnał. Być może nie było opcji jeszcze w 2009 roku, kiedy zadano to pytanie i udzielono na nie odpowiedzi, ale każdy, kto zobaczy to dzisiaj, powinien to zrobić w ten sposób:

@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
Dlaczego jest to najlepszy sposób, jeśli metoda opisana powyżej przez Josha nie obejmuje dodatkowego trafienia do bazy danych?
joshcartme

36
1) ta metoda to hack, sygnały są zasadniczo przeznaczone do zastosowań takich jak ten 2) ta metoda wymaga wprowadzania zmian w modelu, ten nie 3) jak można przeczytać w komentarzach do tej odpowiedzi, ma skutki uboczne, które może być potencjalnie problematyczny, to rozwiązanie nie
Chris Pratt,

2
Ten sposób jest świetny, jeśli zależy ci na uchwyceniu zmiany tuż przed zapisaniem. Nie zadziała to jednak, jeśli chcesz natychmiast zareagować na zmianę. Drugi scenariusz napotkałem wiele razy (i pracuję teraz nad jednym z takich przypadków).
Josh

5
@Josh: Co rozumiesz przez „natychmiast reagować na zmianę”? W jaki sposób nie pozwala to „zareagować”?
Chris Pratt

2
Przepraszam, zapomniałem o zakresie tego pytania i odnosiłem się do zupełnie innego problemu. To powiedziawszy, myślę, że sygnały są dobrym sposobem na przejście tutaj (teraz, gdy są dostępne). Uważam jednak, że wiele osób rozważa zastąpienie oprócz „włamania”. Nie wierzę, że tak jest. Jak sugeruje ta odpowiedź ( stackoverflow.com/questions/170337/… ), myślę, że przesłonięcie jest najlepszą praktyką, gdy nie pracujesz nad zmianami „specyficznymi dla danego modelu”. To powiedziawszy, nie zamierzam nikomu narzucać tej wiary.
Josh

138

A teraz bezpośrednia odpowiedź: jednym ze sposobów sprawdzenia, czy wartość pola uległa zmianie, jest pobranie oryginalnych danych z bazy danych przed zapisaniem instancji. Rozważ ten przykład:

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)

To samo dotyczy pracy z formularzem. Możesz go wykryć przy użyciu metody czyszczenia lub zapisywania ModelForm:

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
Rozwiązanie Josha jest znacznie bardziej przyjazne dla bazy danych. Dodatkowe połączenie w celu sprawdzenia, co się zmieniło, jest drogie.
dd.

4
Dodatkowa lektura przed napisaniem nie jest taka droga. Również metoda śledzenia zmian nie działa, jeśli jest wiele żądań. Chociaż ucierpiałby na tym stan wyścigu pomiędzy pobieraniem a oszczędzaniem.
dalore

1
Przestań mówić ludziom, aby pk is not Noneto sprawdzało , nie dotyczy to na przykład używania UUIDField. To tylko zła rada.
user3467349

2
@dalore możesz uniknąć warunków wyścigu, dekorując metodę zapisywania@transaction.atomic
Frank Pape,

2
@dalore, chociaż musisz się upewnić, że poziom izolacji transakcji jest wystarczający. W postgresql domyślnie czytane jest zatwierdzone, ale konieczne jest powtarzalne czytanie .
Frank Pape,

58

Od czasu wydania Django 1.8 możesz użyć metody class from_db do buforowania starej wartości obrazu remote_image. Następnie w oszczędzania metodzie można porównać stare i nowe wartości pola, aby sprawdzić, czy wartość została zmieniona.

@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
Dzięki - tu jest odniesienie do dokumentów: docs.djangoproject.com/en/1.8/ref/models/instances/... . Sądzę, że nadal powoduje to wspomniany problem, w którym baza danych może się zmieniać między momentem oceny a wykonaniem porównania, ale jest to miła nowa opcja.
trpt4him

1
Czy zamiast przeszukiwania wartości (czyli O (n) na podstawie liczby wartości) nie byłoby to szybsze i bardziej przejrzyste new._loaded_remote_image = new.remote_image?
dalore

1
Niestety muszę cofnąć mój poprzedni (teraz usunięty) komentarz. Podczas gdy from_dbjest wywoływany przez refresh_from_db, atrybuty instancji (tj. Załadowane lub poprzednie) nie są aktualizowane. W rezultacie, nie mogę znaleźć żadnego powodu, dlaczego tak jest lepiej niż __init__jak trzeba jeszcze obsłużyć 3 przypadki: __init__/ from_db, refresh_from_db, i save.
claytond


18

Jeśli korzystasz z formularza, możesz użyć zmienionych danych formularza ( dokumenty ):

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

Działa to dla mnie w Django 1.8

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

4

Możesz użyć zmian modelu django, aby to zrobić bez dodatkowego wyszukiwania bazy danych:

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

Kolejna późna odpowiedź, ale jeśli chcesz tylko sprawdzić, czy nowy plik został przesłany do pola pliku, spróbuj tego: (na podstawie komentarza Christophera Adamsa do linku http://zmsmith.com/2010/05/django -check-if-a-field-has- change / in w komentarzu zach tutaj)

Zaktualizowany 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

To niesamowite rozwiązanie do sprawdzania, czy został przesłany nowy plik. O wiele lepsze niż sprawdzanie nazwy w bazie danych, ponieważ nazwa pliku może być taka sama. Możesz go również użyć w pre_saveodbiorniku. Dziękujemy za udostępnienie tego!
DataGreed

1
Oto przykład aktualizacji czasu trwania dźwięku w bazie danych, gdy plik został zaktualizowany przy użyciu mutagenu do odczytu informacji audio - gist.github.com/DataGreed/1ba46ca7387950abba2ff53baf70fec2
DataGreed

3

Optymalnym rozwiązaniem jest prawdopodobnie takie, które nie obejmuje dodatkowej operacji odczytu bazy danych przed zapisaniem instancji modelu ani żadnej dalszej biblioteki django. Dlatego preferowane są rozwiązania Laffuste. W kontekście strony administratora można po prostu zastąpić save_model-method i wywołać has_changedtam metodę formularza , tak jak w powyższej odpowiedzi Siona. Dochodzisz do czegoś takiego, korzystając z przykładowego ustawienia Siona, ale używając, changed_dataaby uzyskać każdą możliwą zmianę:

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)
  • Zastąp save_model:

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

  • Wbudowana changed_datametoda dla pola:

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


2

Chociaż tak naprawdę to nie odpowiada na twoje pytanie, zrobiłbym to w inny sposób.

Po prostu wyczyść remote_imagepole po pomyślnym zapisaniu lokalnej kopii. Następnie w metodzie zapisywania zawsze możesz zaktualizować obraz, gdy remote_imagenie jest pusty.

Jeśli chcesz zachować odniesienie do adresu URL, możesz użyć nieedytowalnego pola boolowskiego do obsługi flagi buforowania zamiast remote_imagesamego pola.


2

Miałem taką sytuację, zanim moje rozwiązanie polegało na zastąpieniu pre_save()metody docelowej klasy pola, która zostanie wywołana tylko wtedy, gdy zmieniono pole
przydatne w przykładzie FileField:

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

Wada:
nieprzydatne, jeśli chcesz wykonać dowolną operację (post_save), taką jak użycie utworzonego obiektu w pewnym zadaniu (jeśli pewne pole uległo zmianie)


2

poprawiając odpowiedź @josh dla wszystkich pól:

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)

dla wyjaśnienia, getattr działa tak, aby uzyskać pola takie jak person.namełańcuchy (tjgetattr(person, "name")


I nadal nie tworzy dodatkowych zapytań db?
andilabs

Próbowałem zaimplementować twój kod. Działa dobrze, edytując pola. Ale teraz mam problem z wstawieniem nowego. Dostaję DoesNotExist dla mojego pola FK w klasie. Doceniamy niektóre wskazówki, jak to rozwiązać.
andilabs

Właśnie zaktualizowałem kod, teraz pomija on klucze obce, więc nie musisz pobierać tych plików z dodatkowymi zapytaniami (bardzo drogimi), a jeśli obiekt nie istnieje, pominie dodatkową logikę.
Hassek

1

Rozszerzyłem mixin @livskiy w następujący sposób:

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

a DictField to:

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)

można go wykorzystać, rozszerzając go w swoich modelach. Podczas synchronizacji / migracji zostanie dodane pole _dict, w którym będzie przechowywany stan obiektów


1

Co powiesz na korzystanie z rozwiązania Davida Cramera:

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

Miałem sukces, używając go w następujący sposób:

@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
Jeśli zapomnisz super (Mode, self) .save (* args, ** kwargs), wówczas wyłączasz funkcję save, więc pamiętaj, aby umieścić ją w metodzie save.
maks.

Link do tego artykułu jest nieaktualny, to jest nowy link: cra.mr/2010/12/06/tracking-changes-to-fields-in-django
GoTop

1

Modyfikacja odpowiedzi @ 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

get_fieldsZamiast tego używa metody publicznej django 1.10 . To sprawia, że ​​kod jest bardziej odporny na przyszłość, ale co ważniejsze, zawiera także klucze obce i pola, w których editable = False.

Dla odniesienia tutaj jest implementacja .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

Oto inny sposób na zrobienie tego.

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)

Zgodnie z dokumentacją: sprawdzanie poprawności obiektów

„Drugim krokiem, który wykonuje full_clean (), jest wywołanie Model.clean (). Ta metoda powinna zostać przesłonięta, aby wykonać niestandardowe sprawdzenie poprawności w twoim modelu. Ta metoda powinna być użyta do zapewnienia niestandardowego sprawdzania poprawności modelu i do modyfikowania atrybutów w twoim modelu, jeśli jest to pożądane . Możesz na przykład użyć go do automatycznego podania wartości dla pola lub do sprawdzenia poprawności, która wymaga dostępu do więcej niż jednego pola: „


1

Istnieje atrybut __dict__, który ma wszystkie pola jako klucze i wartość jako wartości pól. Możemy więc po prostu porównać dwa z nich

Wystarczy zmienić funkcję zapisu modelu na funkcję poniżej

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)

Przykładowe użycie:

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)

daje wynik tylko z tymi zmienionymi polami

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

1

Bardzo późno do gry, ale jest to wersja odpowiedzi Chrisa Pratta, która chroni przed warunkami wyścigowymi, poświęcając wydajność, używając transactionbloku iselect_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

jako rozszerzenie odpowiedzi SmileyChris, możesz dodać do modelu pole daty i godziny dla ostatniej aktualizacji i ustawić pewien limit maksymalnego wieku, na jaki pozwolisz mu dotrzeć przed sprawdzeniem zmiany


0

Mixin z @ivanlivski jest świetny.

Rozszerzyłem to na

  • Upewnij się, że działa z polami dziesiętnymi.
  • Ujawnij właściwości w celu uproszczenia użytkowania

Zaktualizowany kod jest dostępny tutaj: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py

Aby pomóc ludziom, którzy nie znają języka Python lub Django, podam pełniejszy przykład. To szczególne zastosowanie polega na pobraniu pliku od dostawcy danych i upewnieniu się, że rekordy w bazie danych odzwierciedlają ten plik.

Mój obiekt modelu:

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

Klasa ładująca plik ma następujące metody:

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

Jeśli nie interesuje Cię savemetoda zastępowania , możesz to zrobić

  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()
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.