Django:保存时,如何检查字段是否已更改?

时间:2009-08-30 22:56:27

标签: django image caching django-models

在我的模特中,我有:

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时,如何获取新图像?其次,是否有更好的方法来缓存远程图像?

26 个答案:

答案 0 :(得分:379)

虽然时间有点晚,但是让我为其他人发布这个解决方案。基本上,您希望覆盖__init__的{​​{1}}方法,以便保留原始值的副本。这使得您不必再进行其他数据库查找(这总是一件好事)。

models.Model

答案 1 :(得分:171)

我使用以下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)
>>>

注意

请注意,此解决方案仅适用于当前请求的上下文。因此,它主要适用于简单的情况。在并发环境中,多个请求可以同时操作同一个模型实例,您肯定需要一种不同的方法。

答案 2 :(得分:133)

现在直接回答:检查字段值是否已更改的一种方法是在保存实例之前从数据库中获取原始数据。考虑这个例子:

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)

使用表单时同样适用。您可以在ModelForm的clean或save方法中检测它:

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 = []

答案 3 :(得分:131)

最好的方法是使用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

答案 4 :(得分:52)

自Django 1.8发布以来,您可以使用 from_db classmethod来缓存remote_image的旧值。然后在 save 方法中,您可以比较字段的旧值和新值,以检查值是否已更改。

@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!

答案 5 :(得分:16)

请注意,字段更改跟踪在django-model-utils中可用。

https://django-model-utils.readthedocs.org/en/latest/index.html

答案 6 :(得分:13)

如果您使用的是表单,则可以使用表单的 changed_data docs):

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

答案 7 :(得分:5)

从Django 1.8开始,有from_db方法,正如Serge提到的那样。事实上,Django文档将此特定用例作为示例包含在内:

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

  

下面是一个示例,说明如何记录从数据库加载的字段的初始值

答案 8 :(得分:5)

我参加派对有点晚了,但我也发现了这个解决方案: Django Dirty Fields

答案 9 :(得分:4)

您无需额外的数据库查找即可使用django-model-changes执行此操作:

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

答案 10 :(得分:3)

最佳解决方案可能是在保存模型实例之前不包含其他数据库读取操作的解决方案,也不包括任何进一步的django-library。这就是为什么laffuste的解决方案更可取的原因。在管理站点的上下文中,可以简单地覆盖save_model方法,并在那里调用表单的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

  • Field的内置changed_data-method:

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

答案 11 :(得分:3)

这适用于Django 1.8

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

答案 12 :(得分:2)

另一个迟到的答案,但如果您只是想查看是否已将新文件上传到文件字段,请尝试以下操作:(改编自Christopher Adams对zach评论中链接http://zmsmith.com/2010/05/django-check-if-a-field-has-changed/的评论)

更新了链接: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

答案 13 :(得分: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")

答案 14 :(得分: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)操作,如在某个作业中使用创建的对象(如果某些字段已更改),则无用

答案 15 :(得分:2)

虽然这实际上没有回答你的问题,但我会以不同的方式解决这个问题。

成功保存本地副本后,只需清除remote_image字段即可。然后,在保存方法中,只要remote_image不为空,就可以随时更新图像。

如果您想保留对url的引用,可以使用不可编辑的布尔字段来处理缓存标志,而不是remote_image字段本身。

答案 16 :(得分: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)

根据文件:validating objects

" full_clean()执行的第二步是调用Model.clean()。应重写此方法以对模型执行自定义验证。 此方法应用于提供自定义模型验证,并根据需要修改模型上的属性。例如,您可以使用它自动为字段提供值,或者进行需要访问多个字段的验证:"

答案 17 :(得分: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'}}

答案 18 :(得分: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!"

答案 19 :(得分:1)

在游戏中起步很晚,但这是Chris Pratt's answer的一个版本,它通过使用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

答案 20 :(得分:1)

我已经扩展了@livskiy的mixin,如下所示:

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字段,该字段将存储对象的状态

答案 21 :(得分:0)

我对@iperelivskiy 的解决方案的看法:大规模地,为每个 _initial 创建 __init__ dict 是昂贵的,而且大多数时间 - 不必要。我稍微更改了 mixin,以便它仅在您明确告诉它这样做时才记录更改(通过调用 instance.track_changes):

from typing import KeysView, Optional
from django.forms import model_to_dict

class TrackChangesMixin:
    _snapshot: Optional[dict] = None

    def track_changes(self):
        self._snapshot = self.as_dict

    @property
    def diff(self) -> dict:
        if self._snapshot is None:
            raise ValueError("track_changes wasn't called, can't determine diff.")
        d1 = self._snapshot
        d2 = self.as_dict
        diffs = [(k, (v, d2[k])) for k, v in d1.items() if str(v) != str(d2[k])]
        return dict(diffs)

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

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

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

答案 22 :(得分: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()

答案 23 :(得分:0)

修改@ 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。这使代码更具有未来性,但更重要的是还包括外键和字段,其中editable = 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))
    )

答案 24 :(得分:0)

来自@ivanlivski的mixin很棒。

我已将其扩展到

  • 确保它适用于十进制字段。
  • 公开属性以简化使用

更新的代码可在此处获得: 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()

答案 25 :(得分:0)

作为SmileyChris回答的扩展,您可以在模型中为last_updated添加日期时间字段,并为检查更改前允许的最大年龄设置某种限制