Saat menyimpan, bagaimana Anda dapat memeriksa apakah suatu bidang telah berubah?

293

Dalam model saya, saya punya:

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

Yang bekerja sangat bagus untuk pertama kalinya remote_imageperubahan.

Bagaimana saya bisa mengambil gambar baru ketika seseorang telah memodifikasi remote_imagealias? Dan kedua, apakah ada cara yang lebih baik untuk menyimpan gambar jarak jauh?

Paul Tarjan
sumber

Jawaban:

423

Pada dasarnya, Anda ingin mengganti __init__metode models.Modelsehingga Anda menyimpan salinan dari nilai aslinya. Ini membuatnya agar Anda tidak perlu melakukan pencarian DB lain (yang selalu merupakan hal yang baik).

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
Josh
sumber
24
bukan menimpa init, saya akan menggunakan post_init-sinyal docs.djangoproject.com/en/dev/ref/signals/#post-init
vikingosegundo
22
Metode utama direkomendasikan oleh dokumentasi Django: docs.djangoproject.com/en/dev/topics/db/models/…
Kolonel Sponsz
10
@callum sehingga jika Anda membuat perubahan pada objek, menyimpannya, lalu membuat perubahan tambahan dan memanggilnya save()LAGI, itu masih akan berfungsi dengan benar.
philfreo
17
@Josh tidak akan ada masalah jika Anda memiliki beberapa server aplikasi yang bekerja melawan database yang sama karena hanya melacak perubahan dalam memori
Jens Alm
13
@ajarre, saya pikir komentar Anda agak menyesatkan. Dokumen menyarankan agar Anda berhati-hati saat melakukannya. Mereka tidak merekomendasikan hal itu.
Josh
199

Saya menggunakan mixin berikut:

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

Pemakaian:

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

Catatan

Harap dicatat bahwa solusi ini berfungsi baik dalam konteks permintaan saat ini saja. Jadi itu cocok terutama untuk kasus-kasus sederhana. Dalam lingkungan bersamaan di mana banyak permintaan dapat memanipulasi contoh model yang sama pada saat yang sama, Anda pasti membutuhkan pendekatan yang berbeda.

iperelivskiy
sumber
4
Benar-benar sempurna, dan jangan melakukan kueri tambahan. Terima kasih banyak !
Stéphane
28
+1 untuk mixin yang menggunakan. +1 tanpa hit DB tambahan. +1 untuk banyak metode / properti yang berguna. Saya harus dapat meningkatkan suara beberapa kali.
Jake
ya. Plus satu untuk menggunakan Mixin dan tidak ada hit db tambahan.
David S
2
Mixin bagus, tetapi versi ini memiliki masalah saat digunakan bersama dengan .only (). Panggilan ke Model.objects.only ('id') akan menyebabkan rekursi tak terbatas jika Model memiliki setidaknya 3 bidang. Untuk mengatasi ini, kita harus menghapus bidang yang ditangguhkan dari menyimpan inisial dan mengubah properti _dict sedikit
gleb.pitsevich
19
Sama seperti jawaban Josh, kode ini akan bekerja dengan baik pada server pengujian satu proses Anda, tetapi saat Anda menyebarkannya ke server multi-pemrosesan apa pun, kode ini akan memberikan hasil yang salah. Anda tidak bisa tahu apakah Anda mengubah nilai dalam database tanpa meminta database.
rspeer
154

Cara terbaik adalah dengan pre_savesinyal. Mungkin bukan pilihan di tahun '09 ketika pertanyaan ini diajukan dan dijawab, tetapi siapa pun yang melihat ini hari ini harus melakukannya dengan cara ini:

@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
Chris Pratt
sumber
6
Mengapa ini cara terbaik jika metode yang dijelaskan Josh di atas tidak melibatkan hit database tambahan?
joshcartme
36
1) metode itu adalah peretasan, sinyal pada dasarnya dirancang untuk penggunaan seperti ini 2) metode itu memerlukan perubahan pada model Anda, yang ini tidak 3) seperti yang Anda baca di komentar pada jawaban itu, ia memiliki efek samping yang dapat berpotensi bermasalah, solusi ini tidak
Chris Pratt
2
Cara ini bagus jika Anda hanya ingin menangkap perubahan sesaat sebelum menabung. Namun, ini tidak akan berfungsi jika Anda ingin segera bereaksi terhadap perubahan. Saya telah menemukan skenario terakhir beberapa kali (dan saya sedang mengerjakan satu contoh seperti itu sekarang).
Josh
5
@Josh: Apa yang Anda maksud dengan "segera bereaksi terhadap perubahan"? Dengan cara apa ini tidak membiarkan Anda "bereaksi"?
Chris Pratt
2
Maaf, saya lupa lingkup pertanyaan ini dan merujuk pada masalah yang sama sekali berbeda. Yang mengatakan, saya pikir sinyal adalah cara yang baik untuk pergi ke sini (sekarang tersedia). Namun, saya menemukan banyak orang mempertimbangkan mengganti save "hack." Saya tidak percaya ini masalahnya. Seperti yang disarankan jawaban ini ( stackoverflow.com/questions/170337/… ), saya pikir mengesampingkan adalah praktik terbaik ketika Anda tidak bekerja pada perubahan yang "spesifik untuk model yang dimaksud." Karena itu, saya tidak bermaksud memaksakan kepercayaan itu kepada siapa pun.
Josh
138

Dan sekarang untuk jawaban langsung: salah satu cara untuk memeriksa apakah nilai untuk bidang telah berubah adalah dengan mengambil data asli dari database sebelum menyimpan contoh. Pertimbangkan contoh ini:

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)

Hal yang sama berlaku ketika bekerja dengan formulir. Anda dapat mendeteksinya di metode clean atau save dari 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 = []
zgoda
sumber
24
Solusi Josh jauh lebih ramah basis data. Panggilan ekstra untuk memverifikasi apa yang berubah itu mahal.
dd.
5
Satu baca tambahan sebelum Anda menulis tidak terlalu mahal. Metode pelacakan perubahan juga tidak berfungsi jika ada beberapa permintaan. Meskipun ini akan menderita dari kondisi balapan di antara pengambilan dan tabungan.
dalore
1
Berhenti memberi tahu orang untuk memeriksanya pk is not Nonetidak berlaku misalnya jika menggunakan UUIDField. Ini hanya saran yang buruk.
user3467349
2
@dalore Anda dapat menghindari kondisi balapan dengan mendekorasi metode penyelamatan dengan@transaction.atomic
Frank Pape
2
@dalore meskipun Anda harus memastikan tingkat isolasi transaksi sudah cukup. Dalam postgresql, default dibaca komit, tetapi baca berulang diperlukan .
Frank Pape
58

Sejak Django 1.8 dirilis, Anda dapat menggunakan from_db classmethod untuk cache nilai lama dari remote_image. Kemudian dalam metode simpan, Anda dapat membandingkan nilai bidang lama dan baru untuk memeriksa apakah nilainya berubah.

@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!
Serge
sumber
1
Terima kasih - inilah referensi ke dokumen: docs.djangoproject.com/en/1.8/ref/models/instances/… . Saya percaya ini masih menghasilkan masalah yang disebutkan di mana database dapat berubah antara saat ini dievaluasi dan ketika perbandingan dilakukan, tetapi ini adalah opsi baru yang bagus.
trpt4him
1
Daripada mencari nilai-nilai (yang O (n) berdasarkan jumlah nilai) bukankah lebih cepat dan lebih jelas untuk dilakukan new._loaded_remote_image = new.remote_image?
dalore
1
Sayangnya saya harus membalik komentar saya sebelumnya (sekarang dihapus). Sementara from_dbdipanggil oleh refresh_from_db, atribut pada instance (yaitu dimuat atau sebelumnya) tidak diperbarui. Akibatnya, saya tidak dapat menemukan alasan mengapa ini lebih baik daripada __init__yang Anda masih perlu untuk menangani 3 kasus: __init__/ from_db, refresh_from_db, dan save.
claytond
18

Jika Anda menggunakan formulir, Anda dapat menggunakan Formulir yang diubah_data ( dokumen ):

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
nafsu birahi
sumber
6

Saya agak terlambat ke pesta tetapi saya menemukan solusi ini juga: Django Dirty Fields

Fred Campos
sumber
5

Ini bekerja untuk saya di Django 1.8

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

Anda dapat menggunakan django-model-changes untuk melakukan ini tanpa pencarian basis data tambahan:

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
Robert Kajic
sumber
4

Jawaban terlambat lainnya, tetapi jika Anda hanya mencoba melihat apakah file baru telah diunggah ke bidang file, coba ini: (diadaptasi dari komentar Christopher Adams pada tautan http://zmsmith.com/2010/05/django -check-if-a-field-telah-berubah / dalam komentar zach di sini)

Tautan yang diperbarui: 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
Aaron McMillin
sumber
Itu solusi yang luar biasa untuk memeriksa apakah file baru diunggah. Jauh lebih baik daripada memeriksa nama terhadap database karena nama file bisa sama. Anda dapat menggunakannya di pre_savepenerima juga. Terima kasih telah berbagi ini!
DataGreed
1
Berikut adalah contoh untuk memperbarui durasi audio dalam database ketika file diperbarui menggunakan mutagen untuk membaca info audio - gist.github.com/DataGreed/1ba46ca7387950abba2ff53baf70fec2
DataGreed
3

Solusi optimal mungkin salah satu yang tidak termasuk operasi pembacaan basis data tambahan sebelum menyimpan instance model, atau pustaka Django lebih lanjut. Inilah mengapa solusi laffuste lebih disukai. Dalam konteks situs admin, seseorang dapat dengan mudah menimpa save_model-metode, dan memanggil metode formulir di has_changedsana, seperti dalam jawaban Sion di atas. Anda tiba di sesuatu seperti ini, menggambar pada pengaturan contoh Sion tetapi menggunakan changed_datauntuk mendapatkan setiap kemungkinan perubahan:

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)
  • Timpa save_model:

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

  • Built-in changed_data-metode untuk Field:

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

pengguna3061675
sumber
2

Meskipun ini tidak benar-benar menjawab pertanyaan Anda, saya akan melakukannya dengan cara yang berbeda.

Cukup kosongkan remote_imagebidang setelah berhasil menyimpan salinan lokal. Kemudian dalam metode simpan Anda, Anda selalu dapat memperbarui gambar kapan pun remote_imagetidak kosong.

Jika Anda ingin menyimpan referensi ke url, Anda bisa menggunakan bidang boolean yang tidak dapat diedit untuk menangani flag caching daripada remote_imagebidang itu sendiri.

SmileyChris
sumber
2

Saya mengalami situasi ini sebelum solusi saya adalah untuk menimpa pre_save()metode kelas bidang target itu akan dipanggil hanya jika bidang telah diubah
berguna dengan contoh 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

kerugian:
tidak berguna jika Anda ingin melakukan operasi (post_save) seperti menggunakan objek yang dibuat dalam beberapa pekerjaan (jika bidang tertentu telah berubah)

MYaser
sumber
2

meningkatkan jawaban @josh untuk semua bidang:

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)

hanya untuk memperjelas, getattr berfungsi untuk mendapatkan bidang seperti person.namedengan string (yaitugetattr(person, "name")

Hassek
sumber
Dan masih belum membuat permintaan db ekstra?
andilabs
Saya mencoba menerapkan kode Anda. Ini berfungsi ok dengan mengedit bidang. Tapi sekarang saya punya masalah dengan memasukkan yang baru. Saya mendapatkan DoesNotExist untuk bidang FK saya di kelas. Beberapa petunjuk bagaimana menyelesaikannya akan sangat dihargai.
andilabs
Saya baru saja memperbarui kode, sekarang melompati kunci asing sehingga Anda tidak perlu mengambil file-file itu dengan pertanyaan tambahan (sangat mahal) dan jika objek tidak ada itu akan melewati logika tambahan.
Hassek
1

Saya telah memperpanjang mixin dari @livskiy sebagai berikut:

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

dan DictField adalah:

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)

itu dapat digunakan dengan memperluasnya dalam model Anda bidang _dict akan ditambahkan ketika Anda menyinkronkan / bermigrasi dan bidang itu akan menyimpan keadaan objek Anda

rev MYaser
sumber
1

Bagaimana dengan menggunakan solusi David Cramer:

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

Saya sudah sukses menggunakannya seperti ini:

@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!"
Sion
sumber
2
Jika Anda lupa super (Mode, self) .save (* args, ** kwargs) maka Anda menonaktifkan fungsi save jadi ingatlah untuk memasukkan ini ke dalam metode save.
maks
Tautan artikel sudah usang, ini tautan baru: cra.mr/2010/12/06/tracking-changes-to-fields-in-django
GoTop
1

Sebuah modifikasi pada jawaban @ 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

Ini menggunakan metode publik Django 1.10 get_fieldssebagai gantinya. Ini membuat kode lebih banyak bukti di masa depan, tetapi yang lebih penting juga mencakup kunci dan bidang asing di mana dapat diedit = Salah.

Untuk referensi, berikut adalah implementasinya .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))
    )
theicfire
sumber
1

Inilah cara lain untuk melakukannya.

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)

Sesuai dokumentasi: memvalidasi objek

"Langkah kedua yang dilakukan full_clean () adalah memanggil Model.clean (). Metode ini harus diganti untuk melakukan validasi khusus pada model Anda. Metode ini harus digunakan untuk memberikan validasi model khusus, dan untuk memodifikasi atribut pada model Anda jika diinginkan Misalnya, Anda dapat menggunakannya untuk secara otomatis memberikan nilai untuk suatu bidang, atau untuk melakukan validasi yang memerlukan akses ke lebih dari satu bidang: "

Gonzalo
sumber
1

Ada atribut __dict__ yang memiliki semua bidang sebagai kunci dan nilai sebagai nilai bidang. Jadi kita bisa membandingkan keduanya

Cukup ubah fungsi simpan model ke fungsi di bawah ini

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)

Contoh penggunaan:

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)

menghasilkan output dengan hanya bidang-bidang yang telah diubah

{'name': {'initial_value': '1234515', 'final_value': 'nim'}, 'senior': {'initial_value': 'no', 'final_value': 'yes'}}
Nimish Bansal
sumber
1

Sangat terlambat ke permainan, tetapi ini adalah versi jawaban Chris Pratt yang melindungi terhadap kondisi balapan sambil mengorbankan kinerja, dengan menggunakan transactionblok danselect_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
baqyoteto
sumber
0

sebagai perpanjangan dari jawaban SmileyChris, Anda dapat menambahkan bidang datetime ke model untuk last_updated, dan menetapkan semacam batas untuk usia maksimum yang akan Anda biarkan sebelum memeriksa perubahan

Jiaaro
sumber
0

Mixin dari @ivanlivski bagus.

Saya telah memperpanjangnya

  • Pastikan itu bekerja dengan bidang Desimal.
  • Mengekspos properti untuk menyederhanakan penggunaan

Kode yang diperbarui tersedia di sini: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py

Untuk membantu orang yang baru mengenal Python atau Django, saya akan memberikan contoh yang lebih lengkap. Penggunaan khusus ini adalah untuk mengambil file dari penyedia data dan memastikan catatan dalam database mencerminkan file.

Objek model saya:

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

Kelas yang memuat file memiliki metode ini:

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()
sknutsonsf
sumber
0

Jika Anda tidak menemukan minat dalam savemetode penggantian , Anda dapat melakukannya

  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()
theTypan
sumber