เมื่อบันทึกคุณจะตรวจสอบว่ามีการเปลี่ยนแปลงฟิลด์ได้อย่างไร


293

ในแบบจำลองของฉันฉันมี:

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

ซึ่งใช้งานได้ดีเยี่ยมเป็นครั้งแรกกับremote_imageการเปลี่ยนแปลง

ฉันจะดึงรูปภาพใหม่ได้อย่างไรเมื่อมีคนทำการแก้ไขremote_imageในนามแฝง? และประการที่สองมีวิธีที่ดีกว่าในการแคชอิมเมจระยะไกลหรือไม่?

คำตอบ:


423

เป็นหลักคุณต้องการแทนที่__init__วิธีการmodels.Modelเพื่อให้คุณเก็บสำเนาของค่าเดิม สิ่งนี้ทำให้คุณไม่ต้องทำการค้นหา DB อีกครั้ง (ซึ่งเป็นสิ่งที่ดีเสมอ)

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
แทนการเขียนทับ init ผมใช้ post_init สัญญาณdocs.djangoproject.com/en/dev/ref/signals/#post-init
vikingosegundo

22
แนะนำให้ใช้วิธีการเอาชนะโดยเอกสารของ Django: docs.djangoproject.com/en/dev/topics/db/models/…
พันเอกส

10
@Callum เพื่อที่ว่าถ้าคุณทำการเปลี่ยนแปลงวัตถุให้บันทึกจากนั้นทำการเปลี่ยนแปลงเพิ่มเติมและเรียกsave()มันอีกครั้งมันจะยังคงทำงานได้อย่างถูกต้อง
philfreo

17
@Josh จะไม่มีปัญหาหากคุณมีเซิร์ฟเวอร์แอปพลิเคชั่นหลายตัวที่ทำงานกับฐานข้อมูลเดียวกันเพราะติดตามการเปลี่ยนแปลงของหน่วยความจำเท่านั้น
Jens Alm

13
@lajarre ฉันคิดว่าความคิดเห็นของคุณเป็นความเข้าใจผิดเล็กน้อย เอกสารแนะนำให้คุณดูแลเมื่อคุณทำเช่นนั้น พวกเขาไม่แนะนำให้ต่อต้าน
จอช

199

ฉันใช้ 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])

การใช้งาน:

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

บันทึก

โปรดทราบว่าโซลูชันนี้ทำงานได้ดีในบริบทของคำขอปัจจุบันเท่านั้น ดังนั้นจึงเหมาะสำหรับกรณีที่เรียบง่ายเป็นหลัก ในสภาพแวดล้อมพร้อมกันที่คำขอหลายรายการสามารถจัดการกับอินสแตนซ์โมเดลเดียวกันในเวลาเดียวกันคุณต้องมีแนวทางที่แตกต่างกัน


4
สมบูรณ์แบบจริงๆและไม่ต้องทำการสืบค้นเพิ่มเติม ขอบคุณมาก !
Stéphane

28
+1 สำหรับ mixin ที่ใช้ +1 สำหรับการไม่มี DB จำนวนเยี่ยมชม +1 สำหรับวิธีการ / คุณสมบัติที่มีประโยชน์มากมาย ฉันต้องสามารถลงคะแนนได้หลายครั้ง
เจค

ใช่. อีกอย่างหนึ่งสำหรับการใช้ Mixin และไม่มีการเพิ่มฐานข้อมูล
David S

2
Mixin นั้นยอดเยี่ยม แต่รุ่นนี้มีปัญหาเมื่อใช้ร่วมกับ. only () การเรียกใช้ Model.objects.only ('id') จะนำไปสู่การเรียกซ้ำแบบไม่สิ้นสุดหาก Model มีฟิลด์อย่างน้อย 3 ฟิลด์ เพื่อแก้ปัญหานี้เราควรลบเขตข้อมูลที่ถูกเลื่อนออกไปจากการบันทึกในคุณสมบัติเริ่มต้นและเปลี่ยนคุณสมบัติ _dict เล็กน้อย
gleb.pitsevich

19
เช่นเดียวกับคำตอบของ Josh รหัสนี้จะทำงานได้ดีบนเซิร์ฟเวอร์การทดสอบกระบวนการเดียวของคุณ แต่เมื่อคุณปรับใช้กับเซิร์ฟเวอร์ประมวลผลแบบใด ๆ ก็จะให้ผลลัพธ์ที่ไม่ถูกต้อง คุณไม่สามารถรู้ว่าคุณกำลังเปลี่ยนค่าในฐานข้อมูลโดยไม่ต้องสอบถามฐานข้อมูลหรือไม่
rspeer

154

วิธีที่ดีที่สุดอยู่กับpre_saveสัญญาณ อาจไม่ใช่ตัวเลือกกลับมาในปี '09 เมื่อมีการถามและตอบคำถามนี้ แต่ใครก็ตามที่เห็นในวันนี้ควรทำเช่นนี้:

@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
เหตุใดจึงเป็นวิธีที่ดีที่สุดหากวิธีการที่ Josh อธิบายด้านบนไม่เกี่ยวข้องกับการตีฐานข้อมูลเพิ่มเติม
joshcartme

36
1) วิธีการนั้นคือแฮ็คสัญญาณได้รับการออกแบบมาโดยทั่วไปสำหรับการใช้งานเช่นนี้ 2) วิธีนั้นต้องทำการดัดแปลงโมเดลของคุณแบบนี้ไม่ได้ 3) อย่างที่คุณสามารถอ่านในความคิดเห็นเกี่ยวกับคำตอบนั้นได้ อาจเป็นปัญหาได้วิธีนี้ไม่สามารถทำได้
Chris Pratt

2
วิธีนี้เป็นวิธีที่ดีถ้าคุณสนใจเพียงแค่ติดตามการเปลี่ยนแปลงก่อนที่จะบันทึก อย่างไรก็ตามสิ่งนี้จะไม่ทำงานหากคุณต้องการตอบสนองต่อการเปลี่ยนแปลงในทันที ฉันได้พบกับสถานการณ์หลังหลายครั้ง (และฉันกำลังทำงานกับอินสแตนซ์หนึ่งตอนนี้)
Josh

5
@Josh: คุณหมายถึงอะไรโดย "ตอบสนองต่อการเปลี่ยนแปลงทันที"? สิ่งนี้จะไม่ทำให้คุณ "ตอบสนอง" ในทางใด?
Chris Pratt

2
ขออภัยฉันลืมขอบเขตของคำถามนี้และอ้างถึงปัญหาที่แตกต่างอย่างสิ้นเชิง ที่กล่าวว่าฉันคิดว่าสัญญาณเป็นวิธีที่ดีที่จะไปที่นี่ (ตอนนี้พวกเขามีให้) อย่างไรก็ตามฉันพบว่ามีหลายคนที่คิดว่าจะเอาชนะ "แฮ็ค" ฉันไม่เชื่อว่าเป็นกรณีนี้ ตามที่คำตอบนี้แนะนำ ( stackoverflow.com/questions/170337/… ) ฉันคิดว่าการเอาชนะเป็นวิธีปฏิบัติที่ดีที่สุดเมื่อคุณไม่ได้ทำงานกับการเปลี่ยนแปลงที่เป็น "เฉพาะรุ่นที่เป็นปัญหา" ที่กล่าวว่าฉันไม่ได้ตั้งใจที่จะกำหนดความเชื่อที่ทุกคน
Josh

138

และตอนนี้สำหรับคำตอบโดยตรง: วิธีหนึ่งในการตรวจสอบว่าค่าของเขตข้อมูลมีการเปลี่ยนแปลงคือการดึงข้อมูลต้นฉบับจากฐานข้อมูลก่อนที่จะบันทึกอินสแตนซ์ ลองพิจารณาตัวอย่างนี้:

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)

สิ่งเดียวกันนี้ใช้เมื่อทำงานกับแบบฟอร์ม คุณสามารถตรวจจับได้ที่วิธี clean หรือ save ของ 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
โซลูชันของ Josh เป็นฐานข้อมูลที่เป็นมิตรมากกว่า การโทรเพิ่มเติมเพื่อตรวจสอบสิ่งที่เปลี่ยนแปลงมีราคาแพง
วว

4
อีกหนึ่งการอ่านก่อนที่คุณจะเขียนไม่แพงเลย นอกจากนี้วิธีการติดตามการเปลี่ยนแปลงจะไม่ทำงานหากมีหลายคำขอ แม้ว่าสิ่งนี้จะได้รับผลกระทบจากสภาพการแข่งขันระหว่างการดึงและการบันทึก
dalore

1
หยุดบอกให้คนอื่นตรวจสอบpk is not Noneว่าไม่ได้ใช้ตัวอย่างเช่นถ้าใช้ UUIDField นี่เป็นเพียงคำแนะนำที่ไม่ดี
user3467349

2
@ Dalore คุณสามารถหลีกเลี่ยงสภาพการแข่งขันโดยการตกแต่งวิธีการบันทึกด้วย@transaction.atomic
Frank Pape

2
@ Dalore แม้ว่าคุณจะต้องแน่ใจว่าระดับการแยกธุรกรรมนั้นเพียงพอ ใน PostgreSQL เริ่มต้นคือการอ่านความมุ่งมั่น แต่การทำซ้ำการอ่านเป็นสิ่งที่จำเป็น
Frank Pape

58

ตั้งแต่ Django 1.8 วางจำหน่ายคุณสามารถใช้from_db classmethod เพื่อแคชค่าเก่าของ remote_image จากนั้นในวิธีการบันทึกคุณสามารถเปรียบเทียบค่าของฟิลด์เก่าและใหม่เพื่อตรวจสอบว่ามีการเปลี่ยนแปลงค่าหรือไม่

@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
ขอบคุณ - นี่คือการอ้างอิงถึงเอกสาร: docs.djangoproject.com/en/1.8/ref/models/instance/ … ฉันเชื่อว่าสิ่งนี้ยังคงส่งผลให้เกิดปัญหาดังกล่าวข้างต้นซึ่งฐานข้อมูลอาจเปลี่ยนแปลงระหว่างเมื่อมีการประเมินและเมื่อทำการเปรียบเทียบ แต่นี่เป็นตัวเลือกใหม่ที่ดี
trpt4him

1
แทนที่จะค้นหาค่า (ซึ่งเป็น O (n) ตามจำนวนค่า) มันจะเร็วกว่าและชัดเจนกว่าnew._loaded_remote_image = new.remote_imageหรือไม่?
dalore

1
น่าเสียดายที่ฉันต้องย้อนกลับความคิดเห็นก่อนหน้าของฉัน (ตอนนี้ถูกลบ) ในขณะที่from_dbถูกเรียกโดยrefresh_from_dbแอตทริบิวต์ในอินสแตนซ์ (เช่นโหลดหรือก่อนหน้า) จะไม่ได้รับการอัพเดต เป็นผลให้ฉันไม่สามารถหาเหตุผลว่าทำไมนี้จะดีกว่า__init__ในขณะที่คุณยังคงต้องจัดการกับ 3 กรณี __init__/ from_db, และrefresh_from_db save
claytond


18

หากคุณกำลังใช้แบบฟอร์มคุณสามารถใช้change_data ( เอกสาร ) ของแบบฟอร์ม:

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

ในฐานะของ Django 1.8 มีfrom_dbวิธีการดังที่ Serge กล่าวถึง ในความเป็นจริงเอกสาร Django รวมถึงกรณีการใช้งานเฉพาะนี้เป็นตัวอย่าง:

https://docs.djangoproject.com/en/dev/ref/models/instances/#customizing-model-loading

ด้านล่างเป็นตัวอย่างที่แสดงวิธีการบันทึกค่าเริ่มต้นของเขตข้อมูลที่โหลดจากฐานข้อมูล


5

สิ่งนี้ใช้ได้กับฉันใน Django 1.8

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

4

คุณสามารถใช้django-model-changeเพื่อทำสิ่งนี้โดยไม่ต้องค้นหาฐานข้อมูลเพิ่มเติม:

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

อีกหนึ่งคำตอบที่ล่าช้า แต่ถ้าคุณเพียงแค่พยายามดูว่ามีการอัปโหลดไฟล์ใหม่ไปยังฟิลด์ไฟล์หรือไม่ให้ลองทำสิ่งนี้: (ดัดแปลงมาจากความคิดเห็นของ Christopher Adams ที่ลิงก์http://zmsmith.com/2010/05/django) -check-if-a-field-has- change /ในความคิดเห็นของ zach ที่นี่)

อัปเดตลิงก์: 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

นั่นเป็นทางออกที่ยอดเยี่ยมสำหรับการตรวจสอบว่ามีการอัพโหลดไฟล์ใหม่หรือไม่ ดีกว่าการตรวจสอบชื่อกับฐานข้อมูลเพราะชื่อของไฟล์อาจเหมือนกัน คุณสามารถใช้มันในpre_saveตัวรับสัญญาณได้เช่นกัน ขอบคุณที่แชร์สิ่งนี้!
DataGreed

1
นี่คือตัวอย่างสำหรับการอัปเดตระยะเวลาเสียงในฐานข้อมูลเมื่อไฟล์ถูกอัปเดตโดยใช้ mutagen เพื่ออ่านข้อมูลเสียง - gist.github.com/DataGreed/1ba46ca7387950abba2ff53baf70fec2
DataGreed

3

ทางออกที่ดีที่สุดน่าจะเป็นวิธีที่ไม่รวมการดำเนินการอ่านฐานข้อมูลเพิ่มเติมก่อนที่จะบันทึกอินสแตนซ์ของรุ่นหรือไลบรารี django เพิ่มเติมใด ๆ นี่คือเหตุผลที่โซลูชันของ laffuste เป็นที่ต้องการ ในบริบทของไซต์ผู้ดูแลระบบผู้ใช้สามารถแทนที่save_model-method และเรียกใช้has_changedเมธอดของแบบฟอร์มที่นั่นเช่นเดียวกับในคำตอบของ Sion ด้านบน คุณมาถึงสิ่งนี้วาดภาพการตั้งค่าตัวอย่างของ Sion แต่ใช้changed_dataเพื่อรับการเปลี่ยนแปลงทุกอย่าง:

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)
  • แทนที่save_model:

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

  • วิธีการในchanged_dataตัวสำหรับฟิลด์:

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


2

แม้ว่าสิ่งนี้จะไม่ตอบคำถามของคุณ แต่ฉันจะทำอย่างนี้ในวิธีอื่น

ล้างremote_imageข้อมูลในฟิลด์หลังจากบันทึกสำเนาโลคัลเรียบร้อยแล้ว จากนั้นในวิธีการบันทึกของคุณคุณสามารถอัปเดตภาพได้ทุกเมื่อremote_imageไม่ว่างเปล่า

หากคุณต้องการอ้างอิงถึง url คุณสามารถใช้ฟิลด์บูลีนที่แก้ไขไม่ได้เพื่อจัดการค่าสถานะแคชแทนremote_imageฟิลด์ตัวเอง


2

ฉันมีสถานการณ์นี้ก่อนที่จะแก้ปัญหาของฉันคือการแทนที่pre_save()วิธีการของคลาสฟิลด์เป้าหมายมันจะถูกเรียกว่าถ้าสนามมีการเปลี่ยนแปลงที่
มีประโยชน์กับตัวอย่าง 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

ข้อเสีย:
ไม่มีประโยชน์ถ้าคุณต้องการดำเนินการใด ๆ (post_save) เช่นการใช้วัตถุที่สร้างขึ้นในบางงาน (ถ้ามีการเปลี่ยนแปลงบางฟิลด์)


2

การปรับปรุง @josh คำตอบสำหรับทุกสาขา:

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)

เพื่อชี้แจงให้ชัดเจน getattr ทำงานเพื่อรับฟิลด์ที่เหมือนกับperson.nameสตริง (เช่นgetattr(person, "name")


และมันยังไม่ได้ทำการสืบค้นฐานข้อมูลเพิ่มเติมหรือไม่
andilabs

ฉันพยายามใช้รหัสของคุณ มันทำงานได้ดีโดยการแก้ไขฟิลด์ แต่ตอนนี้ฉันมีปัญหากับการแทรกใหม่ ฉันได้ DoesNotExist สำหรับสาขา FK ของฉันในชั้นเรียน คำแนะนำวิธีการแก้ปัญหาบางอย่างจะได้รับการชื่นชม
andilabs

ฉันเพิ่งอัปเดตรหัสตอนนี้ข้ามคีย์ต่างประเทศดังนั้นคุณไม่จำเป็นต้องดึงไฟล์เหล่านั้นด้วยข้อความค้นหาเพิ่มเติม (แพงมาก) และหากวัตถุไม่มีอยู่มันจะข้ามตรรกะพิเศษ
Hassek

1

ฉันได้ขยาย mixin ของ @livskiy ดังนี้:

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

และ DictField คือ:

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)

มันสามารถใช้งานได้โดยการขยายมันในแบบจำลองของคุณฟิลด์ _dict จะถูกเพิ่มเมื่อคุณซิงค์ / โยกย้ายและฟิลด์นั้นจะเก็บสถานะของวัตถุของคุณ


1

วิธีการใช้โซลูชันของ David Cramer:

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

ฉันเคยประสบความสำเร็จใช้มันเช่นนี้:

@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
หากคุณลืม super (Mode, self) .save (* args, ** kwargs) จากนั้นคุณจะปิดการใช้งานฟังก์ชั่นการบันทึกดังนั้นอย่าลืมใส่ไว้ในวิธีการบันทึก
สูงสุด

ลิงค์ของบทความนี้ล้าสมัยนี่คือลิงค์ใหม่: cra.mr/2010/12/06/tracking-changes-to-fields-in-django
GoTop

1

การแก้ไขคำตอบของ @ 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

วิธีนี้ใช้วิธีสาธารณะของ django 1.10 get_fieldsแทน สิ่งนี้ทำให้การพิสูจน์รหัสในอนาคตมากขึ้น แต่ที่สำคัญกว่านั้นยังรวมถึงคีย์และเขตข้อมูลต่างประเทศที่สามารถแก้ไข = False

สำหรับการอ้างอิงนี่คือการดำเนินการของ .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

นี่เป็นอีกวิธีในการทำมัน

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)

ตามเอกสารประกอบ: การตรวจสอบวัตถุ

"ขั้นตอนที่สอง full_clean () ดำเนินการคือการเรียก Model.clean () วิธีการนี้ควรถูกแทนที่เพื่อทำการตรวจสอบความถูกต้องที่กำหนดเองในแบบจำลองของคุณวิธีการนี้ควรใช้ในการตรวจสอบความถูกต้องของแบบจำลองที่กำหนดเอง ตัวอย่างเช่นคุณสามารถใช้เพื่อระบุค่าให้กับเขตข้อมูลโดยอัตโนมัติหรือทำการตรวจสอบความถูกต้องที่ต้องเข้าถึงมากกว่าหนึ่งเขตข้อมูล: "


1

มีแอตทริบิวต์ __dict__ ซึ่งมีฟิลด์ทั้งหมดเป็นคีย์และค่าเป็นค่าของฟิลด์ เราสามารถเปรียบเทียบสองอันได้

เพียงแค่เปลี่ยนฟังก์ชั่นการบันทึกของรุ่นเป็นฟังก์ชั่นด้านล่าง

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)

ตัวอย่างการใช้งาน:

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)

ให้ผลผลิตที่มีเพียงเขตข้อมูลที่มีการเปลี่ยนแปลง

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

1

ช้าไปมากกับเกม แต่นี่เป็นคำตอบของChris Prattที่ปกป้องสภาพการแข่งขันในขณะที่เสียสละประสิทธิภาพโดยใช้transactionบล็อกและselect_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

ในฐานะที่เป็นส่วนขยายของคำตอบของ SmileyChris คุณสามารถเพิ่มเขตข้อมูลวันที่และเวลาลงในโมเดลสำหรับ last_updated และตั้งค่าขีด จำกัด บางอย่างสำหรับอายุสูงสุดที่คุณจะได้รับก่อนที่จะตรวจสอบการเปลี่ยนแปลง


0

mixin จาก @ivanlivski ยอดเยี่ยมมาก

ฉันขยายมันไป

  • ตรวจสอบให้แน่ใจว่ามันทำงานได้กับเขตทศนิยม
  • เปิดเผยคุณสมบัติเพื่อทำให้การใช้งานง่ายขึ้น

รหัสที่อัปเดตมีให้ที่นี่: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py

เพื่อช่วยให้ผู้คนใหม่กับ Python หรือ Django ฉันจะยกตัวอย่างที่สมบูรณ์มากขึ้น การใช้งานเฉพาะนี้คือการรับไฟล์จากผู้ให้บริการข้อมูลและตรวจสอบให้แน่ใจว่าบันทึกในฐานข้อมูลแสดงถึงไฟล์

วัตถุต้นแบบของฉัน:

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

คลาสที่โหลดไฟล์มีเมธอดเหล่านี้:

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

หากคุณไม่พบความสนใจในsaveวิธีการเอาชนะคุณสามารถทำได้

  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()
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.